防抖和节流

在前端开发中,有一部分用户行为会频繁的触发事件,而对于DOM操作,资源加载等耗费性能的处理,很可能会导致卡顿,甚至浏览器的崩溃。防抖和节流就是为了解决这一类的问题。

window.onscroll  = function () {
    //滚动条位置
    let scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
  console.log(‘滚动条位置:‘ + scrollTop);
}
  • 效果如下

从效果上,我们可以看到,在页面滚动的时候,会在短时间内触发多次绑定事件。

我们知道DOM操作是很耗费性能的,如果在监听中,做了一些DOM操作,那无疑会给浏览器造成大量性能损失。

下面我们进入主题,一起来探究,如何对此进行优化。

防抖:

  • 理解:在车站上车,人员上满了车才发走重点是人员上满触发一次。
  • 场景:实时搜索,拖拽。
  • 原因
  • 定义:多次触发事件后,事件处理函数只执行一次,并且是在触发操作结束时执行。
  • 原理:对处理函数进行延时操作,若设定的延时到来之前,再次触发事件,则清除上一次的延时操作定时器,重新定时。

防抖的思想如下:

参考链接

  • 借助事件循环队列和setTimeout来实现只有空闲的时候才去处理回调函数
  • 使用setTimeout主要是为了使得处理方法挂在事件循环队列后面,保证事件循环队列中的前面的一些操作有时间进行
// 计时器
var timer = false;
//
window.onscroll = function(){
    clearTimeout(timer);
    timer = setTimeout(function(){
        console.log("防抖");
        console.log(new Date());
    },300);
};

为什么要clearTimeout

每次onscroll的时候,先清除掉计时器.如果不清楚,会导致多次触发的时候,其实是把好多次的处理方法放在某个时间点后一起执行。

比如下面:

    for (var i = 0; i < 10; i++) {
        (function (i) {
            setTimeout(function () {
                console.log(i);
            }, 3000);
        })(i);
    }

上面代码在3秒后会一起输出 1,2,3,4,5,6,7,8,9

而下面的代码,只会输出9

    var timer2 = false;
    for (var i = 0; i < 10; i++) {
        clearTimeout(timer2);
        (function (i) {
            timer2 = setTimeout(function () {
                console.log(i);
            }, 3000);
        })(i);
    }

这是因为,每次我将上次的timer给清除掉了,也就是我如果后面同样有处理函数的话,那我就用后面的定时器。

前面定时器没啥用了,所以直接clearTimeout保证了这种实现。

在解决onscroll问题的时候,如果自己观察console可以发现,防抖保证了滚动停止的时候,才会进行处理,因为滚动停止了,没有scroll事件了,最后一次timer会被保留,从而进行调用

  • 实现:
//每一次都要清空定时器,重新设置上计时器值,使得计时器每一次都重新开始,直到最后满足条件并且等待delay时间后,才开始执行handler函数。

 let timer ;
   window.onscroll = function(){

       if(timer){
           clearTimeout(timer);
       }
       timer = setTimeout(function(){
           //滚动条位置
           let scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
           console.log(‘滚动条位置‘+ scrollTop);
           timer = null;

       },2000)
       console.log(timer);//一滚动,timer就有对应的数值,即
       //setTimeout会返回数值,clearTimeout(这个数值),就相当于清除这个定时器
  • 效果如下:滚动结束触发事件

    简单版防抖封装

// func是用户传入需要防抖的函数
// wait是等待时间
const debounce = (func, wait = 50) => {
  // 缓存一个定时器id
  let timer = 0
  // 这里返回的函数是每次用户实际调用的防抖函数
  // 如果已经设定过定时器了就清空上一次的定时器
  // 开始一个新的定时器,延迟执行用户传入的方法
  return function(...args) {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      func.apply(this, args)
    }, wait)
  }
}
window.onscroll = debounce(function(){
    let scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
console.log(‘滚动条位置:‘ + scrollTop);
    }
,200)
// 不难看出如果用户调用该函数的间隔小于wait的情况下,上一次的时间还未到就被清除了,并不会执行函数

这是一个简单版的防抖,但是有缺陷,这个防抖只能在最后调用。一般的防抖会有immediate选项,表示是否立即调用。这两者的区别,举个栗子来说:

