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

549 阅读4分钟

第一篇介绍了防抖节流函数的原理和常见应用场景,接下来我们来看看具体实现。

这次先介绍防抖函数。

underscore.js中它的源码如下:

  _.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;
  };

你会发现里面有一个函数我们还没了解它的实现,首先我们先来看看这个函数restArguments的源码。

var restArguments = function(func, startIndex) {
  startIndex = startIndex == null ? func.length - 1 : +startIndex;
  return function() {
    var length = Math.max(arguments.length - startIndex, 0),
        rest = Array(length),
        index = 0;
    for (; index < length; index++) {
      rest[index] = arguments[index + startIndex];
    }
    switch (startIndex) {
      case 0: return func.call(this, rest);
      case 1: return func.call(this, arguments[0], rest);
      case 2: return func.call(this, arguments[0], arguments[1], rest);
    }
    var args = Array(startIndex + 1);
    for (index = 0; index < startIndex; index++) {
      args[index] = arguments[index];
    }
    args[startIndex] = rest;
    return func.apply(this, args);
  };
};

是不是觉得少了点什么?嗯,少了注释!

先别着急,我们来看看这个函数的作用是什么。

在ES6中,我们可以使用rest接受函数的剩余参数,并且以数组的方式呈现。

function func(a, ...rest) {
    // ...
}

func(1, 2, 3, 4); 
// 其中a参数值为1,rest参数为 [2, 3, 4]

在underScore中,因为不确定执行的环境,underScore内部自己实现了一个restArguments方法,就是用es5的方式来实现,可以让我们可以在大多数浏览器环境下使用剩余参数语法。

使用方法:

function func(a, ...rest) {
    // ...
}

const resFun = _.restArguments(func);
resFun(1, 2, 3, 4); 
// 其中a参数值为1,rest参数为 [2, 3, 4]

以下配上注释啦!

function test(a, b) {
	// ...
}

test(1,2,3,4)
console.log(test.length) // 3
var restArguments = function(func, startIndex) {

// func.length是形参的个数,以上面的例子的话是2。
// 已经有startIndex的话就直接用,没有的话就用func.len找到最后参数所在的位置
  startIndex = startIndex == null ? func.length - 1 : +startIndex;
  
  return function() {
  
  // arguments.length是实际参数的个数,以上面的例子的话是4
  // 有可能实际参数大于形式参数,所以arguments.length - startIndex可能小于0,下面取最大值是防止出现负值的情况
    var length = Math.max(arguments.length - startIndex, 0),
    
        rest = Array(length),
        index = 0;
        
    // 循环拿到剩余参数数组
    for (; index < length; index++) {
      rest[index] = arguments[index + startIndex];
    }
    
    // 接下来要把参数传递给func,也就是执行该函数原本的功能
   
   // 使用call方法实现
    switch (startIndex) {
      // 剩余参数前面没有参数
      case 0: return func.call(this, rest);
      // 剩余参数前面有一个参数
      case 1: return func.call(this, arguments[0], rest);
      // 剩余参数前面有两个参数
      case 2: return func.call(this, arguments[0], arguments[1], rest);
    }
    
    // 你会发现上面使用call方法的时候如果剩余参数前面剩余的参数很多,是没办法一个一个传递的!
    
    // 使用apply方法实现,需要把剩余参数数组和剩余参数前面的参数放在一个数组里面。
    var args = Array(startIndex + 1);
    for (index = 0; index < startIndex; index++) {
      args[index] = arguments[index];
    }
    args[startIndex] = rest;
    return func.apply(this, args);
  };
};

// 最后!你一定发现了,竟然可以用apply解决,干嘛还要多此一举写多个call方式?
// 其实,这里做了一个性能优化,因为call的性能比apply的高。

OK,从上面我们可以了解到restArguments函数,给它传递一个函数以及一个多余参数开始索引(startIndex)作为参数,它会返回一个函数,我们在调用返回的函数时,开始索引之后的多余参数会被放入到数组中,然后一并传递给restArgs的第一个参数函数调用(作为最后一个参数)。

有时候我们所写的函数不确定有多少个要传递的参数,这样在函数内部实现参数处理时就会比较棘手。这时候使用这个函数进行处理之后,可以把这些剩余的参数组合成数组添加到第一个参数的尾部。

接下来我们来具体看看debounce函数吧!

  // 返回一个定时器延迟函数,这个看看就可以知道了吧!
  _.delay = restArguments(function(func, wait, args) {
    return setTimeout(function() {
      return func.apply(null, args);
    }, wait);
  });

  _.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) {	// 如果立即执行参数是true,表明先立刻执行一次
      
      	// 定义callNow,根据它判断要不要直接执行一次,跟timeout定时器绑定起来,是为了避免在第一次立即执行之后,已经有定时器了,但函数会一直触发执行。
        var callNow = !timeout;
        
        // 设置定时器延迟指定时间执行
        timeout = setTimeout(later, wait);
        
        // 如果需要先立即执行一次,现在是第一次触发,那么立即执行函数一次
        if (callNow) result = func.apply(this, args); 
        
      } else {	// 如果立即执行参数是false
      	// 重置定时器,延迟指定时间执行
        timeout = _.delay(later, wait, this, args);
      }
	
    // 如果func函数本身是有返回值的,也要返回去
      return result;
    });
	
    // 如果我的wait延迟时间很长!我等不及想取消这个函数的执行!这时候就可以调用cancel取消掉啦
    debounced.cancel = function() {
      clearTimeout(timeout);	// 清空定时器
      timeout = null;	// 防止内存泄漏
    };
    
    // 返回处理后的函数对象
    return debounced;
  };

嗯,明天再更throttle函数~