说白话:
抖是什么?它啊,就像大炮,投一个炸弹,装一个炸弹。那个函数啊,触发一次就执行一次。
那么,防抖又是什么?就像机关枪,突突突,不管打多少次,打完子弹仓里都要重新装子弹。高频触发函数,时间间隔会重新计算。当在最后一次触发函数时(最后一个子弹打完),时间到达执行一次。
B0003763AC281C21E791E523E80881C2.png
说人话:
事件响应函数在一段规定时间(前/后)才执行。如果在规定时间内,再次触发,重新计算时间。
<div class="box"></div>
<button id="btn">取消防抖</button>
<script>
let obox = document.querySelector('.box')
let count = 0
obox.innerHTML = count
obox.onmousemove = function () {
obox.innerHTML = count++
console.log(count);
}
</script>
当鼠标移动n次,就会触发n次。
// <script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
// 或
<script src="https://cdn.jsdelivr.net/npm/underscore@1.13.1/underscore-umd-min.js"></script>
<script>
let obox = document.querySelector('.box')
let count = 0
obox.innerHTML = count
function todo(e) {
obox.innerHTML = ++count
console.log(e);
}
obox.onmousemove = _.debounce(todo, 1000)
</script>
直接使用lodash.js或者underscore.js中的防抖函数,就可以做到1s内,鼠标疯狂移动只触发一次。
2.gif
对于我们而言,光知其然,是远远不够的;我们更要知其所以然! 二话不说,咱们就来凭空捏造一个把!
就underscore而言,先剖析这个debounced(防抖动)函数。它有三个参数:防抖动的函数fun、需要延迟的毫秒数wait、是否立即执行immediate。
先照葫芦画瓢,把形参先整好。最先在鼠标移动时,它接收的是一个函数,所以需要返回一个函数;其次,需要等待规定时间内执行,需要一个定时器。
function debounce(fn, wait = 200, immediate = false) {
let timer = null
return function () {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
fn()
}, wait)
}
}
可以使用setTimeout
定时器,将功能函数在一定时间内执行一次。这样最基础的防抖函数就🆗拉!
我们不光需要考虑功能函数,还需要考虑到在执行函数功能时,fn函数中可能使用event事件、内部this指向问题。此外第一版只完成了后执行,我们还需要完成立即执行的功能。
let obox = document.querySelector('.box')
let count = 0
obox.innerHTML = count
function todo(e) {
obox.innerHTML = ++count
console.log(this, e);
}
obox.onmousemove = _.debounce(todo, 1000,true)
// <div class="box">1</div>
// MouseEvent{isTruted: true, screenX: 87, screenY: 388, clientX: 68, clientY: 295,...}
在使用我们第一版的this指向的是window,并且e为undefined。 在自定义debounce函数中,我们发现返回的函数this指向div,这时我们就需要在fn函数执行时,改变this指向。
考虑参数传递问题,在返回函数中接收参数,在函数执行时传入参数即可。
function debounce(fn, wait = 200, immediate = false) {
let timer = null
return function (...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, args)
}, wait)
}
}
此外,我们还需要考虑是否立即实行,及第三个参数。
如果传入的参数immediate为true,那么就执行fn函数;如果为false的话,那就需要在一定时间之后执行(使用setTimeout)。
使用immediate来判断是否立即执行:当立即执行时,此时必须没有定时器,执行函数。等待2s,将定时器清空,等待执行下一次。
function debounce(fn, wait = 200, immediate = false) {
let timer = null, result
return function (...args) {
if (timer) clearTimeout(timer)
if (immediate) {// 立即执行
(!timer) && fn.apply(this, args) // 一开始就执行,无定时
timer = setTimeout(() => {
timer = null
}, wait)
} else {// 后执行
timer = setTimeout(() => {
fn.apply(this, args)
}, wait)
}
}
}
此外还可以通过变量存储,记录执行顺序。
function debounce(fn, wait = 200, immediate = false) {
let timer = null
let isEnd = true // 默认后执行
return function (...args) {
if (timer) clearTimeout(timer)
if (immediate) { // 先执行
isEnd && fn.apply(this, args)
isEnd = false
}
timer = setTimeout(() => {
(!immediate) && fn.apply(this, args) // 后执行
isEnd = true
}, wait)
}
}
在第二版的基础上我们可以添加函数返回值和取消抖动的方法。 添加函数返回值,可以记录执行函数的值,不管是立即执行还是后执行,最后统一返回这个值。
function debounce(fn, wait = 200, immediate = false) {
let timer = null, isEnd = true, result
let debounced = function (...args) {
if (timer) clearTimeout(timer)
if (immediate) {
isEnd && (result = fn.apply(this, args))
isEnd = false
}
timer = setTimeout(() => {
(!immediate) && (result = fn.apply(this, args))
isEnd = true
}, wait)
return result
}
return debounced
}
使用result记录返回值,最后返回即可。上述代码做了一点点小改动,将整个返回函数使用变量记录,将该变量返回。这样方便于接下来,给函数添加取消抖动的方法。
function debounce(fn, wait = 200, immediate = false) {
let timer = null, isEnd = true, result
let debounced = function (...args) {
if (timer) clearTimeout(timer)
if (immediate) {
isEnd && (result = fn.apply(this, args))
isEnd = false
}
timer = setTimeout(() => {
(!immediate) && (result = fn.apply(this, args))
isEnd = true
}, wait)
return result
}
debounced.cancel = function () {
if (timer) clearTimeout(timer)
timer = null
}
return debounced
}
在cancel方法中,直接清除抖动的定时器,并将该变量回收。
很感谢读者提的建议,我使用underscore后发现,确实接收的返回值存在异步问题。
let obox = document.querySelector('.box')
let obtn = document.querySelector('#btn')
let count = 0
function todo(e) {
obox.innerHTML = ++count
console.log(this, e);
return count
}
let debounceFn = _.debounce(todo, 1000, false)
obox.onmousemove = (e) => {
let value = debounceFn(e)
console.log(value);
}
当我第一次进入div时,执行一次todo函数,此时返回值count应该为1,但是实际输出为undefined。第二次进入的时候,输出为1,但是页面的count为2。返回值返回的是上一个返回值。
为解决异步问题,我们可以使用promise来解决。
function debounce(fn, wait, immediate) {
let timer = null, result
let debounced = function (...args) {
return new Promise(res => {
if (timer) clearInterval(timer)
if (immediate) {// 立即执行
if (!timer) {
result = fn.apply(this, args)
res(result)
}
timer = setTimeout(() => {
timer = null
}, wait);
} else {
timer = setTimeout(() => {
result = fn.apply(this, args)
res(result)
}, wait);
}
})
}
debounced.cancel = function () {
if (timer) clearTimeout(timer)
timer = null
}
return debounced
}
let obox = document.querySelector('.box')
let obtn = document.querySelector('#btn')
let count = 0
function todo(e) {
obox.innerHTML = ++count
console.log(this, e);
return count
}
let debounceFn = debounce(todo, 1000, false)
obox.onmousemove = async (e) => {
try {
let value = await debounceFn(e)
console.log(value);
} catch (e) {
console.log(e);
}
}
使用promise解决返回值异步问题,在调用时,使用async/await,将其同步。进入div,调用一次,输出值为1,调用两次,输出值为2,返回值同步。
image.png
防抖最常见的应用莫过于解决频繁访问接口的问题了。 总结一下常见的应用: