underscore之防抖函数

1,362 阅读4分钟

underscore之防抖函数

相信点进来的读者对防抖的功能已经不陌生了吧,有时面试也会要求手写防抖函数,这个功能在开发中应用也很广泛,具体有那些应用呢,先举一个栗子吧

应用举例

我们先写一个index.html文件:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>xhx</title>
  <style>
    .box {
      width: 300px;
      height: 300px;
      background-color: green;
      font-size: 40px;
      text-align: center;
    }
  </style>
</head>

<body>
  <div class="box" id="box">

  </div>
</body>
<script>
  let box = document.getElementById('box');
  let count = 0;
  const MouseMove = () => {
    console.log("send a request message");
    console.log("consume cpu");
    count++;
    box.innerHTML = count;
  }
  box.addEventListener("mousemove", MouseMove)
</script>

</html>

运行在chrome浏览器如下所示

每当鼠标移动都会触发一个事件,对于目前的电脑性能足够处理这些事件,可是考虑一下该事件是向服务器发送一个请求,或者是一个很消耗性能的操作,这样的话,服务器的压力就大了,电脑的cpu也可能处理不过来,这只是举一个栗子,类似的场景还有很多,比如:

  1. 文本输入框的验证,输入完成之后验证一次即可,不需要每次都发请求验证。
  2. size/scroll的触发统计事件
  3. 等等

这时就需要一个防抖函数了

这个函数有什么用呢,又或者说我们想达到什么效果呢?

不要不停的触发事件,等一次动作执行完成后再触发动作,比如当鼠标停下来时向服务器发送一个请求,如电话文本输入框,只有当你把整个电话输入完成时,才判断电话的准确性与否,而不是每输入一个数字都要判断一次

1. 初版

我们先写一个解决目前需求的代码

// 第一版代码
  function debounce(func, wait) {
    let timeout;
    return function () {
      clearTimeout(timeout);
      timeout = setTimeout(func, wait);
    }
  }

怎么使用呢,我把script标签的代码放在了下面

<script>
// 定义防抖函数
  function debounce(func, wait) {
    let timeout;
    return function () {
      clearTimeout(timeout);
      timeout = setTimeout(func, wait);
    }
  }

  let box = document.getElementById('box');
  let count = 0;
  const MouseMove = () => {
    console.log("send a request message");
    console.log("consume cpu");
    count++;
    box.innerHTML = count;
  }
  // 使用防抖函数
  box.addEventListener("mousemove", debounce(MouseMove, 500))
</script>

使用后效果如下:

只有当完整的事件停止500ms后,事件才触发


我们再考虑一个问题,使用了setTimeout函数之后,被调用函数的this指向全局对象,为了解决这个问题,只需要改变一下this的指针就行了,何种方式改变,任意!这里我采用call

2. 保持this

// 第二版代码
function debounce(func, wait) {
    let timeout;
    let _this = this;
    return function () {
      clearTimeout(timeout);
      timeout = setTimeout(function(){
        func.call(_this)// 外面有一个函数包裹着,否则调用call时会直接执行,setTimeout就不起作用了
      }, wait);
    }
  }

这时候看到了call就自然会想到可以传入几个参数吧,对的万一原函数又参数呢,这也很简单,这就有了第三版代码

3. 传参

// 第三版代码
<script>
function debounce(func, wait) {
  let timeout;
  let _this = this;
  return function () {
    clearTimeout(timeout);
    timeout = setTimeout(function(){
      func.call(_this, 8, 8)  // 改动,加了参数
    }, wait);
  }
}

let box = document.getElementById('box');
let count = 0;
const MouseMove = (a, b) => {  // 改动,加了参数
  console.log("send a request message");
  console.log("consume cpu");
  console.log(a + b);
  count++;
  box.innerHTML = count;
}
box.addEventListener("mousemove", debounce(MouseMove, 500))
</script>

查看右侧调试窗口打印出了16,说明传参成功

到这里不知道你会不会感觉我们背道而驰了,本来想鼠标刚放上去事件就应该触发一次,等了一会再触发的话,谁知道只有没有事件,为了提升用户体验,我们应该先让事件触发一次,之后再去考虑防抖,这就有了我们的第四版代码,为了这个debounce函数适用性更广,我们加了一个参数immediate来判断函数是不是要立即执行

4. 立即执行

 <script>
 // 第四版代码
function debounce(func, wait, immediate) {
  let timeout;
  let _this = this;
  return function () {
    if (timeout) clearTimeout(timeout);
    if (immediate) {// 通过传入的参数判断是否需要立即执行
      var callNow = !timeout; // 第一次调用时,timeout为undefined,callNow为true
      if (callNow) func.call(_this, 8, 8);
      timeout = setTimeout(function () {
        timeout = null;
      }, wait);
    } else {
      timeout = setTimeout(function () {
        func.call(_this, 8, 8)
      }, wait);
    }
  }
}

let box = document.getElementById('box');
let count = 0;
const MouseMove = (a, b) => {
  console.log("send a request message");
  console.log("consume cpu");
  console.log(a + b);
  count++;
  box.innerHTML = count;
}
box.addEventListener("mousemove", debounce(MouseMove, 500, true))
</script>

如动画所示,刚进去就直接变为1,说明执行一次,后面停止一会后,再移动鼠标,数字直接变成2,之后停一会,移动一下,就会不断加1

不知道你发现了一个问题没,以前当事件结束后会执行一次函数,而上诉代码,只会在开始时执行,而结束后不会执行函数,这时候就需要多加一行代码了

5.立即执行和结束执行

<script>
// 第五版代码
function debounce(func, wait, immediate) {
  let timeout;
  let _this = this;
  return function () {
    if (timeout) clearTimeout(timeout);
    if (immediate) {// 通过传入的参数判断是否需要立即执行
      var callNow = !timeout; // 第一次调用时,timeout为undefined,callNow为true
      if (callNow) func.call(_this, 8, 8);
      timeout = setTimeout(function () {
        timeout = null;// 为下一次立即执行做好准备
        // 多加的一行代码
        func.call(_this, 8, 8); // 鼠标动作停止一个wait执行一次,应用场景:文本输入框停止输入了,应该分析文本框中的内容
      }, wait);
    } else {
      timeout = setTimeout(function () {
        func.call(_this, 8, 8)
      }, wait);
    }
  }
}

let box = document.getElementById('box');
let count = 0;
const MouseMove = (a, b) => {
  console.log("send a request message");
  console.log("consume cpu");
  console.log(a + b);
  count++;
  box.innerHTML = count;
}
box.addEventListener("mousemove", debounce(MouseMove, 500, true))
</script>

现在效果如下所示

此时应该注意一点,就是MouseMove函数可能有返回值

直接改代码,改动的代码也比较好理解

6. 返回值

// 第六版代码
<script>
 function debounce(func, wait, immediate) {
   let timeout;
   let result;
   let _this = this;
   return function () {
     if (timeout) clearTimeout(timeout);
     if (immediate) {// 通过传入的参数判断是否需要立即执行
       var callNow = !timeout; // 第一次调用时,timeout为undefined,callNow为true
       if (callNow) result = func.call(_this, 8, 8);
       timeout = setTimeout(function () {
         timeout = null;// 为下一次立即执行做好准备
         result = func.call(_this, 8, 8); // 鼠标动作停止一个wait执行一次,应用场景:文本输入框停止输入了,应该分析文本框中的内容
       }, wait);
     } else {
       timeout = setTimeout(function () {
         result = func.call(_this, 8, 8)
       }, wait);
     }
     return result
   }
 }

 let box = document.getElementById('box');
 let count = 0;
 const MouseMove = (a, b) => {
   console.log("send a request message");
   console.log("consume cpu");
  // console.log(a + b);
   count++;
   box.innerHTML = count;
   return a + b
 }
 // 注意
 const bindFun = ()=>{
   console.log(debounce(MouseMove, 500, true)());
 }
 box.addEventListener("mousemove", bindFun)
</script>

不知读者是否发现一个问题,在注意后面的代码中,防抖作用会消失,我现在的分析是,每次执行bindFun函数时,会运行一个新的MouseMove,导致每次都是第一次运行,所以防抖函数失去作用,所以现在的情况是防抖函数成功返回了返回值,但我不知道如何调用,如果读者有新发现,请不吝赐教!!!

7. 取消

再来做一个取消的功能吧,就是这么感性,突然想让他不执行函数了

// 第七版代码
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>xhx</title>
  <style>
    .box {
      width: 300px;
      height: 300px;
      background-color: green;
      font-size: 40px;
      text-align: center;
    }
  </style>
</head>

<body>
  <div class="box" id="box">

  </div>
  <button id="button">取消执行</button>
</body>
<script>
  function debounce(func, wait, immediate) {
    let timeout;
    let result;
    let _this = this;
    let debounced = function () {
      if (timeout) clearTimeout(timeout);
      if (immediate) {// 通过传入的参数判断是否需要立即执行
        var callNow = !timeout; // 第一次调用时,timeout为undefined,callNow为true
        if (callNow) result = func.call(_this, 8, 8);
        timeout = setTimeout(function () {
          timeout = null;// 为下一次立即执行做好准备
          result = func.call(_this, 8, 8); // 鼠标动作停止一个wait执行一次,应用场景:文本输入框停止输入了,应该分析文本框中的内容
        }, wait);
      } else {
        timeout = setTimeout(function () {
          result = func.call(_this, 8, 8)
        }, wait);
      }
      return result
    }
    // 取消
    debounced.cancel = function(){
      clearTimeout(timeout);
      timeout = null;
    }
    return debounced
  }

  let box = document.getElementById('box');
  let button = document.getElementById('button');
  let count = 0;
  const MouseMove = (a, b) => {
    console.log("send a request message");
    console.log("consume cpu");
   // console.log(a + b);
    count++;
    box.innerHTML = count;
    return a + b
  }
  
  let newDebounce = debounce(MouseMove, 500, true);
  box.addEventListener("mousemove", newDebounce)
  button.addEventListener("click", newDebounce.cancel)// 调用取消按钮
</script>

</html>

动图中一开始不点取消时,停一会后会执行加一操作,点取消后,无论停多久都不会加一了

8. 更多

下一篇博客写节流,如果这篇博客对你有所启发的话,也可以看看我的下篇博客哟

这篇博客快写了一天了,冒着挂科的风险写博客,这大概就是小孩的思想吧,开心就行了,觉得不错的点个赞鼓励一下哦,上诉内容中有一个未解决的问题,希望能解决的读者帮帮孩子吧!

参考文章: JavaScript专题之跟着underscore学防抖

我在此基础上面加了许多思考和自己的理解,也提出了里面未涉及到的问题。嗯,对的,本孩太棒了!