sisterAn / JavaScript-Algorithms

基础理论+JS框架应用+实践,从0到1构建整个前端算法体系
5.51k stars 634 forks source link

手写一个节流函数 throttle #92

Open sisterAn opened 4 years ago

sisterAn commented 4 years ago

debouncethrottle 是开发中常用的高阶函数,作用都是为了防止函数被高频调用,换句话说就是,用来控制某个函数在一定时间内执行多少次。

使用场景

比如绑定响应鼠标移动、窗口大小调整、滚屏等事件时,绑定的函数触发的频率会很频繁。若稍处理函数微复杂,需要较多的运算执行时间和资源,往往会出现延迟,甚至导致假死或者卡顿感。为了优化性能,这时就很有必要使用 debouncethrottle 了。

debouncethrottle 区别

防抖 (debounce) :多次触发,只在最后一次触发时,执行目标函数。

节流(throttle):限制目标函数调用的频率,比如:1s内不能调用2次。

手写一个 throttle

实现方案有以下两种:

这里我们采用第一种方案来实现,通过闭包保存一个 previous 变量,每次触发 throttle 函数时判断当前时间和 previous 的时间差,如果这段时间差小于等待时间,那就忽略本次事件触发。如果大于等待时间就把 previous 设置为当前时间并执行函数 fn。

我们来一步步实现,首先实现用闭包保存 previous 变量。

const throttle = (fn, wait) => {
    // 上一次执行该函数的时间
  let previous = 0
  return function(...args) {
    console.log(previous)
    ...
  }
}

执行 throttle 函数后会返回一个新的 function ,我们命名为 betterFn

const betterFn = function(...args) {
  console.log(previous)
    ...
}

betterFn 函数中可以获取到 previous 变量值也可以修改,在回调监听或事件触发时就会执行 betterFn ,即 betterFn(),所以在这个新函数内判断当前时间和 previous 的时间差即可。

const betterFn = function(...args) {
  let now = +new Date();
  if (now - previous > wait) {
    previous = now
    // 执行 fn 函数
    fn.apply(this, args)
  }
}

结合上面两段代码就实现了节流函数,所以完整的实现如下。

// fn 是需要执行的函数
// wait 是时间间隔
const throttle = (fn, wait = 50) => {
  // 上一次执行 fn 的时间
  let previous = 0
  // 将 throttle 处理结果当作函数返回
  return function(...args) {
    // 获取当前时间,转换成时间戳,单位毫秒
    let now = +new Date()
    // 将当前时间和上一次执行函数的时间进行对比
    // 大于等待时间就把 previous 设置为当前时间并执行函数 fn
    if (now - previous > wait) {
      previous = now
      fn.apply(this, args)
    }
  }
}

// DEMO
// 执行 throttle 函数返回新函数
const betterFn = throttle(() => console.log('fn 函数执行了'), 1000)
// 每 10 毫秒执行一次 betterFn 函数,但是只有时间差大于 1000 时才会执行 fn
setInterval(betterFn, 10)

underscore 源码解读

上述代码实现了一个简单的节流函数,不过 underscore 实现了更高级的功能,即新增了两个功能

配置 { leading: false } 时,事件刚开始的那次回调不执行;配置 { trailing: false } 时,事件结束后的那次回调不执行,不过需要注意的是,这两者不能同时配置。

所以在 underscore 中的节流函数有 3 种调用方式,默认的(有头有尾),设置 { leading: false } 的,以及设置 { trailing: false } 的。上面说过实现 throttle 的方案有 2 种,一种是通过时间戳判断,另一种是通过定时器创建和销毁来控制。

第一种方案实现这 3 种调用方式存在一个问题,即事件停止触发时无法响应回调,所以 { trailing: true } 时无法生效。

第二种方案来实现也存在一个问题,因为定时器是延迟执行的,所以事件停止触发时必然会响应回调,所以 { trailing: false } 时无法生效。

underscore 采用的方案是两种方案搭配使用来实现这个功能。

