没有比这个更详细的手写防抖函数了吧

1,233 阅读17分钟

防抖

本文件所有代码都可以直接复制,然后在浏览器端打开调试,查看相应效果

这片文章是防抖函数,下一篇文章写一下节流的函数。觉得满意的话,给个赞呀

插播一下另外一篇文章

没有比这个更详细的手写节流函数了吧

第一版、原版,无添加任何'防腐剂'

没有防抖次效果,只要滑动就会有事件发生。如果是count++看成函数如发送网络请求,就会一直进行请求。在实际开发过程中,这样操作肯定是不行的,以为频繁的去切换网络请求,会给后台服务器造成非常大的压力

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <style>
    #container {
      width: 100%;
      height: 200px;
      line-height: 200px;
      text-align: center;
      color: #fff;
      background-color: #444;
      font-size: 30px;
    }
  </style>
</head>
<body>
  <!--主要是声明变量,获取DOM,声明方法,绑定onmousemove的方法-->
  <div id="container"></div>
  <script>
    // 1. 先声明一个变量
    let count = 1;
    // 2. 获取DOM
    let container = document.querySelector("#container")
    // 3. 封装函数,这里doSomething可以是实际的网络请求
    function doSomething() {
      container.innerHTML = count++;
    }
    // 4. 给container绑定鼠标移动事件。调用doSomething,注意这里调用是没有()的
    container.onmousemove = doSomething  //这个是自己写的,没有做到防抖,注释掉下面的引用防抖的函数可以看一下效果。只要鼠标在背景区域滑动,数据就会一直加1
  </script>
</body>
</html>

第二版、使用第三方js库来实现防抖

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <style>
    #container {
      width: 100%;
      height: 200px;
      line-height: 200px;
      text-align: center;
      color: #fff;
      background-color: #444;
      font-size: 30px;
    }
  </style>
</head>
<body>
  <!--主要是声明变量,获取DOM,声明方法,绑定onmousemove的方法-->
  <div id="container"></div>
  <!--导入第三方防抖js库-->
  <script src="https://cdn.bootcss.com/underscore.js/1.9.1/underscore.js"></script>
  <script>
    // 1. 先声明一个变量
    let count = 1;
    // 2. 获取DOM
    let container = document.querySelector("#container")
    // 3. 封装函数,这里doSomething可以是实际的网络请求
    function doSomething() {
      container.innerHTML = count++;
    }

    // 4. 给container绑定鼠标移动事件。调用doSomething,注意这里调用是没有()的
    // // 注意 第三方的debounce函数调用的时候是有_.的,必须这样调用才会执行防抖函数
    container.onmousemove = _.debounce(doSomething,1000) //这里是调用deboucne函数的效果。只有当鼠标不滑动了,1秒后才执行+1操作

    
    // 防抖:事件响应函数在一段时间之后才执行,如果在这段时间内再次调用,则重新计算执行时间;只有当1秒之后不滑动了才会执行相应方法。这个就叫做防抖。
    // 这里我们设置了doSomething。这个方法在1秒后执行,如果我们在停止滑动1秒后,count才会+1;但是如果我们在停止滑动0.9秒后,在0.92秒后又开始滑动了,那么防抖函数又会重新计算时间,当在0.92秒开始滑动了一段时间后,停止了,1秒后count开始+1
  </script>
</body>
</html>

防抖在生活中有几个形象的比喻。

  • 公交车停靠站,在站牌有10个人等着上车,只要这十个人一直在上车,公交车就得等着;10个人都上去了,车就走了。突然,后面又来了一个小姐姐,正在狂奔而来,司机一看,呦呵,女的。不行,我不能走,我得等等她。终于小女孩上车了,司机开车了,注意,这可是真正的开车,真正的开车。
  • 打王者荣耀游戏,有一二三三个技能,三个技能都点完了,就不能点了,只能等技能冷却了下来才可以。

第三版、手写实现防抖函数

首先分成了js文件和html文件,方便后续操作

html文件

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</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="anti-shake2.js"></script>
  <script>
    let container = document.querySelector("#container");
    // 给container绑定鼠标移动事件。调用doSomething,注意这里调用是没有()的
    container.onmousemove = debounce(doSomething,1000,true);
    // 注意,我们是手写的debounce防抖函数,这里是没有_.的,只有引用第三方函数underscore函数才是这样使用的。要问为什么?对不起,underscore就是这样子调用的container.onmousemove = _.debounce(doSomething,1000,true)
  </script>
</body>
</html>

js文件

// 防抖的函数
function debounce (func,wait) { //这个函数是我们自己写的,有两个参数,第一个是需要执行的函数,第二个参数是延迟时间
  let timeout; // 声明一个timeout变量
  return function() { //这里return还是一个函数
    clearTimeout(timeout) // 每次都清空一下timeout
    timeout = setTimeout(func,wait) // 设定setTimeout来设置时间
  }
}

// 先声明一个变量
let count = 0;
// 获取DOM
let container = document.querySelector("#container");
// 封装函数,这里doSomething可以是实际的网络请求
function doSomething() {
  container.innerHTML = count++;
}
// 给container绑定鼠标移动事件。调用doSomething,注意这里调用是没有()的
container.onmousemove = debounce(doSomething,300); //这里是说,当鼠标停止滑动的时候,300ms后count+1,当然你自己也可以为了效果更明显,可以改成1s或者2s

上面的一个基本的功能就实现了。每次滑动停止的那一刻,300ms之后count+1;当滑动停止那一刻,298ms内,299ms又开始滑动了,此时count还没有超过300ms,所以还没有加1,但是我们在299ms开始滑动鼠标了,那么又会重新计算时间。重新计算什么时间呢?你在299ms开始滑动停止的那一刻,300ms后开始count+1。

防抖的特点就是每次都会重新计算时间。

你以为就这么简单,结束了??

太年轻哦,小老弟。。

第四版、使用第三方js库来实现防抖--第三个参数

前面我们写的都是debounce有两个参数,可以达到基本是的效果。但是第三方插件的uderscore封装的debounce函数还有第三个参数。默认不写的情况下是false,如果是true,就是立即执行的意思。如果设定了true,意思就是当停止滑动1s后,再次滑动不会有延迟,会立即执行;如果1s内不断滑动,不会执行+1操作,只有停止滑动1s后,再次开始滑动的时候才会立即执行+1的操作。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <style>
    #container {
      width: 100%;
      height: 200px;
      line-height: 200px;
      text-align: center;
      color: #fff;
      background-color: #444;
      font-size: 30px;
    }
  </style>
</head>
<body>
  <!--主要是声明变量,获取DOM,声明方法,绑定onmousemove的方法-->
  <div id="container"></div>
  <script src="https://cdn.bootcss.com/underscore.js/1.9.1/underscore.js"></script>
  <script>
    // 先声明一个变量
    let count = 1;
    // 获取DOM
    let container = document.querySelector("#container")
    // 封装函数,这里doSomething可以是实际的网络请求
    function doSomething() {
      container.innerHTML = count++;
    }
    
    // 注意 第三方的debounce函数调用的时候是有_.的,必须这样调用才会执行防抖函数
    container.onmousemove = _.debounce(doSomething,1000,true) //这里是调用deboucne函数的效果。只有当鼠标不滑动了,1秒后,再次滑动,就立即执行+1操作

  </script>
</body>
</html>

第五版、手写防抖---第三个参数

html文件

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <style>
    #container {
      width: 100%;
      height: 200px;
      line-height: 200px;
      text-align: center;
      color: #fff;
      background-color: #444;
      font-size: 30px;
    }
  </style>

</head>
<body>
  <!--声明变量,获取DOM,声明方法,绑定onmousemove的方法-->
  <div id="container"></div>
  <script src="anti-shake2.js"></script>
  <script>
    let container = document.querySelector("#container");
    // 给container绑定鼠标移动事件。调用doSomething,注意这里调用是没有()的
    container.onmousemove = debounce(doSomething,1000,true); 
  </script>
</body>
</html>

js文件

// 第三个参数,使用imme来控制是否立即执行
function debounce (func,wait,imme) {
  let timeout;
  return function() { // 同样是返回一个函数
    clearTimeout(timeout)
    if(imme) { // 判断是否是true,立即执行
      
      // 这里面就有很多中写法,第一种 有人想 这还不简单,不就是调用一下之前的操作吗
      // if(callNow) func.apply(context) //这样写没有意义,imme一直是true。一直会执行,和没写没有什么区别。
      
      // 第二种,使用第三个变量来控制true和false。
      /** let callNow = true; //这样设置有问题,不执行。所以我们最终使用的是  设置成和timeout有关的变量 的想法
      timeout = setTimeout(()=>{
       	callNow = false;
      },wait)
    // 立即执行
      if(callNow) func.apply(context) 
      */
      
      // 第三种,设置了和timeout有关的变量,每次都清空一下timeout的值,就完成了操作
      let callNow = !timeout;
      timeout = setTimeout(()=>{
        timeout = null;
      },wait)
    // 立即执行
      if(callNow) func() 
    } else {
      // 不会立即执行
      timeout = setTimeout(function(){
        func()
      },wait)
    }

  }
}

// 需要将let container ... 和 contianer.onmousemove放到需要调用的html中。当然实际开发过程中我们手写的防抖函数里面只是上面的js,下面的声明变量和函数封装都是在相关的业务代码里面。也就是在html里面的

// 先声明一个变量
let count = 0;
// 封装函数,这里doSomething可以是实际的网络请求
function doSomething() {
  container.innerHTML = count++;
}

第六版、完善手写html和js

将js文件里面的业务代码,也就是相关函数和声明的变量都放到了html里面,实际情况也是这样子操作的

html文件

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <style>
    #container {
      width: 100%;
      height: 200px;
      line-height: 200px;
      text-align: center;
      color: #fff;
      background-color: #444;
      font-size: 30px;
    }
  </style>

</head>
<body>
  <!--声明变量,获取DOM,声明方法,绑定onmousemove的方法-->
  <div id="container"></div>
  <script src="anti-shake2.js"></script>
  <script>
    // 下面是相关的业务操作的代码,和自己封装的js完全分隔开了
    // 先声明一个变量
		let count = 0;
		// 封装函数,这里doSomething可以是实际的网络请求
		function doSomething() {
 		 container.innerHTML = count++;
		}
    let container = document.querySelector("#container");
    // 给container绑定鼠标移动事件。调用doSomething,注意这里调用是没有()的
    container.onmousemove = debounce(doSomething,1000,false); // 平时我们都是使用的false,这样能更好的达到防抖效果,可以不写,变成 // container.onmousemove = debounce(doSomething,1000);  也可以是这种container.onmousemove = debounce(doSomething,1000,false);
  </script>
</body>
</html>

js文件

// 第三个参数,使用imme来控制是否立即执行
function debounce (func,wait,imme) {
  let timeout;
  return function() { // 同样是返回一个函数
    clearTimeout(timeout)
    if(imme) { // 判断是否是true,立即执行
      
      // 这里面就有很多中写法,第一种 有人想 着还不简单,不就是调用一下之前的操作吗
      // if(callNow) func.() //这样写没有意义,imme一直是true。一直会执行,和没写没有什么区别。
      
      // 第二种,使用第三个变量来控制true和false。
      /** let callNow = true; //这样设置有问题,不执行。所以我们最终使用的是  设置成和timeout有关的变量 的想法
      timeout = setTimeout(()=>{
       	callNow = false;
      },wait)
    // 立即执行
      if(callNow) func() 
      */
      
      // 第三种,设置了和timeout有关的变量,每次都清空一下timeout的值,就完成了操作
      let callNow = !timeout;
      timeout = setTimeout(()=>{
        timeout = null;
      },wait)
    // 立即执行
      if(callNow) func() 
    } else {
      // 不会立即执行
      timeout = setTimeout(function(){
        func()
      },wait)
    }

  }
}

第七版、underscore.js中的this指向问题

underscore.js中的this指向的是当前的container容器,也就是滑动事件的容器,container,我们是通过document来获取的

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <style>
    #container {
      width: 100%;
      height: 200px;
      line-height: 200px;
      text-align: center;
      color: #fff;
      background-color: #444;
      font-size: 30px;
    }
  </style>
