Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

浅谈节流与防抖 #36

Open
sisterAn opened this issue Apr 11, 2019 · 1 comment
Open

浅谈节流与防抖 #36

sisterAn opened this issue Apr 11, 2019 · 1 comment

Comments

@sisterAn
Copy link
Owner

节流和防抖在开发项目过程中很常见,例如 input 输入实时搜索、scrollview 滚动更新了,等等,大量的场景需要我们对其进行处理。我们由 Lodash 来介绍,直接进入主题吧。

Lodash

API

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

    lodash.debounce(func, [wait=0], [options={}])
  • 节流(throttle):限制目标函数调用的频率,比如:1s内不能调用2次。

    lodash.throttle(func, [wait=0], [options={}])

lodash 在 opitons 参数中定义了一些选项,主要是以下三个:

  • leading:函数在每个等待时延的开始被调用,默认值为false
  • trailing:函数在每个等待时延的结束被调用,默认值是true
  • maxwait:最大的等待时间,因为如果 debounce 的函数调用时间不满足条件,可能永远都无法触发,因此增加了这个配置,保证大于一段时间后一定能执行一次函数

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

  • {leading: true, trailing: false}:只在延时开始时调用

  • {leading: false, trailing: true}:默认情况,即在延时结束后才会调用函数

  • {leading: true, trailing: true}:在延时开始时就调用,延时结束后也会调用

deboucne 还有 cancel 方法,用于取消防抖动调用

使用

  • 防抖 (debounce):

    addEntity = () => {
      console.log('--------------addEntity---------------')
      this.debounceFun();
    }
     
    debounceFun = lodash.debounce(function(e){
      console.log('--------------debounceFun---------------');
    }, 500,{
      leading: true,
      trailing: false,
    })

    首次点击时执行,连续点击且时间间隔在500ms之内,不再执行,间隔在500ms之外再次点击,执行。

  • 节流(throttle):

    addEntity = () => {
      console.log('--------------addEntity---------------');
      this.throttleFun();
    }
     
    throttleFun = lodash.throttle(function(e){
      console.log('--------------throttleFun---------------');
    }, 500,{
      leading: true,
      trailing: false,
    })

    首次点击时执行,连续点击且间隔在500ms之内,500ms之后自动执行一次(注:连续点击次数时间之后小于500ms,则不会自动执行),间隔在500ms之外再次点击,执行。

源码实现

debounce

// 这个是用来获取当前时间戳的
function now() {
  return +new Date()
}
/**
 * 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行
 *
 * @param  {function} func        回调函数
 * @param  {number}   wait        表示时间窗口的间隔
 * @param  {boolean}  immediate   设置为ture时,是否立即调用函数
 * @return {function}             返回客户调用函数
 */
function debounce (func, wait = 50, immediate = true) {
  let timer, context, args

  // 延迟执行函数
  const later = () => setTimeout(() => {
    // 延迟函数执行完毕,清空缓存的定时器序号
    timer = null
    // 延迟执行的情况下,函数会在延迟函数中执行
    // 使用到之前缓存的参数和上下文
    if (!immediate) {
      func.apply(context, args)
      context = args = null
    }
  }, wait)

  // 这里返回的函数是每次实际调用的函数
  return function(...params) {
    // 如果没有创建延迟执行函数(later),就创建一个
    if (!timer) {
      timer = later()
      // 如果是立即执行,调用函数
      // 否则缓存参数和调用上下文
      if (immediate) {
        func.apply(this, params)
      } else {
        context = this
        args = params
      }
    // 如果已有延迟执行函数(later),调用的时候清除原来的并重新设定一个
    // 这样做延迟函数会重新计时
    } else {
      clearTimeout(timer)
      timer = later()
    }
  }
}

throttle

/**
 * underscore 节流函数,返回函数连续调用时,func 执行频率限定为 次 / wait
 *
 * @param  {function}   func      回调函数
 * @param  {number}     wait      表示时间窗口的间隔
 * @param  {object}     options   如果想忽略开始函数的的调用,传入{leading: false}。
 *                                如果想忽略结尾函数的调用,传入{trailing: false}
 *                                两者不能共存,否则函数不能执行
 * @return {function}             返回客户调用函数
 */
_.throttle = function(func, wait, options) {
    var context, args, result;
    var timeout = null;
    // 之前的时间戳
    var previous = 0;
    // 如果 options 没传则设为空对象
    if (!options) options = {};
    // 定时器回调函数
    var later = function() {
      // 如果设置了 leading,就将 previous 设为 0
      // 用于下面函数的第一个 if 判断
      previous = options.leading === false ? 0 : _.now();
      // 置空一是为了防止内存泄漏,二是为了下面的定时器判断
      timeout = null;
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    };
    return function() {
      // 获得当前时间戳
      var now = _.now();
      // 首次进入前者肯定为 true
	  // 如果需要第一次不执行函数
	  // 就将上次时间戳设为当前的
      // 这样在接下来计算 remaining 的值时会大于0
      if (!previous && options.leading === false) previous = now;
      // 计算剩余时间
      var remaining = wait - (now - previous);
      context = this;
      args = arguments;
      // 如果当前调用已经大于上次调用时间 + wait
      // 或者用户手动调了时间
 	  // 如果设置了 trailing,只会进入这个条件
	  // 如果没有设置 leading,那么第一次会进入这个条件
	  // 还有一点,你可能会觉得开启了定时器那么应该不会进入这个 if 条件了
	  // 其实还是会进入的,因为定时器的延时
	  // 并不是准确的时间,很可能你设置了2秒
	  // 但是他需要2.2秒才触发,这时候就会进入这个条件
      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) {
        // 判断是否设置了定时器和 trailing
	    // 没有的话就开启一个定时器
        // 并且不能不能同时设置 leading 和 trailing
        timeout = setTimeout(later, remaining);
      }
      return result;
    };
  };
@913363140
Copy link

913363140 commented Oct 14, 2020

little bottle,你能把这俩的es版本搞一致吗。。。。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants