在前端的日常开发中,可能经常碰到一些频繁触发的事件,比如:
1、window的resizescroll
2、mousedownmousemoveclick
3、键盘事件:keydownkeyup
当我们碰到这类事件需要优化时,该怎么办呢?
首先我们先写一个简单的示例了解下事件频繁触发的情况

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <meta http-equiv="X-UA-Compatible" content="ie=edge">
  7. <title>Document</title>
  8. <style>
  9. #container {
  10. width: 400px;
  11. height: 200px;
  12. line-height: 200px;
  13. margin: 100px auto;
  14. background-color: rgb(219, 68, 68);
  15. color: #FFF;
  16. font-size: 16px;
  17. text-align: center;
  18. }
  19. </style>
  20. </head>
  21. <body>
  22. <div id="container"></div>
  23. <script type="text/javascript">
  24. var count = 1;
  25. var container = document.querySelector('#container');
  26. function getUserAction() {
  27. container.innerHTML = count++;
  28. }
  29. container.addEventListener('mousemove', getUserAction);
  30. </script>
  31. </body>
  32. </html>

我们来看看效果:
mfvat-24vmp.gif

从左边滑到右边就触发了129次getUserAction函数!
这个例子的触发频率还不算很高,所以浏览器完全可以处理,可是如果是复杂的回调函数或是ajax请求呢?假设1秒触发了60次,每个回调都必须在1000 / 60 ~=16.67秒内完成,否则就会有卡顿出现。
为了解决这个问题,一般会有两种解决方案:
1、debounce 防抖
2、throttle 节流

防抖原理

防抖的原理就是:随便用户如何触发事件、但是我一定在事件触发n秒后执行,如果你在一个事件触发的n秒内又触发了这个事件,那我就以新事件的时间为准,n秒后才执行,总之,就是要等你触发完事件n秒内不再触发事件,我才执行。

简单实现

根据上诉原理,我们来简单实现一下:

  1. // 第一版
  2. function debounce(func, wait) {
  3. var timeout;
  4. return function() {
  5. clearTimeout(timeout);
  6. timeout = setTimeout(func, wait);
  7. }
  8. }

我们来改造下上诉栗子,将要执行事件添加进防抖函数中

  1. container.addEventListener('mousemove', debounce(getUserAction, 200));

效果如下:

0d29r-q30zj.gif

现在如论你如何移动,都要等到1s后才会执行事件,顿时就从129次减少成了一次,但是现在还存在一些问题,比如说this。我们接着看

this

如果我们在getUserAction函数中console.log(this), 在不使用debounce函数的时候,this的值为触发事件的元素节点也就是:

  1. <div id="container"></div>

如果我们使用了debounce函数,this就会指向window对象!
所以我们需要将this指向正确的对象。

修改代码如下:

  1. // 第二版
  2. function debounce(func, wait) {
  3. var timeout;
  4. return function() {
  5. var context = this;
  6. clearTimeout(timeout);
  7. timeout = setTimeout(function() {
  8. func.apply(context);
  9. }, wait);
  10. }
  11. }

现在this已经可以正确指向了,让我们看下一个问题:

event对象

javascript在事件处理程序中会提供事件对象event,我们修改下getUserAction函数:

  1. function getUserAction(e) {
  2. console.log(e);
  3. container.innerHTML = count++;
  4. }

如果我们不实用debounce函数,这里会打印MouseEvent对象,如图所示:

image.png

但是如果我们使用debounce函数,却只会打印undefined

image.png

我们再来修改下代码:

  1. // 第三版
  2. function debounce(func, wait) {
  3. var timeout;
  4. return function() {
  5. var context = this;
  6. var args = arguments;
  7. clearTimeout(timeout);
  8. timeout = setTimeout(function() {
  9. func.apply(context, args);
  10. }, wait);
  11. }
  12. }

这样就解决了event问题。

立即执行