</head>
<body>
  <!--主要是声明变量,获取DOM,声明方法,绑定onmousemove的方法-->
  <div id="container"></div>
  <!--导入第三方防抖js库-->
  <script src="https://cdn.bootcss.com/underscore.js/1.9.1/underscore.js"></script>
  <script>
    // 1. 先声明一个变量
    let count = 1;
    // 2. 获取DOM
    let container = document.querySelector("#container")
    // 3. 封装函数,这里doSomething可以是实际的网络请求
    function doSomething() {
      console.log(this) // 这里我们在控制台看一下this,是当前的container,div
      container.innerHTML = count++;
    }

    // 4. 给container绑定鼠标移动事件。调用doSomething,注意这里调用是没有()的
    // // 注意 第三方的debounce函数调用的时候是有_.的,必须这样调用才会执行防抖函数
    container.onmousemove = _.debounce(doSomething,1000) //这里是调用deboucne函数的效果。只有当鼠标不滑动了,1秒后才执行+1操作


    // 防抖:事件响应函数在一段时间之后才执行,如果在这段时间内再次调用,则重新计算执行时间;只有当1秒之后不滑动了才会执行相应方法。这个就叫做防抖。
    // 这里我们设置了doSomething。这个方法在1秒后执行,如果我们在停止滑动1秒后,count才会+1;但是如果我们在停止滑动0.9秒后,在0.92秒后又开始滑动了,那么防抖函数又会重新计算时间,当在0.92秒开始滑动了一段时间后,停止了,1秒后count开始+1
  </script>
</body>
</html>

但是当我们查看自己封装的js的时候,this指向了window,我们需要改一下

html文件

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <style>
    #container {
      width: 100%;
      height: 200px;
      line-height: 200px;
      text-align: center;
      color: #fff;
      background-color: #444;
      font-size: 30px;
    }
  </style>

</head>
<body>
  <!--声明变量,获取DOM,声明方法,绑定onmousemove的方法-->
  <div id="container"></div>
  <script src="anti-shake2.js"></script>
  <script>
    // 下面是相关的业务操作的代码,和自己封装的js完全分隔开了
    // 先声明一个变量
		let count = 0;
		// 封装函数,这里doSomething可以是实际的网络请求
		function doSomething() {
 		 container.innerHTML = count++;
		}
    let container = document.querySelector("#container");
    // 给container绑定鼠标移动事件。调用doSomething,注意这里调用是没有()的
    container.onmousemove = debounce(doSomething,1000,false); // 平时我们都是使用的false,这样能更好的达到防抖效果,可以不写,变成 // container.onmousemove = debounce(doSomething,1000);  也可以是这种container.onmousemove = debounce(doSomething,1000,false);
  </script>
</body>
</html>

js文件

// 第三个参数,使用imme来控制是否立即执行
function debounce (func,wait,imme) {
  let timeout;
  return function() { // 同样是返回一个函数
    let context = this; // 这里我们使用context设定一下this。当然这个变量是可以随意设置的。
    console.log(context) // container
    clearTimeout(timeout)
    if(imme) { // 判断是否是true,立即执行
      // 第三种,设置了和timeout有关的变量,每次都清空一下timeout的值,就完成了操作
      let callNow = !timeout;
      timeout = setTimeout(()=>{
        timeout = null;
      },wait)
    // 立即执行
      if(callNow) func.apply(context) // 通过apply来绑定一下this,这样this就指向了当前的DOM对象 container
    } else {
      // 不会立即执行
      timeout = setTimeout(function(){
        if(callNow) func.apply(context) //这里调用也是同样的操作
      },wait)
    }

  }
}

第八版、underscore.js中事件对象e的指向问题

underscore.js中的e指向的是当前的event事件对象

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <style>
    #container {
      width: 100%;
      height: 200px;
      line-height: 200px;
      text-align: center;
      color: #fff;
      background-color: #444;
      font-size: 30px;
    }
  </style>
