Skip to content

防抖/节流函数important

防抖和节流可视化指南
当前对于前端而言 JavaScript 更多的是运行在浏览器平台中,其中有很多人机交互的行为。比如:可左右切换的轮播图,当短时间内多次重复点击切换按钮时,会频繁触发该按钮绑定的事件监听进而执行对应的处理函数。而浏览器自身也有监听机制,在一定的时间范围内(Chrome 大概是 4 ~ 6ms)如果监听到了某个事件被触发,就会去执行事件对应的处理函数。而函数的执行会占用内存空间,浏览器自身作为一个应用,所占用的内存也是有限的。因此在一些高频次事件触发的场景下,不希望对应的事件处理函数立即或者多次执行。这就是需要使用防抖和节流函数的原因。

防抖函数

对于高频操作,只希望识别一次点击,可以是第一次也可以是最后一次

应用场景

  • 滚动事件
  • 输入的模糊匹配
  • 轮播图切换
  • 点击事件
  • 。。。

前置场景

页面上有一个按钮,可以连续多次点击。

html
<button id="btn">点击</button>

添加点击事件

js
const btn = document.getElementById('btn')

function btnClick() {
  console.log('点击了按钮')
}

btn.onclick = btnClick

此时并没有防抖效果

无防抖

实现防抖函数

1、防抖函数就是在我们要执行的函数外层包裹一个函数,因此它应该返回一个函数:

js
function debounce(handle) {
  return function proxy() {
    handle()
  }
}

handle 就是实际要执行的函数,比如点击事件绑定的处理函数。
调用方式需要修改为:

js
btn.onclick = debounce(btnClick, 300, false)

2、参数类型判断及默认值处理

  • handle 必须是一个函数,否则就抛出错误。
  • 除了 handle 之外,还需要有一个 wait 参数表示事件触发多久之后开始执行。以及一个 immediate 参数控制执行第一次还是最后一次。
js
/**
 * handle 需要执行的事件监听
 * wait 事件触发后多久开始执行
 * immediate 控制执行第一次还是最后一次,false:最后一次,true:第一次
 */
function debounce(handle, wait, immediate) {
  if (typeof handle !== 'function') {
    throw new Error('handle must be an function')
  }
  if (typeof wait === 'boolean') {
    immediate = wait
    wait = 300
  }
  if (typeof wait !== 'number') {
    wait = 300
  }
  if (typeof immediate !== 'boolean') {
    immediate = false
  }
  // ...
}

3、要实现控制 handle 的执行时机和次数,就需要用到 setTimeout 进行延迟执行

js
function debounce(handle, wait, immediate) {
  // ...
  return function proxy() {
    setTimeout(() => {
      handle()
    }, wait)
  }
}

4、参数传递
handle 处理函数中的参数以及 this,在调用 debounce 的时候也要能获取到。

js
function debounce(handle, wait, immediate) {
  // ...
  let timer = null
  return function proxy(...args) {
    const self = this 
    clearTimeout(timer)
    timer = setTimeout(() => {
      handle.call(self, ...args) 
    }, wait)
  }
}

如下图,可以获取到 this 值以及点击事件的事件对象 event

参数传递

5、执行最后一次(immediate=false)
如果想要执行最后一次,就意味着无论点了多少次,前面的 n-1 次都无用。此时只需要使用 clearTimeout()将之前每一次点击生成的定时器清除,这样就只保留了最后一次定时器,达到了只执行最后一次的目的。

js
function debounce(handle, wait, immediate) {
  // ...
  let timer = null
  return function proxy(...args) {
    const self = this
    clearTimeout(timer)
    timer = setTimeout(() => {
      handle.call(self, ...args)
    }, wait)
  }
}

可以看到,当最后一次点击停止时,才执行了点击事件绑定的处理函数。

执行最后一次

试一试

Count is:

6、执行第一次(immediate=true)

如果要执行第一次的话,就需要在 proxy 中立即执行 handle,而且 setTimeout 中不能执行。

js
function debounce(handle, wait, immediate) {
  // ...
  let timer = null
  return function proxy(...args) {
    const self = this
    const init = immediate
    clearTimeout(timer)
    timer = setTimeout(() => {
      !init ? handle.call(self, ...args) : null
    }, wait)

    init ? handle.call(self, ...args) : null
  }
}

以上代码并没有实现防抖功能,因为每次执行 proxy,init 都是 true,虽然 setTimeout 中不会再调用 handle 了,但是 line 12 处的代码每次都会执行。
分析:timer 变量只在第一次是 null,连续的第二次调用 proxy 时,setTimeout 中的代码虽然没有执行,但是 timer 已经不是 null 了。

timer

利用这一点,可以通过 timer 是否为 null 来判断是否是第一次执行。

js
function debounce(handle, wait, immediate) {
  // ...
  let timer = null
  return function proxy(...args) {
    const self = this
    const init = immediate 
    const init = immediate && !timer 
    clearTimeout(timer)
    timer = setTimeout(() => {
      timer = null 
      !init ? handle.call(self, ...args) : null
    }, wait)

    init ? handle.call(self, ...args) : null
  }
}

如上修改之后,执行结果如下图:

执行两次

可以看到,第一次点击按钮,立即就执行了 handle。虽然防抖功能实现了。但是有个 bug:在最后一次点击停止后,会再次执行一次 handle。分析了一下原因,是因为 line 11 setTimeout 中的代码还是用的 init 来判断,而最后一次点击结束,timer!=null,导致 init=false,因此在 setTimeout 中又执行了一次 handle。只需要通过 immediate 判断即可:

js
function debounce(handle, wait, immediate) {
  // ...
  return function proxy(...args) {
    // ...
    timer = setTimeout(() => {
      // ...
      !init ? handle.call(self, ...args) : null 
      !immediate ? handle.call(self, ...args) : null 
    }, wait)

    // ...
  }
}

这样便实现了执行第一次的防抖函数。

debounce

试一试

Count is:

7、完整代码

点击查看
js
/**
 * handle 需要执行的事件监听
 * wait 事件触发后多久开始执行
 * immediate 控制执行第一次还是最后一次,false:最后一次,true:第一次
 */
function debounce(handle, wait, immediate) {
  if (typeof handle !== 'function') {
    throw new Error('handle must be a function')
  }
  if (typeof wait === 'boolean') {
    immediate = wait
    wait = 300
  }
  if (typeof wait !== 'number') {
    wait = 300
  }
  if (typeof immediate !== 'boolean') {
    immediate = false
  }

  let timer = null
  return function proxy(...args) {
    const self = this
    const init = immediate && !timer
    clearTimeout(timer)
    timer = setTimeout(() => {
      timer = null
      !immediate ? handle.call(self, ...args) : null
    }, wait)

    init ? handle.call(self, ...args) : null
  }
}

节流函数

对于高频操作,我们可以自己来设置频率,让本来会执行很多次的事件触发,按着我们定义的频率减少触发的次数

应用场景

前置场景

页面有一个很高的高度。

css
body {
  height: 5000px;
}

页面滚动时,执行方法。

js
function scroll() {
  console.log('滚动了')
}

window.onscroll = scroll

没有使用节流函数,在页面滚动的过程中 scroll 函数就会高频触发。

scroll

实现节流函数

1、思路

思路

如图,我们需要定义变量 prev 记录上一次执行的时间点。

js
let prev = 0 // 记录上一次执行的时间点

还需要记录当前次执行的时间点,以及计算出时间差。

js
const now = new Date() // 记录当前次执行的时间点
const interval = wait - (now - prev)

然后根据时间差进行判断。并重新设置 prev,以及开启定时器。

js
if (interval <= 0) {
  handle.call(self, ...args)
  prev = new Date()
} else {
  // 此时说明这次操作发生在定义的时间范围内,不应该执行handle
  // 这个时候需要定义一个定时器,让handle在interval之后再执行
  setTimeout(() => {
    handle.call(self, ...args)
    prev = new Date()
  }, interval)
}

2、考虑一种场景。

特殊场景

当发现当前系统中有一个定时器了,就意味着不需要再开启定时器了。修改代码如下:

js
let timer = null // 用来管理定时器
// ...
if (interval <= 0) {
  clearTimeout(timer)
  timer = null
  handle.call(self, ...args)
  prev = new Date()
} else if (!timer) {
  timer = setTimeout(() => {
    clearTimeout(timer) // 这个操作只是将系统中的定时器清除了,但是timer中的值还在
    timer = null // 因此需要timer=null
    handle.call(self, ...args)
    prev = new Date()
  }, interval)
}

这样就实现了节流函数。

throttle

试一试

ScrollTop with throttle is:

ScrollTop without throttle is:

3、完整代码

点击查看
js
function throttle(handle, wait) {
  if (typeof handle !== 'function') {
    throw new Error('handle must be a function')
  }
  if (typeof wait === 'undefined') {
    wait = 400
  }

  let prev = 0
  let timer = null

  return function proxy(...args) {
    const self = this
    const now = new Date()
    const interval = wait - (now - prev)

    if (interval <= 0) {
      clearTimeout(timer)
      timer = null
      handle.call(self, ...args)
      prev = new Date()
    } else if (!timer) {
      timer = setTimeout(() => {
        clearTimeout(timer)
        timer = null
        handle.call(self, ...args)
        prev = new Date()
      }, interval)
    }
  }
}