防抖与节流函数<转>

参考连接:https://www.cnblogs.com/zhuanzhuanfe/p/10633019.html

https://blog.csdn.net/Beijiyang999/article/details/79832604

 

我们经常会处理各种事件,比如常见的click、scroll、 resize等等。仔细一想,会发现像scroll、onchange这类事件会频繁触发,如果我们在回调中计算元素位置、做一些跟DOM相关的操作,引起浏览器回流和重绘,频繁触发回调,很可能会造成浏览器掉帧,甚至会使浏览器崩溃,影响用户体验。

还有以下场景往往由于事件频繁被触发,因而频繁执行DOM操作、资源加载等重行为,导致UI停顿甚至浏览器崩溃。

  1. window对象的resize、scroll事件

  2. 拖拽时的mousemove事件

  3. 射击游戏中的mousedown、keydown事件

  4. 文字输入、自动完成的keyup事件

  实际上对于window的resize事件,实际需求大多为停止改变大小n毫秒后执行后续处理;而其他事件大多的需求是以一定的频率执行后续处理。

  针对这两种需求,常用的解决方案:防抖和节流。

防抖(debounce)

所谓防抖,就是指触发事件后,就是把触发非常频繁的事件合并成一次去执行。即在指定时间内只执行一次回调函数,如果在指定的时间内又触发了该事件,则回调函数的执行时间会基于此刻重新开始计算。

以我们生活中乘车刷卡的情景举例,只要乘客不断地在刷卡,司机师傅就不能开车,乘客刷卡完毕之后,司机会等待几分钟,确定乘客坐稳再开车。如果司机在最后等待的时间内又有新的乘客上车,那么司机等乘客刷卡完毕之后,还要再等待一会,等待所有乘客坐稳再开车。

具体应该怎么去实现这样的功能呢?第一时间肯定会想到使用setTimeout方法,那我们就尝试写一个简单的函数来实现这个功能吧~

 

 

思路
用 setTimeout 实现计时,配合 clearTimeout 实现“重新开始计时”。

 

即只要触发,就会清除上一个计时器,又注册新的一个计时器。直到停止触发 wait 时间后,才会执行回调函数。

 

不断触发事件,就会不断重复这个过程,达到防止目标函数过于频繁的调用的目的。

 

初步实现

function debounce(func, wait) {
    let timeout
    return function () {
        clearTimeout(timeout)
        timeout = setTimeout(func, wait) //返回计时器 ID
    }
}

示意

container.onmousemove = debounce(doSomething, 1000);

注解:关于闭包
每当事件被触发,执行的都是那个被返回的闭包函数。

 

因为闭包带来的其作用域链中引用的上层函数变量声明周期延长的效果,debounce 函数的 settimeout计时器 ID timeout 变量可以在debounce 函数执行结束后依然留存在内存中,供闭包使用。

 

优化:修复
相比于未防抖时的

 

container.onmousemove = doSomething 

防抖优化后,指向 HTMLDivElement 的从 doSomething 函数的 this 变成了闭包匿名函数的 this ,前者变成了指向全局变量。
同理,doSomething 函数参数也接收不到 MouseEvent 事件了。

 

修复代码

function debounce(func, wait) {
    let timeout
    return function () {
        let context = this //传给目标函数
        clearTimeout(timeout)
        timeout = setTimeout(
            () => { func.apply(context, arguments) } //修复
            , wait)
    }
}

温馨提示:

1、上述代码中arguments只会保存事件回调函数中的参数,譬如:事件对象等,并不会保存fn、delayTime

2、使用apply改变传入的fn方法中的this指向,指向绑定事件的DOM元素。

优化:立即执行
相比于 一个周期内最后一次触发后,等待一定时间再执行目标函数;

 

我们有时候希望能实现 在一个周期内第一次触发,就立即执行一次,然后一定时间段内都不能再执行目标函数。

 

这样,在限制函数频繁执行的同时,可以减少用户等待反馈的时间,提升用户体验。

 

代码
在原来基础上,添加一个是否立即执行的功能

function debounce(func, wait, immediate) {
    let time;
    let debounced = function () {
        let context = this;

        if (immediate) {
            let callNow = !time;
            if (callNow) func.apply(context, arguments);
            time = setTimeout(
                () => { time = null } //见注解
                , wait);
        } else {
            if (time) clearTimeout(time);
            time = setTimeout(
                () => { func.apply(context, arguments) }
                , wait);
        }
    }
    return debounced;
}

注解

把保存计时器 ID 的 time 值设置为 null 有两个作用:

 

作为开关变量,表明一个周期结束。使得 callNow 为 true,目标函数可以在新的周期里被触发时被执行
timeout 作为闭包引用的上层函数的变量,是不会自动回收的。手动将其设置为 null ,让它脱离执行环境,一边垃圾收集器下次运行是将其回收。
优化:取消立即执行
添加一个取消立即执行的功能。

 

函数也是对象,也可以为其添加属性。

 

为了添加 “取消立即执行”功能,为 debounced 函数添加了个 cancel 属性,属性值是一个函数

 

debounced.cancel = function () {
    clearTimeout(time)
    time = null
}

示意:

 

var setSomething = debounce(doSomething, 1000, true);

container.onmousemove = setSomething;

document.getElementById("button").addEventListener('click', function () {
    setSomething.cancel();
});

完整代码

function debounce(func, wait, immediate) {
    let time;
    let debounced = function () {
        let context = this;

        if (immediate) {
            let callNow = !time;
            if (callNow) func.apply(context, arguments)
            time = setTimeout(
                () => { time = null } //见注解
                , wait);
        } else {
            if (time) clearTimeout(time);
            time = setTimeout(
                () => { func.apply(context, arguments) }
                , wait);
        }
    }

    debounced.cancel = function () {
        clearTimeout(time);
        time = null;
    }

    return debounced;
}

 

