节流和防抖

1 句话概括:

  • 节流: 持续不断的动作隔一段时间执行 1 次。
  • 防抖: 持续不断的动作在停止动作一段时间后执行最后的 1 次动作。

场景模拟:

节流:
未节流   |   节流

  • 也可以把动作比作自动浇水的龙头,如果一天 24 小时都打开阀门浇水,这对它是不好的,为了花的更好生长,添加一个控制,间隔 12 小时共早晚浇两次,花朵能长得越来越好。
  • 在监听页面滚动时,这个滚动的事件是不断在触发的,假设 1s 这个浏览器向我们报告 10 次,那么从滚动开始到停止的 n 秒内,我们对这个滚动事件就需要处理 10 * n 次,再加上对滚动报告时的处理,这无疑是非常大的开销。

防抖:
未防抖   |   防抖

  • 囧豆上了年纪了,手总是不自觉的抖,女儿贴心地为他设置了一个一键拨打电话给自己的按钮,按下就可以直接打给自己。有一天囧豆拿起手机,按下按钮,但是因为手抖,其实按了 3 下,所以打出了 3 个电话(现实生活中不会打出 3 个电话的,这里是强行举例)。对于囧豆的 3 次按下按钮的动作,其实只需要 1 次生效即可,这是防抖对于此类场景的作用。
  • 闪速是一个做什么超级迅速的男孩,今天他打开百度搜索页面,以光速输入了“为什么我的速度这么快”想要探知究竟。正当他输入完他的疑问,百度搜索框下方的智能提示缓缓地显示出“为什么”->“为什么会打呼噜”->“为什么英文怎么写”->“为什么我的眼里常含泪水,因为我对这片土地爱的深沉”->“为什么会打呼噜”->“为什么我的眼里常含泪水全诗”->“为什么我的眼里常含泪水全诗”->“为什么我的速腾螺丝好像都被拧过”…看着以龟速慢慢变化的智能提示,他明白了百度能解答他的疑惑。闪速输入的每一个字,百度都在预想他想问的问题,但是闪速是在是太快了,以至于他输入完了问题,百度还没有反应过来。当百度给问题输入加上了防抖,0.5s 内预想一次,这时闪速再次输入问题搜索,直接打开了他的问题搜索结果,闪速满意地笑了。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 节流
function throttle(func, limit) {
let lastFunc
let lastRan = 0
return function () {
const context = this
const args = arguments
const remain = limit - (Date.now() - lastRan)
if (remain <= 0) {
if (lastFunc) {
clearTimeout(lastFunc)
lastFunc = null
}
func.apply(context, args)
lastRan = Date.now()
} else if (!lastFunc) {
lastFunc = setTimeout(function () {
if (Date.now() - lastRan >= limit) {
func.apply(context, args)
lastRan = Date.now()
}
}, remain)
}
}
}

过程分析:(假设 limit = 1000ms, 初始的 Date.now() = 0, 事件每隔 100ms 触发一次)

  1. 定义变量 定时器任务lastFunc、上次运行时间lastRan,此时Date.now() = 0remain = limit - 当前时间戳(0) 即 remain = 1000 - 0 = 0,此时 remain < 0,执行一次回调,并且标记 lastRan = 当前时间(0)
  2. 事件继续触发中,现在过去了 100ms, 那么此时的 Date.now() = 100remain = 1000 - (100 - 0) = 900,意为距离下一次执行回调还需 900ms,这时进入 else if 分支,lastFunc 定时器此时没有任务,所以继续执行,设定 lastFunc 定时器为 900ms 后如果距离上一次执行回调的时间大于 limit,则执行回调并且标记 lastRan
  3. 事件继续触发中,现在过去了 200ms, 那么此时的 Date.now() = 200remain = 1000 - (200 - 0) = 900,意为距离下一次执行回调还需 800ms,这时进入 else if 分支,lastFunc 定时器此时已经有了任务,任务执行时间(100 + 900)和现在 200 + 800 = 1000 相同,无需重复设置定时任务,本次事件不做处理
  4. 在接下来的 300ms - 1000ms 内,都在重复 3 的步骤
  5. 继续触发,此时的 Date.now() = 1000remain = 1000 - (1000 - 0) = 0,进入 remain <= 0 分支,这时检查是否有定时器任务,将它清除(因为这时需要马上执行回调,不需要等待),执行回调并且标记 lastRan = 1000
  6. 继续触发,此时的 Date.now() = 1100remain = 1000 - (1100 - 1000) = 900,又回到了 2 步骤的处理过程,如果此时事件停止触发,那么定时器任务将会在 900ms 后执行最后一次回调
1
2
3
4
5
6
7
8
9
10
11
12
// 防抖
function debounce(fn, interval = 1000) {
let timer
return function () {
clearTimeout(timer)
const context = this
const args = arguments[0]
timer = setTimeout(function () {
fn.call(context, args)
}, interval)
}
}

过程分析:用户开始输入,噼里啪啦在触发中 input 事件中,每一次 input 会重置之前的定时器任务,一直到停止输入的一段时间(这段时间>=interval)时,此时定时器还有任务没有被清除,执行回调,over。

完结,撒花。