例如在搜索引擎搜索问题的时候,我们当然是希望用户输入完最后一个字才调用查询接口,这个时候适用延迟执行的防抖函数,它总是在一连串(间隔小于wait的)函数触发之后调用。

例如用户给interviewMap点star的时候,我们希望用户点第一下的时候就去调用接口,并且成功之后改变star按钮的样子,用户就可以立马得到反馈是否star成功了,这个情况适用立即执行的防抖函数,它总是在第一次调用,并且下一次调用必须与前一次调用的时间间隔大于wait才会触发。

实现一个带有立即执行选项的防抖函数

// 这个是用来获取当前时间戳的
function now() {
  return +new Date()
}
/**
 * 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行
 *
 * @param  {function} func        回调函数
 * @param  {number}   wait        表示时间窗口的间隔
 * @param  {boolean}  immediate   设置为ture时,是否立即调用函数
 * @return {function}             返回客户调用函数
 */
function debounce (func, wait = 50, immediate = true) {
  let timer, context, args

  // 延迟执行函数
  const later = () => setTimeout(() => {
    // 延迟函数执行完毕,清空缓存的定时器序号
    timer = null
    // 延迟执行的情况下,函数会在延迟函数中执行
    // 使用到之前缓存的参数和上下文
    if (!immediate) {
      func.apply(context, args)
      context = args = null
    }
  }, wait)

  // 这里返回的函数是每次实际调用的函数
  return function(...params) {
    // 如果没有创建延迟执行函数(later),就创建一个
    if (!timer) {
      timer = later()
      // 如果是立即执行,调用函数
      // 否则缓存参数和调用上下文
      if (immediate) {
        func.apply(this, params)
      } else {
        context = this
        args = params
      }
    // 如果已有延迟执行函数(later),调用的时候清除原来的并重新设定一个
    // 这样做延迟函数会重新计时
    } else {
      clearTimeout(timer)
      timer = later()
    }
  }
}

整体函数总结一下。

对于==按钮防点击来==说的实现:如果函数是立即执行的,就立即调用,如果函数是延迟执行的,就缓存上下文和参数,放到延迟函数中去执行。一旦我开始一个定时器,只要我定时器还在,你每次点击我都重新计时。一旦你点累了,定时器时间到,定时器重置为 null,就可以再次点击了。

对于延时执行函数来说的实现:清除定时器ID,如果是延迟调用就调用函数

函数防抖的适用性:

通过上面的例子,我们知道我们可以通过函数防抖,解决了多次触发事件时的性能问题。

比如,我们在监听滚动条位置,控制是否显示返回顶部按钮时,就可以将防抖函数应用其中。

  但依然有些功能并不适用:

当我们做图片懒加载(lazyload)时,需要通过滚动位置,实时显示图片时,如果使用防抖函数,懒加载(lazyload)函数将会不断被延时,

只有停下来的时候才会被执行,对于这种需要实时触发事件的情况,就显得不是很友好了。

下面开始介绍函数节流,通过设定时间片,控制事件函数间断性的触发。

节流:

  • 理解:大于等于10分钟发一次车,重点是一定间隔时间就会触发一次。

    -(即预定一个函数只有在大于等于执行周期时才会执行,周期内不执行)。

  • 场景:窗口调整(调整大小),页面滚动(滚动),抢购时疯狂点击(鼠标按下)
  • 定义:触发函数事件后,短时间间隔内无法连续调用,只有上一次函数执行后,过了规定的时间间隔,才能进行下一次的函数调用。
  • 原理:对处理函数进行延时操作,若设定的延时到来之前,再次触发事件,则清除上一次的延时操作定时器,重新定时。

  scroll 的一个简单例子

let startTime = Date.now(); //开始时间
let time = 500; //间隔时间
let timer;
window.onscroll = function throttle(){
    let currentTime = Date.now();
    if(currentTime - startTime >= time){
        let scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
        console.log(‘滚动条位置:‘ + scrollTop);
        startTime = currentTime;
    }else{
        clearTimeout(timer);
        timer = setTimeout(function () {
            throttle()
        }, 50);
    }
}

-  效果如下:每隔500毫秒触发一次事件

 

  • 封装实现:
/**
 * 节流函数
 * @param method 事件触发的操作
 * @param mustRunDelay 间隔多少毫秒需要触发一次事件
 */
