说白话:
抖是什么?它啊,就像大炮,投一个炸弹,装一个炸弹。那个函数啊,触发一次就执行一次。
那么,防抖又是什么?就像机关枪,突突突,不管打多少次,打完子弹仓里都要重新装子弹。高频触发函数,时间间隔会重新计算。当在最后一次触发函数时(最后一个子弹打完),时间到达执行一次。
说人话:
事件响应函数在一段规定时间(前/后)才执行。如果在规定时间内,再次触发,重新计算时间。
初模样:
<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>
整改模样:
// <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内,鼠标疯狂移动只触发一次。
造个模样
对于我们而言,光知其然,是远远不够的;我们更要知其所以然!
二话不说,咱们就来凭空捏造一个把!
就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。返回值返回的是上一个返回值。
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,返回值同步。
有什么用
防抖最常见的应用莫过于解决频繁访问接口的问题了。
总结一下常见的应用:
- 防止表单多次提交
- 搜索框输入查询(监听输入框输入内容,设定每隔一段时间访问接口)
- scroll滚动触发
- 浏览器窗口缩放时,resize事件
回顾
防抖函数中牵扯到apply改变this绑定、闭包等知识点。我们可以对一下文章做个回顾: