# debounce、throttle

# debounce(防抖)

# 演示debounce的用法

  • debounce就是实现防抖的工具方法, 用法很简单,只需要将真正需要执行的方法,传参给debounce第一个参数,第二个参数是控制防抖的时间,当然还会有第三个参数,我们后面说



 

let test = function(e){
  console.log(e);
}
window.onmousemove = debounce(test, 500)
1
2
3
4
  • 防抖可以理解为:相当于我们给这个函数设置了一个倒计时(第二个参数),第一次触发事件,倒计时开始,当倒计时为0的时候,才会真正的执行绑定的事件(第一个参数),但是如果倒计时还没有结束,就再次触发事件,倒计时将回到初始值,继续在进行倒计时。

# 作用

  • 我们来通过一个示例就可以更清晰的理解防抖的作用




 




搜索框:<input type="text" id="input"/>
<script>
  let inputEl = document.getelementById('input')

  inputEl.oninput=debounce(function(){
    // 调用接口来查询
  },500)
</script>
1
2
3
4
5
6
7
8
  • 这是一个很常见的例子,一个搜索框,搜索框监听oninput事件来向后台调用接口,但是oninput事件没按一个键都会触发,也就调很多次接口,这就会出现一种情况,后调用的接口可能先响应,先调用的接口可能数据后响应,这样数据就不对了

# 由浅入深

  • 还是老样子,我们由浅入深的一步一步分析,先实现一个最简单的,可能有缺陷,没关系,我们慢慢完善
function debounce(fn, delay){
  let timeout;
  return function(){
    if(timeout) clearTimeout(timeout);
    timeout = setTimeout(fn, delay);
  }
}
1
2
3
4
5
6
7
  • 一眼看上去,可以看到这是一个闭包的方式,将timeout变量缓存起来
  • 还可以确定这个方法将返回一个函数,这也符合预期,因为要将这个函数返回,绑定给监听的事件

 

 




functon test(){/*其他逻辑*/}
window.onmousemove = debounce(test,500) 
/*相当于*/ 
window.onmousemove = function(){
  if(timeout) clearTimeout(timeout);
  timeout = setTimeout(test, delay);
}
1
2
3
4
5
6
7
  • 匿名函数体中的逻辑也很简单,如果timeout不为空,就清除定时器,如果为空才会往下走,设置定时器。来执行test方法
  • 好了到这里一个漏洞百出的防抖函数完成了,之所以写成这样,是为了方便理解核心思想

# 优化环节

  • 我们在看一个示例,来引出我们接下来要完善的地方






 



<div id="container" style="width:500px;height:500px;background:#333;"> </div>
<script>
  function changeColor(event){
    console.log(event)
    console.log(this)
  }
  document.getElementById('container').onmousemove = debounce(changeColor,500) 
</script>

1
2
3
4
5
6
7
8
9
  • 可以看到,我们并没有预期的输出参数event,返回的this也不是idcontainerdiv,而是window
  • 所以接下来我们要处理的就是传参和正确的上下文



 
 


 




function debounce(fn, delay){
  let timeout;
  return function(){
    let args = arguments
    let conetxt = this
    if(timeout) clearTimeout(timeout);
    timeout = setTimeout(()=>{
      fn.call(conetxt,...args)
    }, delay);
  }
}
1
2
3
4
5
6
7
8
9
10
11
  • 通过在匿名函数中声明当前函数的入参,和上下文,通过call方法来改变this指向并传递参数
  • 在最开始的时候我们提到了,debounce还是需要第三个参数,第三个参数是用来控制,第一次触发事件是否立即执行一次真正的方法,接下来在进行优化,来实现这个功能






 


 




function debounce(fn, delay, immediate){
  let timeout;
  return function(){
    if(timeout) clearTimeout(timeout);
    let args = arguments
    let conetxt = this
    if(!timeout && immediate) fn.call(conetxt,...args)
    timeout = setTimeout(()=>{
      fn.call(conetxt,...args)
      timeout = null
    }, delay);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
  • 7行,通过判断timeoutimmediate两个变量来判断是否为立即执行,当!timeout为这true时和immediatetrue,才证明时触发了第一次执行,并且为立即执行
  • 10行,为了保证立即执行的逻辑,所以在每次真正执行方法后,都要将闭包缓存的timeout设置为空,为了配合下一次的立即执行逻辑,如果不设置为空,下次就不会立即执行了
  • ok到这里就全部结束了,如果还有什么优化点,后面会继续补充

# throttle(节流)

# 演示节流用法

<div id="container" style="width:500px;height:500px"> </div>
<script>
  function test(){
    let color = ['red','blue','green','yellow','orange']
    this.style.background = color[Math.round(Math.random()*5)]
  }
  document.getElementById('container').onmousemove = throttle(test,2000) 
</script>
1
2
3
4
5
6
7
8

# 原理

  • 在连续触发某个事件,在规定的时间内(throttle第二个参数),只会执行一次绑定的函数(throttle第一个参数)

# 代码实现

  • 实现节流的方式有很多,我们先实现一个易于理解的版本
function throttle(fn, delay){
  let temp = 0
  return function() {
    let current = Date.now()
    if (current - temp > delay) {
      fn.call(this, ...arguments)
      temp = current
    }
  }
}
1
2
3
4
5
6
7
8
9
10
  • 看这段代码,可以很容易看出,又是利用了闭包的方式缓存了一个变量temp
  • 接下里我们又申请了一个变量来获取当前的时间戳
  • 用当前时间戳和保存的变量进行相减,如果这个值大于了我们传入的delay值,就会执行绑定的方法
  • 因为考虑到参数和上下文的问题,我们可以用call方法来帮助我们改变this的指向,并传入参数
  • 最后一步很关键,要更新temp变量的值为当前时间戳,否则后面每次触发事件都会符合第五行的判断,fn也就每次都会执行,节流作用就失效了

# 优化

  • 简单版本完事了,还要进行一下优化,需要判断一下参数是否传递正确
function throttle(fn, delay){
  if (typeof fn !== 'function') {
    throw new TypeError('Expected a function')
  }
  if (typeof delay !== 'number') {
    throw new TypeError('Expected a number')
  }
  let temp=0
  return function(){
    let current = Date.now()
    if(current - temp > delay){
      fn.call(this,...arguments)
      temp = current
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  • 增加了前6行代码,用来判断传入参数是否符合要求,ok到这里throttle函数就完成了

# 其它实现方式

# 利用setTimeout来实现

function throttle(fn, delay){
  if (typeof fn !== 'function') {
    throw new TypeError('Expected a function')
  }
  if (typeof delay !== 'number') {
    throw new TypeError('Expected a number')
  }
  let timeout=null
  return function(){
    if(!timeout){
      let args = arguments
      let context = this
      timeout = setTimeout(()=>{
        fn.call(context, args)
        clearTimeout(timeout)
        timeout = null
      }, delay)
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  • 主要看第10行和16行即可,主要是通过判断当前timeout变量是否为null来判断是否可以执行
Last Updated: 1/23/2022, 10:16:22 AM