这时候已经很完善了,但是为了让这个函数更加完善,我们接下来思考一个新的需求。

这个需求就是:
我们不希望非要等到触发事件停止以后才执行这个函数,我希望立即执行函数,然后等到停止触发n秒后,才可以重新触发执行。

这个需求也是日常开发经常遇到的, 所以我们改造下代码,加个immediate参数判断是否是立即执行:

  1. // 第四版
  2. function debounce(func, wait, immediate) {
  3. var timeout;
  4. return function() {
  5. var context = this;
  6. var args = arguments;
  7. if (timeout) clearTimeout(timeout);
  8. if (immediate) {
  9. // 如果已经执行过,不再执行
  10. var callNow = !timeout;
  11. timeout = setTimeout(function() {
  12. timeout = null;
  13. }, wait)
  14. if (callNow) func.apply(context, args);
  15. } else {
  16. timeout = setTimeout(function() {
  17. func.apply(context, args);
  18. }, wait);
  19. }
  20. }
  21. }

再来看看效果:

y14h9-1o6yb.gif

返回值

值得注意的是,getUserAction函数可能是有返回值的,所以我们也要返回函数的结果,但是当immediate为false的时候,因为使用了setTimeout,我们将func.apply(context, args)的返回值赋给变量,最后在return的时候,因为timeout是一个异步函数,所以值会是undefined,所以我们只在immediate为true的时候返回值
所以我们再来优化一下我们的函数

  1. // 第五版
  2. function debounce(func, wait, immediate) {
  3. var timeout, immediate, result;
  4. return function() {
  5. var context = this;
  6. var args = arguments;
  7. if (timeout) clearTimeout(timeout);
  8. if (immediate) {
  9. var callNow = !timeout;
  10. timeout = setTimeout(function() {
  11. timeout = null;
  12. }, wait);
  13. if (timeout) result = func.apply(context, args);
  14. } else {
  15. timeout = setTimeout(function() {
  16. func.apply(context, args);
  17. }, wait);
  18. }
  19. return result;
  20. }
  21. }

这里效果就不展示了,在实际应用场景中很少出现这种情况。

取消防抖函数延迟

最后我们在思考一个小需求,我们希望可以手动取消debounce函数的延时,比如说我们的debounce函数的wait设置为10s,immediate为true的话,我们需要等10s才能重新触发事件,现在我希望点一下按钮就取消防抖,然后重新执行。

为了实现这个需求,我们来写最后一版代码:

  1. // 第六版代码
  2. function debounce(func, wait, immediate) {
  3. var timeout, immediate, result;
  4. var debounced = function() {
  5. var context = this;
  6. var args = arguments;
  7. if (timeout) clearTimeout(timeout);
  8. if (immediate) {
  9. var callNow = !timeout;
  10. timeout = setTimeout(function() {
  11. timeout = null;
  12. }, wait);
  13. if (callNow) result = func.apply(context, args);
  14. } else {
  15. timeout = setTimeout(function() {
  16. func.apply(context, args);
  17. }, wait);
  18. }
  19. return result;
  20. }
  21. debounced.cancel = function() {
  22. clearTimeout(timeout);
  23. timeout = null;
  24. }
  25. return debounced;
  26. }

那么我们该怎么使用这个cancel函数呢?以上诉demo 为例

  1. var count = 1;
  2. var container = document.querySelector('#container');
  3. var button = document.querySelector('.cancel');
  4. function getUserAction() {
  5. container.innerHTML = count++;
  6. }
  7. var setUserAction = debounce(getUserAction, 10000, true);
  8. container.addEventListener('mousemove', setUserAction);
  9. button.addEventListener('click', function() {
  10. setUserAction.cancel()
  11. })

我们来看下演示效果吧:

txshx-fp6t4.gif
到这里我们就完成了underscore的防抖函数模拟了~~~~


:::info 本文档摘抄自:冴羽大大的博客 :::