function throttle(method, mustRunDelay) {
    let timer,
        args = arguments,
        start;
    return function loop() {
        let self = this;
        let now = Date.now();
        if(!start){
            start = now;
        }
        if(timer){
            clearTimeout(timer);
        }
        if(now - start >= mustRunDelay){
            method.apply(self, args);
            start = now;
        }else {
            timer = setTimeout(function () {
                loop.apply(self, args);
            }, 50);
        }
    }
}
window.onscroll = throttle(function () {
    let scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
    console.log(‘滚动条位置:‘ + scrollTop);
},800)

用时间戳+定时器,当第一次触发事件时马上执行事件处理函数,最后一次触发事件后也还会执行一次事件处理函数

上面的代码一刷新就触发事件,即打印出‘滚动条位置"

这个是关于忽略开始函数的的调用或者忽略结尾函数的节流封装函数

**
 * underscore 节流函数,返回函数连续调用时,func 执行频率限定为 次 / wait
 *
 * @param  {function}   func      回调函数
 * @param  {number}     wait      表示时间窗口的间隔
 * @param  {object}     options   如果想忽略开始函数的的调用,传入{leading: false}。
 *                                如果想忽略结尾函数的调用,传入{trailing: false}
 *                                两者不能共存,否则函数不能执行
 * @return {function}             返回客户调用函数
 */
_.throttle = function(func, wait, options) {
    var context, args, result;
    var timeout = null;
    // 之前的时间戳
    var previous = 0;
    // 如果 options 没传则设为空对象
    if (!options) options = {};
    // 定时器回调函数
    var later = function() {
      // 如果设置了 leading,就将 previous 设为 0
      // 用于下面函数的第一个 if 判断
      previous = options.leading === false ? 0 : _.now();
      // 置空一是为了防止内存泄漏,二是为了下面的定时器判断
      timeout = null;
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    };
    return function() {
      // 获得当前时间戳
      var now = _.now();
      // 首次进入前者肯定为 true
      // 如果需要第一次不执行函数
      // 就将上次时间戳设为当前的
      // 这样在接下来计算 remaining 的值时会大于0
      if (!previous && options.leading === false) previous = now;
      // 计算剩余时间
      var remaining = wait - (now - previous);
      context = this;
      args = arguments;
      // 如果当前调用已经大于上次调用时间 + wait
      // 或者用户手动调了时间
      // 如果设置了 trailing,只会进入这个条件
      // 如果没有设置 leading,那么第一次会进入这个条件
      // 还有一点,你可能会觉得开启了定时器那么应该不会进入这个 if 条件了
      // 其实还是会进入的,因为定时器的延时
      // 并不是准确的时间,很可能你设置了2秒
      // 但是他需要2.2秒才触发,这时候就会进入这个条件
      if (remaining <= 0 || remaining > wait) {
        // 如果存在定时器就清理掉否则会调用二次回调
        if (timeout) {
          clearTimeout(timeout);
          timeout = null;
        }
        previous = now;
        result = func.apply(context, args);
        if (!timeout) context = args = null;
      } else if (!timeout && options.trailing !== false) {
        // 判断是否设置了定时器和 trailing
        // 没有的话就开启一个定时器
        // 并且不能不能同时设置 leading 和 trailing
        timeout = setTimeout(later, remaining);
      }
      return result;
    };
  };

原文地址:https://www.cnblogs.com/chaimi/p/10263066.html

时间: 2024-08-29 23:58:07

防抖和节流的相关文章

【源码】防抖和节流源码分析