节流(throttle)

所谓节流,是指频繁触发事件时,只会在指定的时间段内执行事件回调,即触发事件间隔大于等于指定的时间才会执行回调函数。

类比到生活中的水龙头,拧紧水龙头到某种程度会发现,每隔一段时间,就会有水滴流出。

说到时间间隔,大家肯定会想到使用setTimeout来实现,在这里,我们使用两种方法来简单实现这种功能:时间戳和setTimeout定时器。

时间戳

var throttle = (fn, delayTime) => {
  var _start = Date.now();
  return function () {
    var _now = Date.now(), context = this, args = arguments;
    if(_now - _start >= delayTime) {
      fn.apply(context, args);
      _start = Date.now();
    }
  }
}

通过比较两次时间戳的间隔是否大于等于我们事先指定的时间来决定是否执行事件回调。

定时器

var throttle = function (fn, delayTime) {
  var flag;
  return function () {
    var context = this, args = arguments;
    if(!flag) {
      flag = setTimeout(function () {
        fn.apply(context, args);
        flag = false;
      }, delayTime);
    }
  }
}

在上述实现过程中,我们设置了一个标志变量flag,当delayTime之后执行事件回调,便会把这个变量重置,表示一次回调已经执行结束。
对比上述两种实现,我们会发现一个有趣的现象:

1、使用时间戳方式,页面加载的时候就会开始计时,如果页面加载时间大于我们设定的delayTime,第一次触发事件回调的时候便会立即fn,并不会延迟。如果最后一次触发回调与前一次触发回调的时间差小于delayTime,则最后一次触发事件并不会执行fn;

2、使用定时器方式,我们第一次触发回调的时候才会开始计时,如果最后一次触发回调事件与前一次时间间隔小于delayTime,delayTime之后仍会执行fn。

这两种方式有点优势互补的意思,哈哈~

我们考虑把这两种方式结合起来,便会在第一次触发事件时执行fn,最后一次与前一次间隔比较短,delayTime之后再次执行fn。

想法简单实现如下:

function throttle(fn, wait) {
    let timer;
    let lastTime;
    return function () {
        const context = this;
        const nowTime = new Date();
        if (nowTime - lastTime - wait >= 0) {
            if (timer) {
                clearTimeout(timer);
                timer = null;
            }
            fn.apply(context, agruments);
            lastTime = nowTime;
        } else if (!timer) {
            timer = setTimeout(() => {
                fn.apply(context, agruments);
            }, wait);
        }
    };
}

通过上面的分析,可以很明显的看出函数防抖和函数节流的区别:

频繁触发事件时,函数防抖只会在最后一次触发事件只会才会执行回调内容,其他情况下会重新计算延迟事件,而函数节流便会很有规律的每隔一定时间执行一次回调函数。

requestAnimationFrame

之前,我们使用setTimeout简单实现了防抖和节流功能,如果我们不考虑兼容性,追求精度比较高的页面效果,可以考虑试试html5提供的API--requestAnimationFrame。

与setTimeout相比,requestAnimationFrame的时间间隔是有系统来决定,保证屏幕刷新一次,回调函数只会执行一次,比如屏幕的刷新频率是60HZ,即间隔1000ms/60会执行一次回调。

var throttle = function(fn, delayTime) {
  var flag;
  return function() {
    if(!flag) {
      requestAnimationFrame(function() {
        fn();
        flag = false;
      });
      flag = true;
    }
  }

上述代码的基本功能就是保证在屏幕刷新的时候(对于大多数的屏幕来说,大约16.67ms),可以执行一次回调函数fn。使用这种方式也存在一种比较明显的缺点,时间间隔只能跟随系统变化,我们无法修改,但是准确性会比setTimeout高一些。

注意:

  1. 防抖和节流只是减少了事件回调函数的执行次数,并不会减少事件的触发频率。

  2. 防抖和节流并没有从本质上解决性能问题,我们还应该注意优化我们事件回调函数的逻辑功能,避免在回调中执行比较复杂的DOM操作,减少浏览器reflow和repaint。

上面的示例代码比较简单,只是说明了基本的思路。目前已经有工具库实现了这些功能,比如underscore,考虑的情况也会比较多,大家可以去查看源码,学习作者的思路,加深理解。

underscore的debounce方法源码:

_.debounce = function(func, wait, immediate) {
    var timeout, result;

    var later = function(context, args) {
      timeout = null;
      if (args) result = func.apply(context, args);
    };

    var debounced = restArguments(function(args) {
      if (timeout) clearTimeout(timeout);
      if (immediate) {
        var callNow = !timeout;
        timeout = setTimeout(later, wait);
        if (callNow) result = func.apply(this, args);
      } else {
        timeout = _.delay(later, wait, this, args);
      }

      return result;
    });

    debounced.cancel = function() {
      clearTimeout(timeout);
      timeout = null;
    };

    return debounced;
  };

underscore的throttle源码:

_.throttle = function(func, wait, options) {
    var timeout, context, args, result;
    var previous = 0;
    if (!options) options = {};

    var later = function() {
      previous = options.leading === false ? 0 : _.now();
      timeout = null;
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    };

    var throttled = function() {
      var now = _.now();
      if (!previous && options.leading === false) previous = now;
      var remaining = wait - (now - previous);
      context = this;
      args = arguments;
      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) {
        timeout = setTimeout(later, remaining);
      }
      return result;
    };

    throttled.cancel = function() {
      clearTimeout(timeout);
      previous = 0;
      timeout = context = args = null;
    };

    return throttled;
  };

posted on 2019-11-09 23:56  王泽平  阅读(877)  评论(1编辑  收藏  举报

导航