阅读 lodash 源码学防抖
前言
在介绍防抖之前,我们先看下面这个例子:鼠标滑过黑布触发 onmousemove
事件。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge, chrome=1">
<title>debounce</title>
<style>
#wrapper {
width: 100%;
height: 200px;
line-height: 200px;
text-align: center;
font-size: 30px;
color: #fff;
background-color: #666;
}
</style>
</head>
<body>
<div id="wrapper"></div>
<script src="debounce.js"></script>
</body>
</html>
// debounce.js
var count = 1
var wrapperDom = document.getElementById('wrapper')
function doEvent (e) {
// 这两个是为了验证防抖前后的变化,后面会提到。
// console.log(this)
// console.log(e)
wrapperDom.innerHTML = count++
}
wrapperDom.onmousemove = doEvent
运行效果图如下:
从效果图可以看出,浏览器处理数字累加还是蛮流畅的,因为这个例子的事件函数相对简单。假设 1s 触发 1000 次事件,且事件是发送 ajax 请求,那么浏览器处理时就会出现卡顿。
为了解决这类问题,通常使用防抖(debounce)和节流(throttle)的方案。
介绍
防抖指的是触发事件后,在 n
秒内函数只能执行一次,如果触发事件后在 n
秒内又触发了事件,则会重新计算函数,延长执行时间。
实现
根据上面的介绍,我们现在可以简单实现下:
function debounce (func, wait) {
let timeout
return function () {
clearTimeout(timeout)
timeout = setTimeout(func, wait)
}
}
wrapperDom.onmousemove = debounce(doEvent, 1000)
运行效果图如下:
现在不管你在 1s 内鼠标移动多少次,它都只在移动完 1s 后再触发事件。
细心的你应该已经发现了,上面的实现过程中,this 的指向和 MouseEvent 对象参数发生了改变。那么现在我们来修复这两个已知问题,如下:
function debounce (func, wait) {
let timeout
return function (...args) {
const lastThis = this
const lastArgs = args
clearTimeout(timeout)
timeout = setTimeout(function () {
func.apply(lastThis, lastArgs)
}, wait)
}
}
立即调用
虽然上面的防抖函数已经基本完成了,但是产品经理有这么个需求:添加一个控制立即调用函数 func
的开关。
// leading 默认不开启立即调用
function debounce (func, wait, leading = false) {
let timerId
function debounced (...args) {
const lastThis = this
const lastArgs = args
timerId && clearTimeout(timerId)
if (leading) {
const invokeNow = !timerId
timerId = setTimeout(function () {
timerId = null
}, wait)
invokeNow && func.apply(lastThis, lastArgs)
} else {
timerId = setTimeout(function () {
func.apply(lastThis, lastArgs)
}, wait)
}
}
return debounced
}
wrapperDom.onmousemove = debounce(doEvent, 1000, true)
运行效果图如下:
返回值
如果调用的 func
函数有返回值怎么办?我们得添加它的返回值。代码实现如下:
function debounce (func, wait, leading = false) {
let timerId,
result
function debounced (...args) {
const lastThis = this
const lastArgs = args
timerId && clearTimeout(timerId)
if (leading) {
const invokeNow = !timerId
timerId = setTimeout(function () {
timerId = null
}, wait)
result = invokeNow && func.apply(lastThis, lastArgs)
} else {
timerId = setTimeout(function () {
func.apply(lastThis, lastArgs)
}, wait)
}
return result
}
return debounced
}
注意:当 leading 为 false 的时候,因为使用了 setTimeout ,我们将 func.apply(context, args)
的返回值赋给变量,最后再 return 的时候,值将会一直是 undefined,所以我们只在 leading 为 true 的时候返回函数的执行结果。
虽然实际开发中,这个返回值几乎用不上,但是作为工具库的 Lodash 考虑情况比较全面。
取消
如果我不想等待防抖函数执行了,是否可以取消呢?答案是可以的哈~,下面我们为防抖函数添加一个取消属性即可。
function debounce (func, wait, leading = false) {
let timerId,
result
function debounced (...args) {
const lastThis = this
const lastArgs = args
timerId && clearTimeout(timerId)
if (leading) {
const invokeNow = !timerId
timerId = setTimeout(function () {
timerId = null
}, wait)
result = invokeNow && func.apply(lastThis, lastArgs)
} else {
timerId = setTimeout(function () {
func.apply(lastThis, lastArgs)
}, wait)
}
return result
}
function cancel () {
clearTimeout(timerId)
timerId = null
}
debounced.cancel = cancel
return debounced
}
// 给页面添加一个取消防抖函数按钮
var activity = debounce(doEvent, 20000, false) // 设置 20s 后才执行函数
$('#wrapper').on('mousemove', activity)
$('#btn').on('click', function () {
activity.cancel()
})
题外话
或许有小伙伴会问,如果我想要调试 Lodash 源码怎么办?有没有方案推荐呢?
有的哈。我已经把它总结到这篇文章了。戳这里学习
结语
本文到这里就结束了,通过文章我们了解到什么是防抖以及它的实现原理,使用防抖函数可以解决项目中,搜索框输入关键字后间隔一段时间,才会请求获取建议列表......
在前端面试中,防抖函数还是一道高频考题,希望小伙伴们看完本文后能够顺利拿下。