每天一个知识点-防抖节流

前言

老生常谈的面经了。之前一直用的 lodash 库,今天在做 vue 练习的时候遇到了自定义指令的需求,然后发现不调库居然写不出来这玩意,因此学习并记录一下

出于不重复造轮子的原则,对于一些在参考博客里已经写的很好的内容就不再赘述了。更详尽的解释请参照 ref 部分

ref

有动画:

css-tricks.com/debouncing-...

写的非常详尽:

blog.windstone.cc/interview/j...

muyiy.cn/blog/7/7.1....

视频:

www.bilibili.com/video/BV1ky...

评论区很有趣:

github.com/lishengzxc/...

防抖

连续(本次触发与上一次触发时间差 < wait)触发事件时,保证只执行最后一次的 fn

或者理解为:停止触发事件时执行 fn

相比节流,连续触发时会重置定时器

最简实现

javascript 复制代码
// @ts-nocheck
function debounce(func, wait) {
  let timer = null
  return function (...args) {
    const context = this
    // console.log('timer', timer)
    // 打开注释以对照事件-func的触发
    if (timer) clearTimeout(timer)
    timer = setTimeout(function () {
      func.apply(context, args)
      timer = null
    }, wait)
  }
}

注意点

  1. 采用 function 以保存 this

习惯使用箭头函数的话,很可能出现这样的问题 考虑到传进来的 fn 有可能带着上下文和参数 (e或者其他),这样的处理是更完善的

实际上我用箭头函数写,对于对象里的方法,传一个 getter 进去还是读的到 this 的,只能说 this 学的不好 这里一直没有找到好的 hack 数据,留个坑吧。

  1. 调用函数后,timer 还原成初始状态
  2. 第一次触发事件也不会立即执行

完整实现

取消

给返回的函数取个名,然后对外暴露 cancel 方法

实现原理很简单:清除计时器

javascript 复制代码
_debounced.cancel = function () {
  if (timer) clearTimeout(timer)
}

立即执行

只需要用 timer 是否为 null 来标识

这里其实对应着两种需求:

  1. 每次触发都保持 immediate 状态
  2. immediate 状态只维护一次,做法为不清除 timer 的状态(第一次 null,后面都有值)

当然也可以引入额外变量,我比较推荐这种方式,更难犯错。

最后

这样就是一个简单且相对完善的防抖实现了。

javascript 复制代码
// @ts-nocheck
function debounce(func, wait, immediate = false) {
  let timer = null
  let isInvoke = false
  function _debounced(...args) {
    // 这里保存其实没有多大必要,但是我很烦this的动态性,因此习惯先存一份
    // 事实上我几乎不用this
    const context = this
    // console.log('timer', timer)
    // 打开注释以对照事件-func的触发
    if (timer) clearTimeout(timer)

    if (immediate && !isInvoke) {
      func.apply(context, args)
      isInvoke = true
      return
    }

    timer = setTimeout(function () {
      func.apply(context, args)
      timer = null
      isInvoke = false
    }, wait)
  }
  _debounced.cancel = function () {
    if (timer) clearTimeout(timer)
    timer = null
    isInvoke = false
  }
  return _debounced
}

不过还存在着一些问题。

例如:若以小于 wait 的间隔持续调用 _debouncedfunc 函数可能永远不会执行

可以参照 ref 部分 underscore.debounce 的源码。

lodash 是使用节流 + 防抖一起实现的,和 underscore.debounce 的实现方式非常相似。同时还支持配置 maxWait 参数来解决上述问题

这份手写代码和 lodash 使用起来还是有区别:指定 immediate = true 的情况下,点击 + 在延时内点击,lodash 只会执行一次,而上述代码会立即执行一次 + 等待完延时后再执行一次(这里其实是因为 lodash 有对 leading & trailing 的配置,默认为 false

这里也可以看出,对于项目中的使用,要么自己封装一个统一使用,要么统一使用某函数库,各写各的只会导致一些意料之外的问题

个人认为对源码理解即可,面试写出取消和立即执行已经足够,记得多了只会更容易犯错。

大佬看一乐就好

此外,对于函数返回值,这里也没有接收。一般来说是没有这个需求的,lodash 的做法也仅仅是在 leading:true 的情况下拿了同步返回的值。当然可以通过回调函数再拿到异步的返回值,不过这样就画蛇添足了

节流

连续触发事件时,保证在 wait 时间内 fn 执行且只执行一次

相比防抖,连续触发时无视后来的事件

首先,节流有两种实现方案:

  1. 时间戳判断是否已到执行时间
  2. 定时器

最简实现-时间戳

javascript 复制代码
function throttle(func, wait) {
  let startTime = 0
  const _throttle = function (...args) {
    const context = this
    const now = Date.now()
    if (now - startTime >= wait) {
      func.apply(context, args)
      startTime = now
    }
  }
  return _throttle
}

没什么好说的。理解那句 if 就写出来了

此外,对于立即执行,理解了 if 应该也能写出来了,也就是把 startTime 置为 now

javascript 复制代码
if (immediate && startTime) {
  startTime = now
}

leading & trailing

javascript 复制代码
function throttle(func, wait, options) {
  let startTime = 0
  let timer = null
  const { leading, trailing } = options

  const _throttle = function (...args) {
    const context = this
    const now = Date.now()

    if (!leading && startTime === 0) {
      startTime = now
    }

    const remaining = wait - (now - startTime)

    if (remaining <= 0) {
      if (timer) clearTimeout(timer)
      func.apply(context, args)
      startTime = now
      timer = null
      return
    }

    if (trailing && !timer) {
      timer = setTimeout(() => {
        func.apply(context, args)
        // 非常重要!为了消除误差,这里要重新找到开始时间
        // 如果直接赋值为now,理论是正确的
        // 然而事实上的得到的remaining会由于误差不一定等于0,也就不会执行函数
        startTime = Date.now()
        timer = null
      }, remaining)
    }
  }
  return _throttle
}

源码

请参照 ref ,两篇分析源码部分都写的非常好,受益匪浅

通过分析源码也知道了 jsDate() 对象拿到的是系统时间而非真实时间

总结

underscore 的源码近似于上述的完整实现,更全面一些,原理一致

lodash.debounce 是引入了 maxWait 的防抖+节流实现

lodash.throttle 是基于 lodash.debounce 的简单封装

相关推荐
Fan_web1 分钟前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
Jiaberrr1 小时前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
安冬的码畜日常3 小时前
【D3.js in Action 3 精译_029】3.5 给 D3 条形图加注图表标签(上)
开发语言·前端·javascript·信息可视化·数据可视化·d3.js
太阳花ˉ3 小时前
html+css+js实现step进度条效果
javascript·css·html
john_hjy3 小时前
11. 异步编程
运维·服务器·javascript
风清扬_jd4 小时前
Chromium 中JavaScript Fetch API接口c++代码实现(二)
javascript·c++·chrome
yanlele4 小时前
前瞻 - 盘点 ES2025 已经定稿的语法规范
前端·javascript·代码规范
It'sMyGo4 小时前
Javascript数组研究09_Array.prototype[Symbol.unscopables]
开发语言·javascript·原型模式
xgq4 小时前
使用File System Access API 直接读写本地文件
前端·javascript·面试