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

本文介绍了在Vue开发中如何使用自定义指令实现防抖和节流功能,以及对lodash库相关实现的比较,重点讲述了防抖和节流的原理、代码示例和注意事项,同时提到了源码分析的重要性。
摘要由CSDN通过智能技术生成

前言

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

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

ref

有动画:

https://css-tricks.com/debouncing-throttling-explained-examples/

写的非常详尽:

https://blog.windstone.cc/interview/javascript/performance-optimization/debounce-throttle/

https://muyiy.cn/blog/7/7.1.html

视频:

https://www.bilibili.com/video/BV1ky4y1Q7f9?p=3&vd_source=133a4c6b8765759be3947374e6336df7

评论区很有趣:

https://github.com/lishengzxc/bblog/issues/7

防抖

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

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

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

最简实现

// @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 方法

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

_debounced.cancel = function () {
  if (timer) clearTimeout(timer)
}
立即执行

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

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

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

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

最后

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

// @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. 定时器

最简实现-时间戳

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

if (immediate && startTime) {
  startTime = now
}

leading & trailing

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 的简单封装

  • 17
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sayoriqwq

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值