js基础中的 防抖与 节流

昨天被问到这个问题,发现自己还不是很清楚。找了很多博客,学习一下。

防抖和节流是属于浏览器性能优化部分,事件的频繁触发,频繁操作dom,会导致页面卡顿和浏览器崩溃。但是,dom绑定的某些事件我们是控制不了它的触发频率

  1. window 的 resize()、scroll()
  2. mousemove
  3. mousedown、keydown
  4. 文字输入、自动完成的keyup事件,用户输入过程中事件不断的触发,会导致大量请求发出,但是响应跟不上。

对于window.resize()的处理,停止改变n毫秒后执行后续的处理,防抖 debounce

其他事件的需求,以一定频率执行后续的处理,期间不执行,节流 throttle

防抖 debounce

定义 : 策略是当事件被触发时,设定一个周期延迟执行动作,若期间又被触发,则重新设定周期,直到周期结束,执行动作。

简单实现的例子,滚动条监听

function debounce(fn, delay) {
            let timer = null; // 借助闭包
            console.log("借助闭包来实现")
            return function () {
                if (timer) {
                    // 正在计时的过程中,又触发了相同的事件,所以要取消当前计时,重新开始
                    clearTimeout(timer);
                }
                // 没有计时的存在,那就重新开始一个计时
                timer = setTimeout(fn, delay);
            }
        }

        function showTop() {
            let scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
            console.log("获取滚动位置", scrollTop);
        }
        // 实现 停止滚动1s后才能再次打印出位置
        window.onscroll = debounce(showTop, 1000);

lodash 中的 debounce 函数:  _.debounce(func, [wait=0], [options={}])

 再贴一个lodash中的源码,有时间看看

function debounce(func, wait, options) {
  var nativeMax = Math.max,
    toNumber,
    nativeMin

  var lastArgs,
    lastThis,
    maxWait,
    result,
    timerId,
    lastCallTime,
    // func 上一次执行的时间
    lastInvokeTime = 0,
    leading = false,
    maxing = false,
    trailing = true;

  // func必须是函数
  if (typeof func != 'function') {
    throw new TypeError(FUNC_ERROR_TEXT);
  }

  // 对间隔时间的处理
  wait = toNumber(wait) || 0;

  // 对options中传入参数的处理
  if (isObject(options)) {
    leading = !!options.leading;
    maxing = 'maxWait' in options;
    maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait;
    trailing = 'trailing' in options ? !!options.trailing : trailing;
  }

  // 执行要被触发的函数
  function invokeFunc(time) {
    var args = lastArgs,
      thisArg = lastThis;
    lastArgs = lastThis = undefined;
    lastInvokeTime = time;
    result = func.apply(thisArg, args);
    return result;
  }

  // 在leading edge阶段执行函数
  function leadingEdge(time) {
    // Reset any `maxWait` timer.
    lastInvokeTime = time;
    // 为 trailing edge 触发函数调用设定定时器
    timerId = setTimeout(timerExpired, wait);
    // leading = true 执行函数
    return leading ? invokeFunc(time) : result;
  }

  // 剩余时间
  function remainingWait(time) {
    // 距离上次debounced函数被调用的时间
    var timeSinceLastCall = time - lastCallTime,
      // 距离上次函数被执行的时间
      timeSinceLastInvoke = time - lastInvokeTime,
      // 用 wait 减去 timeSinceLastCall 计算出下一次trailing的位置
      result = wait - timeSinceLastCall;
    // 两种情况
    // 有maxing: 比较出下一次maxing和下一次trailing的最小值,作为下一次函数要执行的时间
    // 无maxing: 在下一次trailing时执行timerExpired
    return maxing ? nativeMin(result, maxWait - timeSinceLastInvoke) : result;
  }

  // 根据时间判断 func 能否被执行
  function shouldInvoke(time) {
    var timeSinceLastCall = time - lastCallTime,
      timeSinceLastInvoke = time - lastInvokeTime;
    // 几种满足条件的情况
    return (lastCallTime === undefined // 首次执行
      || (timeSinceLastCall >= wait) // 距离上次被调用已经超过 wait
      || (timeSinceLastCall < 0)// 系统时间倒退
      || (maxing && timeSinceLastInvoke >= maxWait)); //超过最大等待时间
  }

  // 在 trailing edge 且时间符合条件时,调用 trailingEdge函数,否则重启定时器
  function timerExpired() {
    var time = now();
    if (shouldInvoke(time)) {
      return trailingEdge(time);
    }
    // 重启定时器
    timerId = setTimeout(timerExpired, remainingWait(time));
  }

  // 在trailing edge阶段执行函数
  function trailingEdge(time) {
    timerId = undefined;
    // 有lastArgs才执行,
    // 意味着只有 func 已经被 debounced 过一次以后才会在 trailing edge 执行
    if (trailing && lastArgs) {
      return invokeFunc(time);
    }
    // 每次 trailingEdge 都会清除 lastArgs 和 lastThis,目的是避免最后一次函数被执行了两次
    // 举个例子:最后一次函数执行的时候,可能恰巧是前一次的 trailing edge,函数被调用,而这个函数又需要在自己时延的 trailing edge 触发,导致触发多次
    lastArgs = lastThis = undefined;
    return result;
  }

  // cancel方法
  function cancel() {
    if (timerId !== undefined) {
      clearTimeout(timerId);
    }
    lastInvokeTime = 0;
    lastArgs = lastCallTime = lastThis = timerId = undefined;
  }

  // flush方法--立即调用
  function flush() {
    return timerId === undefined ? result : trailingEdge(now());
  }

  function debounced() {
    var time = now(),
      //是否满足时间条件
      isInvoking = shouldInvoke(time);
    lastArgs = arguments;
    lastThis = this;
    lastCallTime = time; //函数被调用的时间
    // 无timerId的情况有两种:
    // 1.首次调用
    // 2.trailingEdge执行过函数
    if (isInvoking) {
      if (timerId === undefined) {
        return leadingEdge(lastCallTime);
      }
      if (maxing) {
        // Handle invocations in a tight loop.
        timerId = setTimeout(timerExpired, wait);
        return invokeFunc(lastCallTime);
      }
    }
    // 负责一种case:trailing 为 true 的情况下,在前一个 wait 的 trailingEdge 已经执行了函数;
    // 而这次函数被调用时 shouldInvoke 不满足条件,因此要设置定时器,在本次的 trailingEdge 保证函数被执行
    if (timerId === undefined) {
      timerId = setTimeout(timerExpired, wait);
    }
    return result;
  }

  debounced.cancel = cancel;
  debounced.flush = flush;
  return debounced;
}

