函数节流与函数防抖

Author Avatar
jiasm 3月 10, 2018
  • 在其它设备中阅读本文章

函数节流和函数防抖是一个老生常谈的话题了,两者都是对大量频繁重复调用代码的一种优化方案
今天在某群和大家讨论时,顺便搜了一些相关博客
发现有一篇关于两者的定义竟然写反了。。。所以决定自己来写一下-.-权当加深记忆了

函数节流(throttle)

正如其命名的含义,节流。
限制函数在一定时间内调用的次数。

类似的实际生活中的场景

早晚高峰的地铁排队。
早高峰的地铁站
太多的人拥挤到站台上,大家都想搭上这班车,人挤人之间,难免会出现一些问题。
所以在很多地铁站,高峰期会设置很多层的屏障,来增加你进站的时间,从而减少站台的压力。
节流的实践

在程序中的实践

同理,代入程序中,我们可以通过限制函数调用的频率,来抑制资源的消耗。
比如我们要实现一个元素拖拽的效果,我们是可以在每次move事件中都进行重绘DOM,但是这样做,程序的开销是非常大的。
所以在这里我们可以用到函数节流的方法,来减少重绘的次数:

// 普通方案
$dragable.addEventListener('mousemove', () => {
  console.log('trigger')
})

// 函数节流的实现方案
let throttleIdentify = 0
$dragable.addEventListener('mousemove', () => {
  if (throttleIdentify) return
  throttleIdentify = setTimeout(() => throttleIdentify = 0, 500)
  console.log('trigger')
})

这样来做的话,在拖动的过程中,能保证500ms内,只会重绘一次DOM。
在同时监听了mouseover后,两者最终的效果是一致的,但是在拖动的过程中,函数节流版触发事件的次数会减少很多,遂消耗更少的资源。

通用的函数节流实现

/**
 * 函数节流的实现
 * @param  {Function} func      要实现函数节流的原函数
 * @param  {Number}   interval  节流的间隔
 * @return {Function}           添加节流功能的函数
 */
function throttle (func, interval) {
  let identify = 0
  return (...args) => {
    if (identify) return
    identify = setTimeout(() => identify = 0, interval)
    func.apply(this, args)
  }
}

类似函数节流的操作

平时开发中经常会做的ajax请求获取数据,这里可以用到类似函数节流的操作。
在我们发送一个请求到后台时,当返回的数据还没有接收到时,我们会添加一个标识,来表明当前有一个请求正在被处理,如果这时用户再触发ajax请求,则会直接跳过本次函数的执行。
同样的还有滑动加载更多数据,如果不添加类似的限制,可能会导致发送多条请求,渲染重复数据。


我曾经在某软件里遇到过-.-连续点击登录按钮数十次,结果连着给我弹了三次密码错误,随后告诉我输入密码错误超过三次,您的账号已被锁定。
黑人问号脸

函数节流的定义:限制函数在一定时间内调用的次数

函数防抖(debounce)

是函数在特定的时间内不被再调用后执行。

实际的例子

还是拿城市交通工具来说事儿。。
坐公交时,到站了,是由司机来操作车门的开合的。
当司机准备离站时,关闭车门,这是突然跑过来一人,司机只好再将车门打开,让人上来。

又或者,乘坐升降电梯时,电梯门关闭后,外边跑来一人又将电梯门按开。
这两件事儿都是因为关门这一个事件处理太快导致的,徒增一次开关门的消耗。

在程序中的实践

监听窗口大小重绘的操作。

在用户拖拽窗口时,一直在改变窗口的大小,如果我们在resize事件中进行一些操作,消耗将是巨大的。
而且大多数可能是无意义的执行,因为用户还处于拖拽的过程中。
所以我们可以用函数防抖来优化相关的处理

// 普通方案
window.addEventListener('resize', () => {
  console.log('trigger')
})

// 函数防抖方案
let debounceIdentify = 0
window.addEventListener('resize', () => {
  debounceIdentify && clearTimeout(debounceIdentify)
  debounceIdentify = setTimeout(() => {
    console.log('trigger')
  }, 300)
})

我们在resize事件中添加了一个300ms的延迟执行逻辑。
并且在每次事件触发时,都会重新计时,这样做也就可以保证,函数的执行肯定是在距离上次resize事件被触发的300ms后。
两次resize事件间隔小于300ms的都会被忽略,这样就节省了很多无意义的事件触发。

输入框的输入联想

几乎所有的搜索引擎都会对你输入的文字进行预判,并在下方推荐相关的结果。
但是这个联想意味着我们需要将当前用户所输入的文本传递到后端,并获取返回数据,展示在页面中。
如果遇到打字速度快的人,比如260字母/分钟的我,在一小段时间内,会连续发送大量的ajax请求到后端。
并且当前边的数据返回过来后,其实已经失去了展示的意义,因为用户可能从you输入到了young,这两个单词相关的结果肯定是不一样的。
所以我们就可以在监听用户输入的事件那里做函数防抖的处理,在XXX秒后发送联想搜索的ajax请求。

通用的函数防抖实现

/**
 * 函数防抖的实现
 * @param  {Function} func   要实现函数节流的原函数
 * @param  {Number}   delay  结束的延迟时间
 * @return {Function}        添加节流功能的函数
 */
function debounce (func, delay) {
  let debounceIdentify = 0
  return (...args) => {
    debounceIdentify && clearTimeout(debounceIdentify)
    debounceIdentify = setTimeout(() => {
      debounceIdentify = 0 // clear timer flag
      func.apply(this, args)
    }, delay)
  }
}

类似函数防抖的操作

在一些与用户的交互上,比如提交表单后,一般都会显示一个loading框来提示用户,他提交的表单正在处理中。
但是发送表单请求后就显示loading是一件很不友好的事情,因为请求可能在几十毫秒内就会得到响应。
这样在用户看来就是页面中闪过一团黑色,所以可以在提交表单后添加一个延迟函数,在XXX秒后再显示loading框。
这样在快速响应的场景下,用户是不会看到一闪而过的loading框,当然,一定要记得在接收到数据后去clearTimeout

let identify = setTimeout(showLoadingModal, 500)
fetch('XXX').then(res => {
  // doing something

  // clear timer
  clearTimeout(identify)
})

函数防抖的定义:函数在特定的时间内不被再调用后执行

总结

函数节流、函数防抖
两者都是用来解决代码短时间内大量重复调用的方案。
当然,也是各有利弊。
在实际开发中,两者的界定也很模糊。
比如搜索关键字联想,用节流或者防抖都可以来做,拖拽DOM、监听resize等等,这两个都是可以来实现的。

两者的区别在于:
函数节流在一定时间内肯定会触发多次,但是最后不一定会触发
函数防抖可能仅在最后触发一次

记住上边这两句,我觉得遇到类似需要进行优化的场景,应该就能够知道该用哪个了。

参考资料

  1. Javascript debounce vs throttle function
  2. Javascript function debounce and throttle