- 本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
- 这是源码共读的第25期,链接:juejin.cn/post/708744…
防抖
防抖原理
一句话概括一下就是:将多个连续事件组合成一个执行,如果在小于delay时间内连续事件不停止,则不触发事件执行
应用
- window的resize、scroll
- input时停止输入
- 鼠标事件mousemove等
示例
鼠标移动事件增加防抖演示 delay时间为1s
代码如下:
const debounce = function(fn, wait) {
let timer = null
return function() {
const context = this
const args = arguments
if(timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
fn.apply(context, args)
}, wait)
}
}
const countEl = document.getElementById('count')
let num = 1
const count = function() {
countEl.innerHTML = num++
}
document.addEventListener('mousemove', debounce(count, 1000), false)
实现效果:
当鼠标一直移动不停时,函数不会被调用
返回值
有时候需要获取一个包装函数的返回值,那么,我们需要添加一个返回值result,代码如下:
const debounce = function(fn, wait) {
let result
let timer
return function() {
const context = this
const args = arguments
if(timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
result = fn.apply(context, args)
}, wait)
return result
}
}
细心的同学可能会发现。这里的返回值会是undefined,因为返回操作是同步操作,而赋值是异步操作。那么underscore是怎么解决的呢?事实上,使用underscore的debounce也是同样的结果,这个异步结果也是正常的。
立即执行
支持函数立即执行,不需要等到wait之后执行。
const debounce = function(fn, wait, immediate) {
let result
let timer
return function() {
const context = this
const args = arguments
if(!timer && immediate) {
result = fn.apply(context, args)
}
if(timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
result = fn.apply(context, args)
}, wait)
return result
}
}
如果直接加立即执行,则首次会在wait时间内执行两次,即wait前一次,wait后的定时器一次,效果见下图:
underscore的表现是立即执行时,在wait时间内仅执行一次,现在我们改造一下,如果是立即执行,并且是第一次,那么清除定时器(事实上不是首次立即执行)。
const debounce = function(fn, wait, immediate) {
let result
let timer
return function() {
const context = this
const args = arguments
if(!timer && immediate) {
result = fn.apply(context, args)
}
if(timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
// 需要立即执行时,清除定时器并改变immediate的值,保证首次立即执行
if(immediate) {
timer = null
immediate = false
} else {
result = fn.apply(context, args)
}
}, wait)
return result
}
}
我以为的immediate是这样的,实际执行过程中,发现immediate改变的是每个wait时间内的执行时机,由wait后 ——> 到wait前执行。那么,我们还需要再改造一下: 我们要实现的效果如图:
- 延时时间内,鼠标移动则立即执行函数,计数立即变化
- 超过延时时间后,再次触发鼠标移动会再次立即执行函数
- 延时时间内再次触发鼠标移动事件,函数不会执行
const debounce = function(fn, wait, immediate) {
let result
let timer
let debounceFn = function() {
let context = this
let args = arguments
timer && clearTimeout(timer)
if(immediate) {
// 定义函数是否立即执行
// 无定时器时 立即执行
let isCall = !timer
// 延时时间内不执行 赋值timer
timer = setTimeout(function() {
timer = null // 延时时间过后清除定时器 可以立即执行
}, wait)
if(isCall) {
result = fn.apply(context, args)
}
} else {
timer = setTimeout(function() {
result = fn.apply(context, args)
}, wait)
}
return result
}
return debounceFn
}
观察underscore的代码,可以知道:
可以根据用户的停顿时间,也就是函数执行时间和触发事件的时间差(也就是下面代码中的passed)来控制函数的执行时机。
代码实现如下:
const debounce = function(fn, wait, immediate) {
let result
let timer
let context
let args
let previous // 存储每次事件触发的时机(此例为鼠标移动)
// 延时执行的函数 全局存储定时器
const later = function() {
// 计算定时器执行的时间与事件触发的时机的时间差
const passed = Date.now() - previous
// 时间差小于延时时 重新设置定时器 延后执行
if(passed < wait) {
timer = setTimeout(later, wait-passed)
} else {
// 时间差超过延时时间 清除定时器
// 非立即执行时执行函数
timer = null
if(!immediate)result = fn.apply(context, args)
}
}
return function() {
context = this
args = arguments
previous = Date.now()
if(!timer) {
timer = setTimeout(later, wait)
if(immediate) result = fn.apply(context, args)
}
return result
}
}
cancel定时器
增加cancel方法:
const debounce = function(fn, wait, immediate) {
...
debounceFn.cancel = function() {
clearTimeout(timer)
timer = null
}
return debounceFn
}
调用cancel之后,延时执行的函数被清除,不会执行。仅对当次有效。
如果immediate为true,那么执行cancel方法后,则可以直接触发立即执行,不需要等待wait时间后触发立即执行。
效果如图:
延时时间为5s,点击清除定时器按钮,移动鼠标则可以立即执行函数
至此,underscore的debounce函数就完整实现了!😄
总结
防抖是我们开放中常用到的功能,为了更好地理解其原理和实现,最好自己实践一遍。任何学习都是这样,纸上得来终觉浅,绝知此事要躬行!
参考文章: juejin.cn/post/684490…