</head>
<body>
  <!--主要是声明变量,获取DOM,声明方法,绑定onmousemove的方法-->
  <div id="container"></div>
  <!--导入第三方防抖js库-->
  <script src="https://cdn.bootcss.com/underscore.js/1.9.1/underscore.js"></script>
  <script>
    // 1. 先声明一个变量
    let count = 1;
    // 2. 获取DOM
    let container = document.querySelector("#container")
    // 3. 封装函数,这里doSomething可以是实际的网络请求
    function doSomething(e) {
      console.log(this)
      console.log(e)
      container.innerHTML = count++;
    }

    // 4. 给container绑定鼠标移动事件。调用doSomething,注意这里调用是没有()的
    // // 注意 第三方的debounce函数调用的时候是有_.的,必须这样调用才会执行防抖函数
    container.onmousemove = _.debounce(doSomething,1000) //这里是调用deboucne函数的效果。只有当鼠标不滑动了,1秒后才执行+1操作


    // 防抖:事件响应函数在一段时间之后才执行,如果在这段时间内再次调用,则重新计算执行时间;只有当1秒之后不滑动了才会执行相应方法。这个就叫做防抖。
    // 这里我们设置了doSomething。这个方法在1秒后执行,如果我们在停止滑动1秒后,count才会+1;但是如果我们在停止滑动0.9秒后,在0.92秒后又开始滑动了,那么防抖函数又会重新计算时间,当在0.92秒开始滑动了一段时间后,停止了,1秒后count开始+1
  </script>
</body>
</html>

我们之前手写的e指向的是undefined。我们可以使用arguments来操作,变成event事件对象

html文件

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <style>
    #container {
      width: 100%;
      height: 200px;
      line-height: 200px;
      text-align: center;
      color: #fff;
      background-color: #444;
      font-size: 30px;
    }
  </style>

</head>
<body>
  <!--是声明变量,获取DOM,声明方法,绑定onmousemove的方法-->
  <div id="container"></div>
  <script src="anti-shake2.js"></script>
  <script>
    // 下面是相关的业务操作的代码,和自己封装的js完全分隔开了
    // 先声明一个变量
		let count = 0;
		// 封装函数,这里doSomething可以是实际的网络请求
		function doSomething() {
 		 container.innerHTML = count++;
		}
    let container = document.querySelector("#container");
    // 给container绑定鼠标移动事件。调用doSomething,注意这里调用是没有()的
    container.onmousemove = debounce(doSomething,1000,false); // 平时我们都是使用的false,这样能更好的达到防抖效果,可以不写,变成 // container.onmousemove = debounce(doSomething,1000);  也可以是这种container.onmousemove = debounce(doSomething,1000,false);
  </script>
</body>
</html>

JS文件

// 第三个参数,使用imme来控制是否立即执行
function debounce (func,wait,imme) {
  let timeout;
  return function(e) { // 同样是返回一个函数
    console.log(e) // 如果没有let args = arguments 这一句,那么会变成undefinde;如果有这句,就变成了当前mousermove的事件对象。当然在传递的时候需要传递一下args
    console.log(this) // 如果没有let context = this,这一句,会变成window;如果有这句,就此变成了container。当然在调用的时候需要传递一下context
    let context = this; // 这里我们使用context设定一下this。当然这个变量是可以随意设置的。
    let args = arguments // 这里使用arguments传递一下
    clearTimeout(timeout)
    if(imme) { // 判断是否是true,立即执行
      // 第三种,设置了和timeout有关的变量,每次都清空一下timeout的值,就完成了操作
      let callNow = !timeout;
      timeout = setTimeout(()=>{
        timeout = null;
      },wait)
    // 立即执行
      if(callNow) func.apply(context,args) // 通过apply来绑定一下this,这样this就指向了当前的DOM对象 container;通过传递args,当前的事件对象就变成了mousemove事件对象
    } else {
      // 不会立即执行
      timeout = setTimeout(function(){
        if(callNow) func.apply(context,args) //这里调用也是同样的操作
      },wait)
    }

  }
}

终结

第八版封装的js后续可以直接拿来用,放到项目中,单独封装一个js。

当然你也可以使用第三方的underscore.js <script src="https://cdn.bootcss.com/underscore.js/1.9.1/underscore.js"></script>来操作。但是设计到一个文件大小的问题

比如underscore.js你引入了,就是120kb大小文件,而我们自己写的只有20kb;万一underscore库出现问题,自己的项目也是不会受影响的。