节流 throttle

定义 :  固定周期内,只允许执行一次,期间如再次触发,不执行。周期结束后,又有事件触发,开始新的周期。

例子 : 还是监听滚动条,如果用户一直拖着滚动条移动,使用debounce防抖不停止就不会打印信息。但是,用户想在这种情况下,也能够每个一段时间触发一下。

        function throttle(fn, delay) {
            let valid = true; // 这是一个状态位
            return function () {
                if (!valid) {
                    // 正在一个执行周期内,不允许再次执行
                    return false;
                }
                // 可以执行,并开始新的周期
                valid = false;
                setTimeout(() => {
                    fn(),
                        valid = true; // 新周期开始标志
                }, delay)
            }
        }

        function showTop() {
            let scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
            console.log("获取滚动位置", scrollTop);
        }
        // 实现 停止滚动1s后才能再次打印出位置
        window.onscroll = throttle(showTop, 1000);

lodash 中提供的 throttle方法:  _.throttle(func, [wait=0], [options={}])

扫描二维码关注公众号,回复: 12403763 查看本文章

应用场景

  1. 搜索框的input事件,需要支持输入的实时搜索,可以使用节流,设置每隔一段时间后,必须查询相关内容
  2. resize() 事件,常见于页面适配时,一般使用防抖,因为只需要处理最后一次的变化。

注意

  • debounce 返回的函数必须是立即执行函数
function test() {
  console.log(123)
}
setInterval(function () {
  _.debounce(test, 1500)
}, 500)


// 每次setInterval执行后,返回函数没有执行
// 点击事件 这样写也不会生效
btn.addEventListener('click', function () {
  _.debounce(test, 1500)
})
// 正确做法
btn.addEventListener('click', test)
setInterval(_.debounce(test, 1500), 500)

参考文章:

https://segmentfault.com/a/1190000018428170

https://blog.csdn.net/hupian1989/article/details/80920324

https://blog.csdn.net/duola8789/article/details/78871789

猜你喜欢

转载自blog.csdn.net/vampire10086/article/details/109525628