前言

在前端开发过程中,我们会遇到一些频繁触发的事件,比如:window的resize和scroll、mousemove、keyup、keydown等等。

下面我们通过代码来看看mousemove事件是如何频繁触发的:

index.html文件代码如下:

  1. <!DOCTYPE html>

  2. <html>

  3. <head>

  4. <metacharset="utf-8">

  5. <title>debounce防抖</title>

  6. <style>

  7. #container{

  8.            width: 100%; height: 200px; line-height: 200px; text-align: center; color: #fff; background-color: #444; font-size: 30px;

  9. }

  10. </style>

  11. </head>

  12. <body>

  13. <divid="container"></div>


  14. <scriptsrc="debounce.js"></script>

  15. <script>

  16. var count = 1;

  17. var container = document.getElementById("container");


  18. function mouseMove() {

  19.                console.log(this);

  20.                container.innerHTML = count++;

  21. };


  22.            container.onmousemove = mouseMove

  23. </script>

  24. </body>

  25. </html>

运行该html文件,我们将鼠标在我们定义的矩形区域移动,只是简单的从下往上滑动,mouseMove函数就被触发了99次。

JavaScript-函数防抖_java

假设mouseMove函数时复杂的回调函数或者是ajax请求,如果我们没有对事件处理函数调用的频率进行限制,会加重浏览器的负担,导致用户体验极差。这时候我们可以采用debounce(防抖)或throttle(节流)的方式来减少调用频率,同时又不影响实际效果。

今天我们主要讲讲防抖。

防抖原理

函数防抖(debounce):在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。

初步实现

根据上面防抖的描述,我们可以利用延时函数setTimeout写第一版防抖函数的实现代码:

function debounce(func, wait) {var timeoutreturnfunction() {    clearTimeout(timeout)    timeout = setTimeout(func, wait)}}

在最开始的例子中使用debounce:

container.onmousemove = debounce(mouseMove, 1000)

这样子修改后,只有在我们移动完1s内不再触发,才会执行回调事件mouseMove。

this

我们在mouseMove函数中执行 console.log(this),会发现不使用debounce和使用debounce情况下,this的值是不一样的。

不使用debounce时this的值为

<divid="container"></div>

而使用debounce函数时,this则会指向window对象。

所以我们需要将this指向正确的函数,这时候我们可以利用apply()方法实现。代码修改如下:

function debounce(func, wait) {var timeoutreturnfunction() {var context = this    clearTimeout(timeout)    timeout = setTimeout(function() {      func.apply(context)}, wait)}}

修改完成后,我们再触发事件,可以看到此时this的指向正确了。

event对象

JavaScript在事件处理函数中会提供事件对象event,我们将mouseMove函数修改如下:

function mouseMove(e) {    console.log(this);    console.log(e);    container.innerHTML = count++;};

如果不使用debounce函数,控制台打印的e是MouseEvent对象,但当我们调用debounce函数,打印出来的确实undefined。

所以我们再次修改代码如下:

  1. function debounce(func, wait) {

  2. var timeout

  3. returnfunction() {

  4. var context = this

  5. var args = arguments


  6.    clearTimeout(timeout)

  7.    timeout = setTimeout(function() {

  8.      func.apply(context, args)

  9. }, wait)

  10. }

  11. }

至此,我们修复了this指向和event对象问题,整个防抖函数已经算是比较完善了。

立即执行

接下来我们再考虑一个新的需求:如果我们希望是在事件一触发就立刻执行函数,而不是等到事件停止触发后再执行;并且等到停止触发n秒后,才可以重新触发执行。

我们可以通过immediate参数来判断是否立刻执行,代码修改如下:

  1. function debounce(func, wait, immediate) {

  2. var timeout

  3. returnfunction() {

  4. var context = this

  5. var args = arguments


  6. if(timeout) {

  7.            clearTimeout(timeout)

  8. }

  9. if(immediate) {

  10. // 已经执行过不再执行

  11. var callNow = !timeout

  12.            timeout = setTimeout(function() {

  13.                timeout = null

  14. }, wait)

  15. if(callNow) {

  16.                func.apply(context, args)

  17. }

  18. } else{

  19.            timeout = setTimeout(function() {

  20.              func.apply(context, args)

  21. }, wait)

  22. }

  23. }

  24. }

container.onmousemove = debounce(mouseMove, 1000, true)

返回值

我们需要注意的一点是:mouseMove函数可能是有返回值的,所以我们也要返回函数的执行结果,但是immediate为false的时候,因为使用setTimeout,我们将func.apply(context, args)的返回值赋给变量,最后再return的时候,值会一直是undefined,所以我们只在immediate为true的时候返回函数的执行结果。

  1. function debounce(func, wait, immediate) {

  2. var timeout, result

  3. returnfunction() {

  4. var context = this

  5. var args = arguments


  6. if(timeout) {

  7.            clearTimeout(timeout)

  8. }

  9. if(immediate) {

  10. // 已经执行过不再执行

  11. var callNow = !timeout

  12.            timeout = setTimeout(function() {

  13.                timeout = null

  14. }, wait)

  15. if(callNow) {

  16.                result = func.apply(context, args)

  17. }

  18. } else{

  19.            timeout = setTimeout(function() {

  20.              func.apply(context, args)

  21. }, wait)

  22. }

  23. return result

  24. }

  25. }

取消

假设防抖的时间间隔为10秒,immediate为true的情况下,只有等10秒后才能重新触发事件,这时候我希望有个按钮可以取消防抖,这样我再去触发时,又可以立即执行了。

下面我们来实现这个取消功能:

  1. function debounce(func, wait, immediate) {

  2. var timeout, result

  3. var debounced = function() {

  4. var context = this

  5. var args = arguments


  6. // 每次新的尝试调用func,会使抛弃之前等待的func

  7. if(timeout) clearTimeout(timeout)


  8. // 如果允许新的调用尝试立即执行

  9. if(immediate) {

  10. // 如果之前尚没有调用尝试,那么此次调用可以立马执行,否则就需要等待

  11. var callNow = !timeout

  12. // 刷新timeout

  13.            timeout = setTimeout(function() {

  14.                timeout = null

  15. }, wait)

  16. // 如果能被立即执行,立即执行

  17. if(callNow) result = func.apply(context, args)

  18. } else{

  19.            timeout = setTimeout(function() {

  20.              func.apply(context, args)

  21. }, wait)

  22. }

  23. return result

  24. }


  25.    debounced.cancel = function() {

  26.        clearTimeout(timeout)

  27.        timeout = null

  28. }

  29. return debounced

  30. }

如何调用这个cancel函数?

  1. var setMouseMove = debounce(mouseMove, 10000, true)

  2. container.onmousemove = setMouseMove


  3. // buttonClick为button的click事件

  4. function buttonClick() {

  5.  setMouseMove.cancel()

  6. }

效果如下:

JavaScript-函数防抖_java_02

到这里,一个完整的debounce函数已经实现了。

总结

debounce防抖函数,满足的是:高频下只响应一次。

在实际开发过程中,常见的应用场景有:

  • 在输入框快速输入文字(高频),我们只想在其完全停止输入时再对输入文字做处理(一次)

  • ajax,大多数场景下,每个异步请求在短时间内只能响应一次,比如下拉刷新、不停地上拉加载,但只发送一次ajax请求。