-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Description
前言
在前端开发中会遇到一些频繁的事件触发,比如:
- window 的 resize、scroll
- mousedown、mousemove
- keyup、keydown
……
为此,我们举个示例代码来了解事件如何频繁的触发:
我们写个 index.html
文件:
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="IE=edge, chrome=1">
<title>debounce</title>
<style>
#container{
width: 100%; height: 200px; line-height: 200px; text-align: center; color: #fff; background-color: #444; font-size: 30px;
}
</style>
</head>
<body>
<div id="container"></div>
<script src="debounce.js"></script>
</body>
</html>
debounce.js
文件的代码如下:
var count = 1;
var container = document.getElementById('container');
function getUserAction() {
container.innerHTML = count++;
};
container.onmousemove = getUserAction;
我们来看看效果:
从左边滑到右边就触发了 165 次 getUserAction 函数!
因为这个例子很简单,所以浏览器完全反应的过来,可是如果是复杂的回调函数或是 ajax 请求呢?假设 1 秒触发了 60 次,每个回调就必须在 1000 / 60 = 16.67ms 内完成,否则就会有卡顿出现。
为了解决这个问题,一般有两种解决方案:
- debounce 防抖
- throttle 节流
防抖
今天重点讲讲防抖的实现。
防抖的原理就是:你尽管触发事件,但是我一定在事件触发 n 秒后才执行,如果你在一个事件触发的 n 秒内又触发了这个事件,那我就以新的事件的时间为准,n 秒后才执行,总之,就是要等你触发完事件 n 秒内不再触发事件,我才执行,真是任性呐!
第一版
根据这段表述,我们可以写第一版的代码:
// 第一版
function debounce(func, wait) {
var timeout;
return function () {
clearTimeout(timeout)
timeout = setTimeout(func, wait);
}
}
如果我们要使用它,以最一开始的例子为例:
container.onmousemove = debounce(getUserAction, 1000);
现在随你怎么移动,反正你移动完 1000ms 内不再触发,我才执行事件。看看使用效果:
顿时就从 165 次降低成了 1 次!
棒棒哒,我们接着完善它。
this
如果我们在 getUserAction
函数中 console.log(this)
,在不使用 debounce
函数的时候,this
的值为:
<div id="container"></div>
但是如果使用我们的 debounce 函数,this 就会指向 Window 对象!
所以我们需要将 this 指向正确的对象。
我们修改下代码:
// 第二版
function debounce(func, wait) {
var timeout;
return function () {
var context = this;
clearTimeout(timeout)
timeout = setTimeout(function(){
func.apply(context)
}, wait);
}
}
现在 this 已经可以正确指向了。让我们看下个问题:
event 对象
JavaScript 在事件处理函数中会提供事件对象 event,我们修改下 getUserAction 函数:
function getUserAction(e) {
console.log(e);
container.innerHTML = count++;
};
如果我们不使用 debouce 函数,这里会打印 MouseEvent 对象,如图所示:
但是在我们实现的 debounce 函数中,却只会打印 undefined!
所以我们再修改一下代码:
// 第三版
function debounce(func, wait) {
var timeout;
return function () {
var context = this;
var args = arguments;
clearTimeout(timeout)
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
}
到此为止,我们修复了两个小问题:
- this 指向
- event 对象
立刻执行
这个时候,代码已经很是完善了,但是为了让这个函数更加完善,我们接下来思考一个新的需求。
这个需求就是:
我不希望非要等到事件停止触发后才执行,我希望立刻执行函数,然后等到停止触发 n 秒后,才可以重新触发执行。
想想这个需求也是很有道理的嘛,那我们加个 immediate 参数判断是否是立刻执行。
// 第四版
function debounce(func, wait, immediate) {
var timeout;
return function () {
var context = this;
var args = arguments;
if (timeout) clearTimeout(timeout);
if (immediate) {
// 如果已经执行过,不再执行
var callNow = !timeout;
timeout = setTimeout(function(){
timeout = null;
}, wait)
if (callNow) func.apply(context, args)
}
else {
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
}
}
再来看看使用效果:
返回值
此时注意一点,就是 getUserAction 函数可能是有返回值的,所以我们也要返回函数的执行结果,但是当 immediate 为 false 的时候,因为使用了 setTimeout ,我们将 func.apply(context, args) 的返回值赋给变量,最后再 return 的时候,值将会一直是 undefined,所以我们只在 immediate 为 true 的时候返回函数的执行结果。
// 第五版
function debounce(func, wait, immediate) {
var timeout, result;
return function () {
var context = this;
var args = arguments;
if (timeout) clearTimeout(timeout);
if (immediate) {
// 如果已经执行过,不再执行
var callNow = !timeout;
timeout = setTimeout(function(){
timeout = null;
}, wait)
if (callNow) result = func.apply(context, args)
}
else {
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
return result;
}
}
取消
最后我们再思考一个小需求,我希望能取消 debounce 函数,比如说我 debounce 的时间间隔是 10 秒钟,immediate 为 true,这样的话,我只有等 10 秒后才能重新触发事件,现在我希望有一个按钮,点击后,取消防抖,这样我再去触发,就可以又立刻执行啦,是不是很开心?
为了这个需求,我们写最后一版的代码:
// 第六版
function debounce(func, wait, immediate) {
var timeout, result;
var debounced = function () {
var context = this;
var args = arguments;
if (timeout) clearTimeout(timeout);
if (immediate) {
// 如果已经执行过,不再执行
var callNow = !timeout;
timeout = setTimeout(function(){
timeout = null;
}, wait)
if (callNow) result = func.apply(context, args)
}
else {
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
return result;
};
debounced.cancel = function() {
clearTimeout(timeout);
timeout = null;
};
return debounced;
}
那么该如何使用这个 cancel 函数呢?依然是以上面的 demo 为例:
var count = 1;
var container = document.getElementById('container');
function getUserAction(e) {
container.innerHTML = count++;
};
var setUseAction = debounce(getUserAction, 10000, true);
container.onmousemove = setUseAction;
document.getElementById("button").addEventListener('click', function(){
setUseAction.cancel();
})
演示效果如下:
至此我们已经完整实现了一个 underscore 中的 debounce 函数,恭喜,撒花!
演示代码
相关的代码可以在 Github 博客仓库 中找到
专题系列
JavaScript专题系列目录地址:https://github.com/mqyqingfeng/Blog。
JavaScript专题系列预计写二十篇左右,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是研(chao)究(xi) underscore 和 jQuery 的实现方式。
如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。
Activity
wcflmy commentedon Jun 6, 2017
建议将第四版和第五版换个顺序,在第四版中,由于func始终是异步执行的,return result返回一直是undefined,只有在第五版中immediate参数为true的情况下,result才会取到结果,所以建议换个顺序更加严谨一些。
mqyqingfeng commentedon Jun 6, 2017
@wcflmy 确实存在这个问题,非常感谢指出~ o( ̄▽ ̄)d
xxxgitone commentedon Jun 9, 2017
谢谢您的文章,最近想梳理自己的知识,但是却不知道从哪里入手好,跟着您的文章走真的是事半功倍啊,很多以前的疑点豁然开朗,再次谢谢!
mqyqingfeng commentedon Jun 9, 2017
@xxxgitone 我也在梳理自己的知识,与你共勉哈~
hujiulong commentedon Jun 20, 2017
写得很好,在做动画时也经常用到这种方式,防止在一帧时间中(大概16ms)渲染多次。
mqyqingfeng commentedon Jun 20, 2017
@hujiulong requestAnimationFrame 确实是神器呐~
allen3xiaokai commentedon Jul 13, 2017
学习了 点赞, 一路学到这里 收获颇丰
YeaseonZhang commentedon Aug 1, 2017
第五版有一点不解,为什么要
return result
,直接执行不可以么,望解答,谢谢
mqyqingfeng commentedon Aug 3, 2017
@YeaseonZhang 直接执行当然可以呀,之所以 return result ,是考虑到 func 这个函数,可能有返回值,尽管这个功能,我们在实际的开发中基本用不到……但是作为一个工具库,underscore 考虑得会更齐全一点~
xietao91 commentedon Aug 13, 2017
跟着大神涨姿势了,我看了一两个小时才算看明白😂
271 remaining items
DefeatLaziness commentedon Mar 24, 2022
好咧,谢谢大佬
Kento97 commentedon Mar 28, 2022
定时器的回调函数为什么不用箭头函数呢
Marszht commentedon Apr 17, 2022
我理解的是,当我们addEventListener 的时候注册的事件的 debounce 返回的function 而不是debounce, 返回的函数形成了闭包,所以返回的函数里面能访问该作用域里面的timeout.
而你下面的代码时每次都是执行debounce 所以returen 之前的函数每次都会打印。
xi2And33 commentedon Jul 7, 2022
我觉得没有问题
leslie555 commentedon Jul 8, 2022
这个函数名叫throttle比较好吧,raf 也是根据帧率来的,高帧率屏幕时间会更短
YuFengjie97 commentedon Oct 10, 2022
事件里的函数如果有返回值是不是没什么用啊?怎么接收?
Yang-y-good commentedon Oct 12, 2022
这样的你应该在防抖结束之后将isFirst 设置为true,不然下一次就不会立即执行了
Lemon-Cai commentedon Jan 12, 2023
关于第六版,这个写法是不是更简洁呢
nibiewabc commentedon Feb 20, 2023
我就说看着有点奇怪,这样舒服多了
ChenKun1997 commentedon Jun 14, 2023
使用context保存this是没有必要的吧,直接在apply的地方写this也是一样的
Lemon-Cai commentedon Jun 14, 2023