前言
老生常谈的面经了。之前一直用的 lodash
库,今天在做 vue
练习的时候遇到了自定义指令的需求,然后发现不调库居然写不出来这玩意,因此学习并记录一下
出于不重复造轮子的原则,对于一些在参考博客里已经写的很好的内容就不再赘述了。更详尽的解释请参照 ref
部分
ref
有动画:
写的非常详尽:
blog.windstone.cc/interview/j...
视频:
www.bilibili.com/video/BV1ky...
评论区很有趣:
防抖
连续(本次触发与上一次触发时间差 < 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)
}
}
注意点
- 采用
function
以保存this
习惯使用箭头函数的话,很可能出现这样的问题 考虑到传进来的 fn
有可能带着上下文和参数 (e或者其他)
,这样的处理是更完善的
实际上我用箭头函数写,对于对象里的方法,传一个 getter
进去还是读的到 this
的,只能说 this
学的不好 这里一直没有找到好的 hack
数据,留个坑吧。
- 调用函数后,timer 还原成初始状态
- 第一次触发事件也不会立即执行
完整实现
取消
给返回的函数取个名,然后对外暴露 cancel
方法
实现原理很简单:清除计时器
javascript
_debounced.cancel = function () {
if (timer) clearTimeout(timer)
}
立即执行
只需要用 timer
是否为 null
来标识
这里其实对应着两种需求:
- 每次触发都保持
immediate
状态 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
的间隔持续调用 _debounced
,func
函数可能永远不会执行
可以参照 ref
部分 underscore.debounce
的源码。
lodash
是使用节流 + 防抖一起实现的,和 underscore.debounce
的实现方式非常相似。同时还支持配置 maxWait
参数来解决上述问题
这份手写代码和 lodash
使用起来还是有区别:指定 immediate = true
的情况下,点击 + 在延时内点击,lodash
只会执行一次,而上述代码会立即执行一次 + 等待完延时后再执行一次(这里其实是因为 lodash
有对 leading & trailing
的配置,默认为 false
)
这里也可以看出,对于项目中的使用,要么自己封装一个统一使用,要么统一使用某函数库,各写各的只会导致一些意料之外的问题
个人认为对源码理解即可,面试写出取消和立即执行已经足够,记得多了只会更容易犯错。
大佬看一乐就好
此外,对于函数返回值,这里也没有接收。一般来说是没有这个需求的,lodash
的做法也仅仅是在 leading:true
的情况下拿了同步返回的值。当然可以通过回调函数再拿到异步的返回值,不过这样就画蛇添足了
节流
连续触发事件时,保证在 wait
时间内 fn
执行且只执行一次
相比防抖,连续触发时无视后来的事件
首先,节流有两种实现方案:
- 时间戳判断是否已到执行时间
- 定时器
最简实现-时间戳
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
,两篇分析源码部分都写的非常好,受益匪浅
通过分析源码也知道了 js
的 Date()
对象拿到的是系统时间而非真实时间
总结
underscore
的源码近似于上述的完整实现,更全面一些,原理一致
lodash.debounce
是引入了 maxWait
的防抖+节流实现
lodash.throttle
是基于 lodash.debounce
的简单封装