• 2000字助你精通防抖与节流


    前言

    防抖与节流是老生常谈的话题了,不管是在面试还是实际开发中都经常涉及。

    本文将介绍防抖与节流的概念、应用场景、代码实现,其中代码实现参考了 lodash 的源码,剔除了其中参数类型与运行环境的检测,使代码更简洁易懂,方便学习理解。

    概念与应用

    防抖与节流都是为了避免代码被频繁执行

    防抖

    防抖(debounce):在下达指令后会开始计时,如果在计时范围内又重复下达指令,就重新计时,等待计时完成后才执行代码。

    打个比方:公交车进站,行人陆续上车,假设等待时间是20秒,那就只有持续20秒无人上车时,公交车才会开走(执行代码)。

    节流

    节流(throttle):在代码执行后进入冷却,冷却期间不会重复执行,冷却到了才会再次执行。

    打个比方:女神每天回复舔狗一次,今天回复过后即便舔狗发再多信息,女神也只会等到第二天才回复(再次执行代码)。

    应用场景

    在说应用场景之前,先统一防抖和节流的概念

    节流是包含最大时限的防抖

    解释一下:假设100秒内持续频繁下达指令,防抖的处理结果就是100秒后才会执行,但这样对用户极不友好的。往往会在防抖代码中加一个最大时限,当达到最大时限时,即便仍然在等待时间内下达指令,但代码也会执行一次。这就变成了节流

    防抖与节流一般应用于 搜索提示、页面滚动等

    也用来限制那些频繁触发又不确定次数的事件:mousemove、scroll、resize

    目前浏览器性能过剩,为了用户良好的体验,以上这些场景基本都采用了节流。

    代码实现

    在上面也说过了,防抖与节流的区别就在于是否具有最大时限

    所以在实现方面,先实现防抖函数,在后续加上最大时限来使其变为节流函数

    在写代码前,要明确要实现的函数的参数、返回值

    • 函数需要传入两个参数,分别为想要限制执行频率的函数与延迟时间(单位为毫秒)
    • 函数的返回值也是一个函数,是与传入函数功能相同,已经防抖动的函数

    由于在实现期间需要涉及到三个函数,为了避免混乱,在此统一一下称呼:

    • 我们即将要实现的函数,命名为 debounce,下文称作外部函数
    • 已经防抖动的函数,也就是外部函数的返回值,命名为 debounced,下文称作防抖函数
    • 需要防抖动的函数,也就是用户调用外部函数时传入的函数,命名为 func,下文称作内部函数

    而对于内部函数,由于其函数并不会立刻执行,也就不应该具有返回值

    认识 requestAnimationFrame

    基于 setTimeout 实现的防抖/节流函数网上已有很多,lodash 源码中使用的是 requestAnimationFrame,其性能与稳定性都要优于 setTimeout,所以本文也基于 requestAnimationFrame 实现

    requestAnimationFrame() 需要传入一个函数作为参数,该函数会在浏览器下一次重绘之前执行

    浏览器的重绘频率是每秒 60 次,约 16ms 重绘一次。

    可以简单的将 requestAnimationFrame 函数视为延迟为16ms 的 setTimeout 函数

    防抖函数实现

    防抖函数代码实现如下,每次调用防抖函数都会重新计时,由于是基于 requestAnimationFrame,需要递归开启计时器

    /**
     * @description:
     * @param {Function} func 要防抖的函数,内部函数
     * @param {number} wait 等待时间
     * @return {Function} 已防抖的函数
     */
    function debounce(func, wait) {let lastArgs, // 保存参数lastThis, // 保存thistimerId, // 定时器idlastCallTime // 最近调用防抖函数的时间// 重置定时器function startTimer(pendingFunc) {// 取消掉上一次开启的定时器cancelAnimationFrame(timerId)// 开启新的定时器并返回定时器idreturn requestAnimationFrame(pendingFunc)}// 检测是否到了该执行的时间function shouldInvoke(time) {const timeSinceLastCall = time - lastCallTime// 距离上一次防抖函数的调用已超过等待时间return timeSinceLastCall >= wait}// 调用内部函数function invokeFunc() {// 获取之前保存的this与参数const args = lastArgsconst thisArg = lastThis// this与参数置空,不影响垃圾回收lastArgs = lastThis = undefinedfunc.apply(thisArg, args)}// 传入定时器的回调函数// 不断获取当前时间判断是否应该调用内部函数function timerExpired() {const time = Date.now()if (shouldInvoke(time)) {// 执行内部函数timerId = undefinedinvokeFunc()} else {// 递归开启定时器timerId = startTimer(timerExpired)}}// 返回的防抖函数,该函数无返回值function debounced(...args) {const time = Date.now()// 更新this与参数lastArgs = argslastThis = this// 更新防抖函数调用的时间lastCallTime = time// 开启定时器timerId = startTimer(timerExpired)}return debounced
    }
    
    // 测试功能
    let preTime = Date.now()
    const func = () => {let nextTime = Date.now()console.log(nextTime - preTime)preTime = nextTime
    }
    
    const dfunc = debounce(func, 50)
    dfunc()
    dfunc()
    // 经过50ms后,控制台打印50
    
    setTimeout(() => {dfunc()dfunc()
    }, 100)
    // 经过150ms后,控制台打印100 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    节流函数实现

    在 lodash 中,节流函数与防抖函数公用一套代码,只是配置参数不同,本文也将复用之前的代码

    节流函数比防抖函数多两个特点

    • 节流函数包含最大时限(maxWait),这是防抖函数与节流函数的区别
    • 节流函数往往会立刻执行内部函数一次(leading)

    完整代码如下:

    /**
     * @description:
     * @param {Function} func 要防抖的函数,内部函数
     * @param {number} wait 等待时间
     * @param {number|undefined} maxWait 最大时限,有的话是节流函数,没有的话是防抖函数
     * @param {boolean} leading 规定在延迟开始前是否调用内部函数,默认不调用
     * @return {Function}
     */
    function debounce(func, wait, maxWait = undefined, leading = false) {let lastArgs, // 保存参数lastThis, // 保存thistimerId, // 定时器idlastCallTime, // 最近调用防抖函数的时间lastInvokeTime // 最近调用内部函数的时间,0初值确保let maxing = !!maxWait // 是否指定了最大等待时间// 最大时限不应该小于等待时间if (maxWait) {maxWait = Math.max(wait, maxWait)}// 重置定时器function startTimer(pendingFunc) {cancelAnimationFrame(timerId)return requestAnimationFrame(pendingFunc)}// 检测是否到了该执行的时间function shouldInvoke(time) {const timeSinceLastCall = time - lastCallTimeconst timeSinceLastInvoke = time - lastInvokeTime// 上次内部函数时间尚未定义 (首次执行节流函数)// 距离上一次防抖函数的调用已超过等待时间 (防抖函数的功能)// 设置了最大时限,且距离上次内部函数的调用已达到最大时限 (节流函数的功能)return (lastInvokeTime === undefined ||timeSinceLastCall >= wait ||(maxing && timeSinceLastInvoke >= maxWait))}// 调用内部函数function invokeFunc(time) {// 获取之前保存的this与参数const args = lastArgsconst thisArg = lastThis// this与参数置空,不影响垃圾回收lastArgs = lastThis = undefined// 更新最近内部函数的调用时间lastInvokeTime = timefunc.apply(thisArg, args)}// 不断获取当前时间判断是否应该调用内部函数function timerExpired() {const time = Date.now()if (shouldInvoke(time)) {timerId = undefined// 如果已经立刻执行内部函数// 且等待时间内没有再次调用节流函数的话// 就不需要在等待时间过后再次执行内部函数了if (lastArgs) invokeFunc(time)} else {// 重新开启定时器timerId = startTimer(timerExpired)}}// 返回的防抖函数,该函数无返回值function debounced(...args) {const time = Date.now()// 这里检测是否应该重置定时器const isInvoking = shouldInvoke(time)// 更新this与参数lastArgs = argslastThis = this// 更新防抖函数调用的时间lastCallTime = timeif (isInvoking) {if (timerId === undefined) {// 首次执行节流函数,更新内部函数调用时间lastInvokeTime = timetimerId = startTimer(timerExpired)// 检测leading属性,立即调用内部函数if (leading) invokeFunc(time)} else if (maxing) {// 节流功能,执行内部函数并重置定时器timerId = startTimer(timerExpired)invokeFunc(time)}} else if (timerId === undefined) {// 内部函数刚执行完又调用了节流函数// 只开启定时器,无需更新内部函数调用时间timerId = startTimer(timerExpired)}}return debounced
    }
    
    /**
     * @description:
     * @param {Function} func 要防抖的函数,内部函数
     * @param {number} wait 冷却时间
     * @param {boolean} leading 规定在延迟开始前是否调用内部函数,默认调用
     * @return {Function}
     */
    function throttle(func, wait, leading = true) {return debounce(func, wait, wait, leading)
    }
    
    // 测试功能
    let preTime = Date.now()
    let arr = []
    const func = () => {let nextTime = Date.now()arr.push(nextTime - preTime)preTime = nextTime
    }
    
    const tfunc = throttle(func, 100, false)
    let id = setInterval(tfunc, 10)
    setTimeout(() => {clearInterval(id)console.log(arr) // [112, 101, 104, 103, 100, 100, 100, 101, 106, 101]
    }, 1000) 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    其他功能实现

    来看一个业务场景吧 鼠标悬停在按钮上 0.5 秒后出现按钮的功能提示,有多个按钮,只显示最后鼠标悬停的按钮的功能提示,很明显要用防抖来实现

    然而如果用户在 0.5 秒内从按钮移出,理应不显示提示,但防抖函数的定时器已经设置,0.5 秒后提示依旧显示,很明显的 bug

    所以,防抖函数身上应该有个取消定时器的功能

    function debounce(func, wait, maxWait = undefined, leading = false) {……debounced.cancel = function () {// 清除定时器if (timerId !== undefined) {cancelAnimationFrame(timerId)}// 清空变量lastArgs = lastThis = timerId = lastCallTime = lastInvokeTime = undefined}return debounced
    } 
    
    • 1
    • 2

    结语

    相信经过本文的阅读,您对防抖与节流一定有较深的理解了

    本文代码实现只提取了 lodash 源码中核心的部分,且做了一定的修改

    lodash 提供的其他配置项与功能在业务中极少使用,本文便不做介绍了,有兴趣的可以自行去查看

    如果文中有不理解或不严谨的地方,欢迎评论提问。

    如果喜欢或有所帮助,希望能点赞关注,鼓励一下作者。

  • 相关阅读:
    【MySQL】MySQL锁(四)其它锁概念
    面试中常用消息中间件对比
    Postman —— HTTP请求基础组成部分
    vscode远程链接下的python环境配置
    深入理解 Happens-Before 原则
    纯Python实现遗传算法
    Win10安装Anaconda和VSCode
    二分图最佳匹配(kuhn munkras 算法 O(m*m*n))
    SpringMVC之JSON数据返回及异常处理机制
    52. N皇后 II(难度:困难)
  • 原文地址:https://blog.csdn.net/web22050702/article/details/125993241