throttle-debounce 源码解读

822 阅读2分钟

概念

throttle(节流)

一个函数在一定时间内只能执行一次。

debounce(防抖)

触发事件后,在 n 秒后只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数的执行时间。

源码学习

throttle 的实现

function throttle(delay, callback) {
  let timeoutID;
  let lastExec = 0;

  function wrapper() {
    const self = this;
    const elapsed = Number(new Date()) - lastExec;
    const args = arguments;

    function exec() {
      lastExec = Number(new Date());
      callback.apply(self, args);
    }

    clearTimeout(timeoutID);

    if (elapsed > delay) {
      exec();
    } else {
      timeoutID = setTimeout(exec, delay - elapsed);
    }
  }

  return wrapper;
}

整个代码的逻辑十分清晰,一共只有三步:

  1. 计算距离最近一次函数执行后经过的时间 elapsed,并清除之前设置的计时器。
  2. 如果经过的时间大于设置的时间间隔 delay,那么立即执行函数,并更新最近一次函数的执行时间。
  3. 如果经过的时间小于设置的时间间隔 delay,那么通过 setTimeout 设置一个计数器,让函数在 delay - elapsed 时间后执行。

源码并不难理解,不过需要关注一下 this 的使用:

function throttle(delay, callback) {
    // ...
    function wrapper() {
        const self = this;
        const args = arguments;
        // ...
        
        function exec() {
            // ...
            callback.apply(self, args);
        }
        
    }
}

在上面的代码中,通过 self 变量临时保存 this 的值,从而在 exec 函数中通过 callback.apply(self, args) 传入正确的 this 值,这种做法在闭包相关的函数调用中十分常用。正因为这里对 this 的处理,所以可以实现下面的能力:

function foo() { console.log(this.name);  }
​
const fooWithName = throttle(200, foo);
​
const obj = {name: 'elvin'};
​
fooWithName.call(obj, 'elvin');
​
// => 'elvin'

debounce 实现

由于 debouncen 只是往后推延函数的执行时间,并不具有 throttle 每隔一段时间一定会执行的能力,所以其实现起来更加简单:

function debounce(delay, callback) {
  let timeoutID;
  
  function wrapper() {
    const self = this;
    const args = arguments;
    
    function exec() {
      callback.apply(self, args);
    }
    
    clearTimeout(timeoutID);
    timeoutID = setTimeout(exec, delay);
  }
  return wrapper;
}

将上述代码与 throttle 实现的代码相比,可以发现其就是去除了 elapsed 相关逻辑后的代码,其余大部分代码一模一样,所以 debounce 函数可以借助 throttle 函数实现(throttle-debounce 源代码中也是这样做的),throttle 函数也可以借助 debounce 函数实现。

使用场景举例

throttle 和 debounce 适用于用户短时间内频繁执行某一相同操作的场景,例如:

  • 用户拖动浏览器窗口改变窗口大小,触发 resize 事件。
  • 用户移动鼠标,触发 mousemove 等事件。
  • 用户在输入框内进入输入,触发 keydown | keypress | keyinput | keyup 等事件。
  • 用户滚动屏幕,触发 scroll 事件。
  • 用户在点击按钮后,由于 API 请求耗时未立即看到响应,可能会不断点击按钮触发 click 事件。

使用

import { throttle } from 'throttle-debounce'

this.scrollThrottle = throttle(300, this.mainScroll)

method: {
    throttleFlow: throttle(200, function() {
        this.doSomething()
    }
}

待完善

iOS 版本的实现。

引用

  1. zhuanlan.zhihu.com/p/43410181
  2. www.runoob.com/w3cnote/js-…
  3. demo.nimius.net/debounce_th…