前端边角料 | 防抖函数和节流函数的理解与应用

812 阅读4分钟

关键词:防抖函数 节流函数

这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战

背景

在日常的项目中,经常会遇到“防抖”、“节流”这样的性能优化手段,或者可以说是功能需求。这两个名词经常成对出现,以至于有很长一段时间里,从概念上我经常把他们当做相反的两种方法或实现逻辑来对待,所以就想用简单的利于自己理解的话来对这两个概念做一个简单的区分。

目录

  • 防抖函数(debounce)
  • 节流函数(throttle)
  • 防抖函数与节流函数的区别

防抖函数(debounce)

  • 应用场景示例:搜索输入框,减少实时搜索的请求
  • 代码示例:
    let inputELe = document.getElementById('inputEle')
    // 声明一个加持防抖函数的变量
    const inputHandler = debounce(()=>{
        console.log('debounce',inputELe.value)
    },1000, true)
    // 监听输入框 input 变化,触发执行函数
    inputELe.addEventListener('input', inputHandler)
    /**
     * 防抖函数
     * @params func 最终执行的函数
     * @params time 延迟执行时间,例如当 time 为 1000 时,则表示在1s内只执行1次 func 方法
     * @params immediate 初次是否立刻执行,当 immediate 为 true 时,表示第一次不受 1s 延迟执行的影响,会立即执行
    */
    function debounce(func,time,immediate) {
        let timer = null;
        let context = this;
        let _immediate = immediate
        return function() {
            let args = arguments;
            if(_immediate) {
                func.apply(context,args)
                _immediate = false
            } else {
                if (timer) {
                    clearTimeout(timer)
                }
                timer = setTimeout(function(){
                func.apply(context,args)
                },time)
            }
        }
    }

节流函数(throttle)

  • 应用场景示例:射击类游戏中子弹的发射,一定时间内只允许发射一次。
  • 代码示例:
   /**
     * 节流函数
     * @param func 最终执行的函数
     * @param time 每次执行的间隔时间,如果是 1000,则表示多次触发时,仅 1s 可以执行一次
    */
    function throttle(func,time) {
        let timer = null;
        let context = this;
        let startTime = Date.now();
        return function () {
            let current = Date.now();
            let args = arguments;
            let duration = current - startTime;
            // 如果已经存在 timer ,则清除,后续会创建新的 timer 用于保证最后一次可执行
            if(timer) clearTimeout(timer)
            // 当触发的时间间隔大于设定的间隔时,执行一次目标函数
            if(duration > time) {
                func.apply(context, args)
                startTime = Date.now();
            } else {
                // 当触发的时间间隔不满足时,设置一个定时器,保证最后一次模板函数可以顺利执行
                timer = setTimeout(function(){
                    func.apply(context,args);
                    startTime = Date.now();
                    timer = null;
                }, time - duration)
            }
        }
    }

防抖函数与节流函数的区别

  • 相同点:两者都是减少函数的执行次数,只是执行次数不同。
  • 不同点:防抖函数是将多次执行变为最后一次执行,节流函数是将多次执行变成每隔一段时间执行。
  • tips:注意两种实现的 this 和 arguments 的处理

杂谈

在网上或论坛里,“防抖函数”和“节流函数”已经有比较成熟的实现方式,如果本文没有帮助到大家理解,也希望大家自行查阅更多的资料。

在实际开发中,很多时候我们其实有用到“防抖”和“节流”这样的原理去实现或者优化一些功能,只不过我们并没有抽象的很彻底。比如在一个活动页面,为了禁止用户在接口响应之前持续发起类似“抢购”、“抽奖”等请求,往往会做一次状态控制,如果没有返回对应的状态,则不允许按钮为可点击状态。类似这种按钮是否可点击的状态控制,也可以称为一种“防抖动”的应用,只不过没有抽离出一个类似“debounce”这样的方法而已。

同样的本文举例的两种实现,也仅满足大部分需求而已,如果有特殊的要求,建议大家看一下 loadsh 这个库的 _.debounce 和 _.throttle 的实现。

同样的在我们熟悉的各类框架中,也有不少不是很明显的防抖函数的应用,如 React(v17.0.2) 框架中,源码示例:

// 源码地址:https://github.com/facebook/react/blob/v17.0.2/packages/react-reconciler/src/ReactFiberWorkLoop.old.js#L674-L736
// ... 省略部分无关代码
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
  // 前半部分: 判断是否需要注册新的调度
  const existingCallbackNode = root.callbackNode;
  const nextLanes = getNextLanes(
    root,
    root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
  );
  const newCallbackPriority = returnNextLanesPriority();
  if (nextLanes === NoLanes) {
    return;
  }
  // 节流防抖
  if (existingCallbackNode !== null) {
    const existingCallbackPriority = root.callbackPriority;
    if (existingCallbackPriority === newCallbackPriority) {
      return;
    }
    cancelCallback(existingCallbackNode);
  }
  // 后半部分: 注册调度任务 省略代码...

  // 更新标记
  root.callbackPriority = newCallbackPriority;
  root.callbackNode = newCallbackNode;
}

参考资料

浏览知识共享许可协议

知识共享许可协议
本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。