前言 防抖.节流主要用于频繁事件触发,例如鼠标移动.改变窗口大小等.lodash等函数库具备相对应的api, _.debounce ._.throttle. 核心技术:闭包. 区别: 防抖, 连续触发, 第一次和最后一次触发有效 节流, 一段时间内仅触发一次(第一次) 本文以防抖函数为例, 详细说明. 实现 原理, 计时器存储状态. function debounce(fn, delay) { return function() { console.log(fn.timer, count2);

详谈js防抖和节流

本文由小芭乐发表 0. 引入 首先举一个例子: 模拟在输入框输入后做ajax查询请求,没有加入防抖和节流的效果,这里附上完整可执行代码: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>没有防抖</title> <style type="text/css"></sty

函数防抖与节流

1.为什么会有函数的防抖与节流及使用场景 在进行窗口的resize.scroll,输入框内容校验或者向后端发起ajax等操作时, 如果事件处理函数调用的频率无限制,会加重浏览器的负担,导致用户体验非常糟糕. 此时我们可以采用debounce(防抖)和throttle(节流)的方式来减少调用频率,同时又不影响实际效果. 2.函数防抖与函数节流的概念及实现原理 函数防抖(debounce): 当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定的时间到来之前,又一次触发了

防抖和节流函数最简版

防抖和节流函数最简版 防抖: 多次触发事件只执行一次(适用于断续的事件, 比如 click.input) 1 function debounce(fn, time) { 2 let timeout 3 return () => { 4 if (timeout) { 5 clearTimeout(timeout) 6 } 7 timeout = setTimeout({ 8 fn 9 }, time) 10 } 11 } 节流: 在指定时间内多次触发事件只执行一次(适用于连续的事件, 比如 scr

前端进击的巨人(八):浅谈函数防抖与节流

本篇课题,或许早已是烂大街的解读文章.不过春招系列面试下来,不少伙伴们还是似懂非懂地栽倒在(-面试官-)深意的笑容之下,权当温故知新. JavaScript的执行过程,是基于栈来进行的.复杂的程序代码被封装到函数中,程序执行时,函数不断被推入执行栈中.所以 "执行栈" 也称 "函数执行栈". 函数中封装的代码块,一般都有相对复杂的逻辑处理(计算/判断),例如函数中可能会涉及到 DOM 的渲染更新,复杂的计算与验证, Ajax 数据请求等等. 前端页面的操作权,大部分

闲聊前端性能----防抖、节流、重绘与回流。

在最近,小米9卖的特别火,在官方抢购的时候基本都是一点既空.这不禁让我想到了,官网是怎样控制顾客不停点击发起请求而不导致官网崩溃的呢?这由此引出了前端性能的优化中的----防抖和节流.在闲聊完后你就会发现有些时候在抢购商品的时候,你用鼠标在几秒钟不停的按了数十次,或许它仅仅是发送了你第一次点击抢购的那个请求.所以说 抢购时间内的第一次点击尤为关键! 下面来介绍一下什么是防抖!        防抖:任务频繁触发的情况下,只有任务触发的间隔超过制定的时间间隔的时候,任务才会被执行. 下面引用一下知乎

深入理解JS防抖与节流

参考博客:JS防抖和节流,感谢作者的用心分享 日常开发过程中,滚动事件做复杂计算频繁调用回调函数很可能会造成页面的卡顿,这时候我们更希望把多次计算合并成一次,只操作一个精确点,JS把这种方式称为debounce(防抖)和throttle(节流) 函数防抖 当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定时间到来之前,又触发了事件,就重新开始延时.也就是说当一个用户一直触发这个函数,且每次触发函数的间隔小于既定时间,那么防抖的情况下只会执行一次. function

JS的防抖和节流

数个月之前,在一次前端的性能优化中,接触到了JS中防抖和节流,一开始还不明白他们的应用在哪里,可后来才知道,这是前端中最基础的性能优化,在绑定 scroll .resize 这类事件时,当它发生时,它被触发的频次非常高,间隔很近.如果事件中涉及到大量的位置计算.DOM 操作.元素重绘等工作且这些工作无法在下一个 scroll 事件触发前完成,就会造成浏览器掉帧.加之用户鼠标滚动往往是连续的,就会持续触发 scroll 事件导致掉帧扩大.浏览器 CPU 使用率增加.用户体验受到影响.尤其是在涉及与

2019 面试准备 - JS 防抖与节流 (超级 重要!!!!!)

Hello 小伙伴们,如果觉得本文还不错,记得给个 star , 你们的 star 是我学习的动力!GitHub 地址 本文涉及知识点: 防抖与节流 重绘与回流 浏览器解析 URL DNS 域名解析 TCP 三次握手与四次挥手 浏览器渲染页面 在本文中,jsliang 会讲解通过自我探索后关于上述知识点的个人理解,如有纰漏.疏忽或者误解,欢迎各位小伙伴留言指出. 如果小伙伴对文章存有疑问,想快速得到回复. 或者小伙伴对 jsliang 个人的前端文档库感兴趣,也想将自己的前端知识整理出来. 欢迎