const throttle = function(func, wait, options) {
  var timeout, context, args, result;

  // 上一次执行回调的时间戳
  var previous = 0;

  // 无传入参数时,初始化 options 为空对象
  if (!options) options = {};

  var later = function() {
    // 当设置 { leading: false } 时
    // 每次触发回调函数后设置 previous 为 0
    // 不然为当前时间
    previous = options.leading === false ? 0 : _.now();

    // 防止内存泄漏,置为 null 便于后面根据 !timeout 设置新的 timeout
    timeout = null;

    // 执行函数
    result = func.apply(context, args);
    if (!timeout) context = args = null;
  };

  // 每次触发事件回调都执行这个函数
  // 函数内判断是否执行 func
  // func 才是我们业务层代码想要执行的函数
  var throttled = function() {

    // 记录当前时间
    var now = _.now();

    // 第一次执行时(此时 previous 为 0,之后为上一次时间戳)
    // 并且设置了 { leading: false }(表示第一次回调不执行)
    // 此时设置 previous 为当前值,表示刚执行过,本次就不执行了
    if (!previous && options.leading === false) previous = now;

    // 距离下次触发 func 还需要等待的时间
    var remaining = wait - (now - previous);
    context = this;
    args = arguments;

    // 要么是到了间隔时间了,随即触发方法(remaining <= 0)
    // 要么是没有传入 {leading: false},且第一次触发回调,即立即触发
    // 此时 previous 为 0,wait - (now - previous) 也满足 <= 0
    // 之后便会把 previous 值迅速置为 now
    if (remaining <= 0 || remaining > wait) {
      if (timeout) {
        clearTimeout(timeout);

        // clearTimeout(timeout) 并不会把 timeout 设为 null
        // 手动设置,便于后续判断
        timeout = null;
      }

      // 设置 previous 为当前时间
      previous = now;

      // 执行 func 函数
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    } else if (!timeout && options.trailing !== false) {
      // 最后一次需要触发的情况
      // 如果已经存在一个定时器,则不会进入该 if 分支
      // 如果 {trailing: false},即最后一次不需要触发了,也不会进入这个分支
      // 间隔 remaining milliseconds 后触发 later 方法
      timeout = setTimeout(later, remaining);
    }
    return result;
  };

  // 手动取消
  throttled.cancel = function() {
    clearTimeout(timeout);
    previous = 0;
    timeout = context = args = null;
  };

  // 执行 _.throttle 返回 throttled 函数
  return throttled;
};

参考

【进阶 7-1 期】深入浅出节流函数 throttle

浅谈节流与防抖

lyihuan commented 4 years ago
function throttle( fn, delay ) {
  let prevDate = 0;
  return function () {
      let context = this;
      let nowDate = new Date();
      if(nowDate - prevDate > delay){
         fn.apply(context, args, argments);
         prevDate = nowDate;
  }
}
Leecason commented 4 years ago
function throttle (func, wait) {
  let prev = 0;

  return function (...args) {
    const now = Date.now();
    const context = this;
    if (now - prev > wait) {
      func.apply(context, args);
      prev = now
    }
  }
}
JerryWen1994 commented 4 years ago
leading:

|    |    |    |    |    |
a b c  d    e       f
--------------------------->
a    c    d    e    f

a 属于第一轮首次直接执行, f 距离上次执行已超过一个单位时间, 属于新一轮首次直接执行

no leading:

|    |    |    |    |    |
a b c   d   e       f
--------------------------->
     c       e           f

a 属于第一轮首次没有直接执行, 而是延迟一个单位时间执行, 一个单位时间期间又有 b c 抢占, 所以一个单位时间后执行 c, 同理在 d 延迟一个单位时间后被 e 抢占, 所以执行 e , f 在延迟一个单位时间后未被抢占就直接执行
trailing:

|    |    |    |    |    |
a b c  d    e       fg
--------------------------->
a    c d    e       f    g

no trailing:

|    |    |    |    |    |
a b c  d    e       fg
--------------------------->
a      d    e       f

在一个单位时间内, 首次会被执行, 但尾部执行会被抛弃掉, 如 c g
/**
 * leading: 是否执行首次
 * trailing: 是否执行尾次
 */
function throttle(fn, threshhold = 250, leading = true, trailing = true) {
  let lastTime = 0;
  let timer;
  const cancel = () => {
    lastTime = 0;
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }
  };
  const throttled = (...args) => {
    lastTime = !leading ? lastTime || Date.now() : lastTime;
    const rest = threshhold - (Date.now() - lastTime);
    if (rest <= 0) {
      cancel();
      lastTime = Date.now();
      fn(...args);
    } else if (timer == null && trailing) {
      timer = setTimeout(() => {
        lastTime = leading ? Date.now() : 0;
        timer = null;
        fn(...args);
      }, rest);
    }
  };

  throttled.flush = (...args) => {
    if (timer) {
      cancel();
      fn(...args);
    }
  };

  throttled.cancel = cancel;
  return throttled;
}
xllpiupiu commented 3 years ago
/**
 * 2. 防抖函数
 */
function debounce(callback, time) {
    let timer=null;
    return function(){
        if(timer) {
            clearTimeout(timer);
        }
        timer = setTimeout(callback,time);
    }
}
/**
 * 3. 节流
 */
function throttle(name,time) {
    return new Promise((resolve,reject)=>{
        if(this[name]) {
            return false;
        }
        this[name] = true;
        setTimeout(()=>{
            this[name] = false;
            console.log('点击成功')
        },time);
        resolve(this[name]);
    })
}