还有人不知道防抖节流?(三)

·  阅读 140

昨天分析了防抖函数的源码,今天来看看节流。

所谓节流,其实就像是控制水龙头的水不要一下子流太多,所以控制它让它缓缓按一定的流速去流下来。也就是在持续触发的情况下控制函数按一定的时间持续执行。

先来看看它是怎么使用的。

// 用节流函数包装要执行的函数doSomething
// _.throttle(func, [wait=0], [options={}])
let doSome = throttle(doSomeThing, 1000, {
    leading: true,
    trailing: true
})
// 鼠标移动到container时就触发
container.onmousemove = doSome;
复制代码

可以发现,它除了需要执行函数,延迟时间,还有第三个参数,这是一个对象。

opitons参数中定义了一些选项:

  • leading,函数在每个等待时延的开始被调用,默认值为false
  • trailing,函数在每个等待时延的结束被调用,默认值是true

根据leadingtrailing的组合,可以实现不同的调用效果:

  • 第一种:leading-true, trailing-false:只在延时开始时调用,延时结束后不调用
  • 第二种:leading-false,trailing-true:默认情况,即在延时结束后才会调用函数
  • 第三种:leading-true,trailing-true:在延时开始时就调用,延时结束后也会调用

注意:没有leading-false,trailing-false的情况噢!

我们可以先来看看第一种情况是怎么实现的。要在一开始立即调用,结束的时候不调用,我们可以利用时间戳来实现。刚开始要满足现在时间与之前时间之差要大于延迟时间,后面触发的时候如果差值没有超过延迟时间,就不执行最后一次了。

刚开始我们可以直接获取当前时间,把之前时间定为0,此时两个值之差一定是大于延迟时间的,也就是满足可以立即执行。

// 刚开始会立即触发,但是一旦在未达到触发时间时停止触发,就不执行函数。
// 相当于:
// let doSome = _.throttle(doSomeThing, 1000, {
//     leading: true,
//     trailing: false
// });
// 第一次触发,最后不会调用触发函数
function throttle(func, wait){
    let context, args;

    // 之前的时间戳
    let old = 0;

    return function(){
        // 改变内部this指向
        context = this;
        // 将参数传给真正执行的函数,目的是获取event事件对象
        args = arguments;

        // 获取当前的时间戳
        let now = new Date().valueOf();

        // 如果当前时间减去前一次触发事件等于延迟时间,则执行函数
        if(now - old > wait){
            // 立即执行
            func.apply(context, args);
            // 同时将当前时间赋值给旧时间
            old = now;
        }
    }

复制代码

接下来看看第二种情况。

// 相当于:
// let doSome = _.throttle(doSomeThing, 1000, {
//     leading: false,
//     trailing: true
// });
// 第一次不会触发,最后一次会触发
function throttle(func, wait){
    let context, args, timeout;

    return function(){
        // 改变内部this指向
        context = this;
        // 将参数传给真正执行的函数,目的是获取event事件对象
        args = arguments;

        // 如果还没有设置定时器,就设置定时器延时执行
        if(!timeout){
            timeout = setTimeout(() => {
                // 等到到达延迟时间的时候,就把延时器置空,然后执行函数。
                // 定时器置空之后,如果继续触发,就会继续设置定时器,循环这个过程
                timeout = null; 
                func.apply(context, args);
            }, wait);
        }
    }
}
// 我们可以发现,刚开始触发的时候是没有立即执行的,因为是设置定时器延时执行。
// 但是最后的时候如果定时器置空之后继续触发这个函数,即使还没有到达延时时间,还是会重新设置定时器,等到到达延时时间后会再执行一次。
复制代码

最后我们来看看源码是怎么实现将这两个结合在一起的!

_.throttle = function(func, wait, options) {

  var timeout, context, args, result;
  
  // 之前的时间戳
  var previous = 0;
  
  // 如果没有传第三个参数,就将其设置为空对象,防止访问出现问题
  if (!options) options = {};
  
  // 清空定时器,立即执行,注意这个函数是在!timeout && options.trailing !== false情况下触发的,也就是顾尾而且此时定时器为空
  var later = function() {
  
  // 如果leading为false,也就是刚开始没有立即执行,顾尾不顾头,那就设置为0,否则顾尾也顾头,重新获取当前时间将其赋给他,更新previous的值
    previous = options.leading === false ? 0 : _.now();
    
    // 清空定时器
    timeout = null;
    // 立即执行函数
    result = func.apply(context, args);
    // 防止内存泄漏
    if (!timeout) context = args = null;
  };

  var throttled = function() {
  // 获取当前时间
    var now = _.now();
    
    // 如果此时leading为false,也就是不立即执行,同时previous为空,意味着第一次执行或者是设置了定时器(later里面有判断),那么就把之前时间设置为现在时间,防止时间差影响判断。
    // 此时remaining结果会等于wait,不会进入第一个判断
    if (!previous && options.leading === false) previous = now;
    
    // 距下一次执行剩下的时间:延时时间减去(现在时间和过去时间的差值)
    var remaining = wait - (now - previous);
    
    // 改变内部this指向
    context = this;
    // 将参数传给真正执行的函数,目的是获取event事件对象
    args = arguments;
    
    // 剩下时间小于等于0(第一种情况,通过时间戳触发执行)或者大于延时时间(过去时间比现在时间还要晚)
    if (remaining <= 0 || remaining > wait) {
    // 已经有定时器了就直接清空,防止时间戳方式和定时器方式冲突,同时触发两种
      if (timeout) {
        clearTimeout(timeout);
        timeout = null;
      }
      // 将现在时间赋值给之前的时间
      previous = now;
      // 立即执行并将执行结果返回给result
      result = func.apply(context, args);
      // 防止内存泄漏
      if (!timeout) context = args = null;
      
    } else if (!timeout && options.trailing !== false) {
    // 相当于第二种情况,顾尾而且此时定时器为空了,将重新设置定时器
      timeout = setTimeout(later, remaining);
    }
    
    // 返回原来执行函数的结果
    return result;
  };
  
  // 如果wait延迟执行的时间比较长的话,可以调用cancel中途取消函数执行。
  throttled.cancel = function() {
    clearTimeout(timeout);	// 清空定时器
    previous = 0;	// 把之前的时间也重置为0
    timeout = context = args = null;	// 防止内存泄漏
  };	
  
  // 返回包装后的函数对象
  return throttled;
};
复制代码
分类:
前端
标签: