在前端的日常开发中,可能经常碰到一些频繁触发的事件,比如:
1、window的resize、scroll
2、mousedown、mousemove、click
3、键盘事件:keydown、keyup等
当我们碰到这类事件需要优化时,该怎么办呢?
首先我们先写一个简单的示例了解下事件频繁触发的情况
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>Document</title><style>#container {width: 400px;height: 200px;line-height: 200px;margin: 100px auto;background-color: rgb(219, 68, 68);color: #FFF;font-size: 16px;text-align: center;}</style></head><body><div id="container"></div><script type="text/javascript">var count = 1;var container = document.querySelector('#container');function getUserAction() {container.innerHTML = count++;}container.addEventListener('mousemove', getUserAction);</script></body></html>
我们来看看效果:

从左边滑到右边就触发了129次getUserAction函数!
这个例子的触发频率还不算很高,所以浏览器完全可以处理,可是如果是复杂的回调函数或是ajax请求呢?假设1秒触发了60次,每个回调都必须在1000 / 60 ~=16.67秒内完成,否则就会有卡顿出现。
为了解决这个问题,一般会有两种解决方案:
1、debounce 防抖
2、throttle 节流
防抖原理
防抖的原理就是:随便用户如何触发事件、但是我一定在事件触发n秒后执行,如果你在一个事件触发的n秒内又触发了这个事件,那我就以新事件的时间为准,n秒后才执行,总之,就是要等你触发完事件n秒内不再触发事件,我才执行。
简单实现
根据上诉原理,我们来简单实现一下:
// 第一版function debounce(func, wait) {var timeout;return function() {clearTimeout(timeout);timeout = setTimeout(func, wait);}}
我们来改造下上诉栗子,将要执行事件添加进防抖函数中
container.addEventListener('mousemove', debounce(getUserAction, 200));
效果如下:

现在如论你如何移动,都要等到1s后才会执行事件,顿时就从129次减少成了一次,但是现在还存在一些问题,比如说this。我们接着看
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);}}
event对象
javascript在事件处理程序中会提供事件对象event,我们修改下getUserAction函数:
function getUserAction(e) {console.log(e);container.innerHTML = count++;}
如果我们不实用debounce函数,这里会打印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);}}
这样就解决了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的时候,因为timeout是一个异步函数,所以值会是undefined,所以我们只在immediate为true的时候返回值
所以我们再来优化一下我们的函数
// 第五版function debounce(func, wait, immediate) {var timeout, immediate, 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 (timeout) result = func.apply(context, args);} else {timeout = setTimeout(function() {func.apply(context, args);}, wait);}return result;}}
取消防抖函数延迟
最后我们在思考一个小需求,我们希望可以手动取消debounce函数的延时,比如说我们的debounce函数的wait设置为10s,immediate为true的话,我们需要等10s才能重新触发事件,现在我希望点一下按钮就取消防抖,然后重新执行。
为了实现这个需求,我们来写最后一版代码:
// 第六版代码function debounce(func, wait, immediate) {var timeout, immediate, 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.querySelector('#container');var button = document.querySelector('.cancel');function getUserAction() {container.innerHTML = count++;}var setUserAction = debounce(getUserAction, 10000, true);container.addEventListener('mousemove', setUserAction);button.addEventListener('click', function() {setUserAction.cancel()})
我们来看下演示效果吧:

到这里我们就完成了underscore的防抖函数模拟了~~~~
:::info 本文档摘抄自:冴羽大大的博客 :::
