节流和防抖
# 节流和防抖
# 为什么需要节流和防抖
在日常我们写代码的过程中,会出现很多的高频事件,比如浏览器页面的滚动事件 onscroll
,浏览器窗口的缩放事件 resize
,这些事件在我们触发时,会执行回调函数很多次,有可能 1s执行了好几十次,但是对于功能需求而言,我们可能只需要最后一次的执行结果,或者希望它的执行有一个间隔时间,比如 1s 执行一次,那么这个时候就需要用到 节流和防抖 来对回调函数的执行加一个限制
# 什么是节流和防抖,它的作用是什么
节流(throttle)
节流就是保证一段时间内,核心代码只会执行一次。
简单的节流函数
function throttle (func, wait) {
let last = 0
return function () {
const now = + new Date
if (now - last > wait) {
func.apply(this, arguments)
last = now
}
}
}
2
3
4
5
6
7
8
9
10
防抖(debounce)
在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则重新计时
简单的防抖函数
function debounce(func, wait) {
let timer
return function () {
clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, arguments)
}, wait)
}
}
2
3
4
5
6
7
8
9
简单的 节流和防抖 就这样就可以实现了,接下来我们来看看比较出名的两个库,看看他们对于 节流和防抖 是怎么处理的
# underscore.js
# throttle
首先在定义函数时,和简易版的比起来,多了 options
配置,可以做一些额外的操作,比如 trailing
表示是否在在延时结束后调用 func
function throttle(func, wait, options) {}
定义了返回 函数 throttled
以及使用到的变量,实现简单的节流
function throttle(func, wait, options) {
let args, context, last = 0
const throttled = () => {
args = arguments
context = this
const now = +new Date
const remaning = wait - (now - last)
if (remaning <= 0 || remaning > wait) { // 第一次会触发 或者 wait 时间到了会触发
func.apply(context, args)
last = now
}
}
return throttled
}
2
3
4
5
6
7
8
9
10
11
12
13
14
这个时候 options
还是没有用到, 接下来要处理 options
的内容
function throttle(func, wait, options = {}) {
// trailing 最后一次默认是触发的
let args, context, last = 0, timer // 添加一个定时器 用来计算最后一次
const later = () => { // 最后一次执行的方法
timer = null
func.apply(context, args)
last = +new Date
}
const throttled = () => {
args = arguments
context = this
const now = +new Date
const remaning = wait - (now - last)
if (remaning <= 0 || remaning > wait) {
if (timer) {
clearTimeout(timer)
timer = null
}
func.apply(context, args)
last = now
return
} else if (!timer && options.trailing !== false) {
timer = setTimeout(later, remaning) // 定时器为最后剩余的时间,也就是 wait 减去 已经度过的时间
}
}
return throttled
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
处理 leading
, 判断一开始是否执行,完善一下逻辑就实现了一个节流函数
function throttle(func, wait, options = {}) {
// trailing 最后一次默认是触发的
let args, context, last = 0, timer
const later = () => {
last = options.leading === false ? 0 : +new Date
func.apply(context, args)
args = context = null
}
const throttled = () => {
args = arguments
context = this
const now = +new Date
if (!last && options.leading === false) last = now // 如果 leading 为 false , 那么此时将 last 置为 now ,那么第一次就不会触发 事件执行
const remaning = wait - (now - last)
if (remaning <= 0) {
if (timer) {
clearTimeout(timer)
timer = null
}
func.apply(context, args)
last = now
} else if (!timer && options.trailing !== false) { // last 置为 now 就会走到这里
timer = setTimeout(later, remaning)
}
}
return throttled
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# debounce
underscore
在简单版本的基础上,新增加了一些 options
配置,核心逻辑就是这些东西,在源码中还添加了 cancel
等方法,这里就不赘述了,实现核心逻辑就可以了
function debounce(func, wait, immediate) {
// immediate 第一次是否触发事件
let timer
return function () {
clearTimeout(timer)
if (immediate && !timer) func.apply(this, arguments)
timer = setTimeout(() => {
func.apply(this, arguments)
timer = null
}, wait)
}
}
2
3
4
5
6
7
8
9
10
11
12
# lodash
lodash 对于 节流和防抖的处理是放在同一个函数中的,也就是 debounce
,throttle
是引用的 debounce
实现的,之前在分析 lodash 源码时,对于 debounce 的源码做了一个分析 (opens new window) ,那么接下来我们来看看怎么一步一步实现
# 首先,lodash 中 debounce 的流程是怎样的
# 接着根据流程图,看看怎么实现这么一个函数
首先创建一个 简单的 防抖函数
function debounce(func, wait) {
let timer
return function () {
clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, arguments)
}, wait)
}
}
2
3
4
5
6
7
8
9
对于当前函数,并没有做参数合法化的校验,我们添加这一段逻辑,对于 func
我们希望它是一个函数,如果不是则抛出类型错误,对于 wait
希望它是一个数字,使用 一元正号进行转换,有可能为 NaN
, 如果为
NaN 则取 0
function debounce(func, wait) {
if (typeof func !== 'function') {
throw new TypeError('Expected a function')
}
wait = +wait || 0
let timer
return function () {
clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, arguments)
}, wait)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
对于 防抖和节流,我们都希望它第一次和最后一次默认都触发,那么可以定义两个标识
function debounce(func, wait) {
if (typeof func !== 'function') {
throw new TypeError('Expected a function')
}
wait = +wait || 0
let timer
let leading = true // 第一次进入时触发
let trailing = true // 最后一次也要触发
function debounced () {
clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, arguments)
}, wait)
}
return debounced
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
现在我们使用的是 setTimeout
来实现防抖,现代浏览器有一个 requestAnimationFrame
的 api
,会在浏览器重绘之前调用,性能比 setTimeout
更好
function debounce(func, wait) {
if (typeof func !== 'function') {
throw new TypeError('Expected a function')
}
// 只有在 wait 假值,但是不是 0, 并且浏览器支持的情况下 使用 requestAnimationFrame
const useRAF = (!wait && wait !== 0 && typeof window.requestAnimationFrame === 'function')
wait = +wait || 0
let timer
let leading = true // 第一次进入时触发
let trailing = true // 最后一次也要触发
function debounced () {
if (useRAF) {
window.cancelAnimationFrame(timer)
} else {
clearTimeout(timer)
}
if (useRAF) {
window.cancelAnimationFrame(timerId)
timer = window.requestAnimationFrame(() => {
func.apply(this, arguments)
})
} else {
timerId = setTimeout(() => {
func.apply(this, arguments)
}, wait)
}
}
return debounced
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
可以看到 此时 debounced
的处理逻辑 就已经有点复杂了,中间的 if 判断 比较多,那么可以考虑抽离成两个单独的函数来处理 定时器的开启 (startTimer
) 和 清除(clearTimer
)
提取方法后,针对 this
和 arguments
就需要缓存下来
function debounce(func, wait) {
if (typeof func !== 'function') {
throw new TypeError('Expected a function')
}
// 只有在 wait 假值,但是不是 0, 并且浏览器支持的情况下 使用 requestAnimationFrame
const useRAF = (!wait && wait !== 0 && typeof window.requestAnimationFrame === 'function')
wait = +wait || 0
let timer
let leading = true // 第一次进入时触发
let trailing = true // 最后一次也要触发
let lastThis; // 返回函数的 this
let lastArgs; // 返回函数的参数
// 开启定时器
function startTimer (func, wait) {
if (useRAF) {
window.cancelAnimationFrame(timerId)
return window.requestAnimationFrame(() => {
func.apply(lastThis, lastArgs)
})
}
return setTimeout(() => {
func.apply(lastThis, lastArgs)
}, wait)
}
// 清除定时器
function clearTimer (timerId) {
if (useRAF) {
return window.cancelAnimationFrame(timerId)
}
clearTimeout(timerId)
}
function debounced (...args) {
lastThis = this
lastArgs = args
clearTimer(timer)
timer = startTimer(func, wait)
}
return debounced
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
现在我们可以发现,在 setTimeout
和 requestAnimationFrame
中,对于执行函数这里的逻辑是一致的,可以考虑提成一个单独的方法来处理(invokeFunc
)
function debounce(func, wait) {
if (typeof func !== 'function') {
throw new TypeError('Expected a function')
}
// 只有在 wait 假值,但是不是 0, 并且浏览器支持的情况下 使用 requestAnimationFrame
const useRAF = (!wait && wait !== 0 && typeof window.requestAnimationFrame === 'function')
wait = +wait || 0
let leading = true // 第一次进入时触发
let trailing = true // 最后一次也要触发
let timer,
lastThis, // 返回函数的 this
lastArgs, // 返回函数的参数
result; // 最后的返回结果
// 执行函数
function invokeFunc () {
let args = lastArgs
let thisArg = lastThis
result = func.apply(thisArg, args)
lastArgs = lastThis = undefined
return result
}
// 开启定时器
function startTimer (pendingFunc, wait) {
if (useRAF) {
window.cancelAnimationFrame(timerId)
return window.requestAnimationFrame(pendingFunc)
}
return setTimeout(pendingFunc, wait)
}
// 清除定时器
function clearTimer (timerId) {
if (useRAF) {
return window.cancelAnimationFrame(timerId)
}
clearTimeout(timerId)
}
function debounced (...args) {
lastThis = this
lastArgs = args
clearTimer(timer)
timer = startTimer(invokeFunc, wait)
}
return debounced
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
现在我们要来处理,来判断,debounced
的方法 是否需要执行,包含第一次进来的事件触发也在这个判断里
function debounce(func, wait) {
if (typeof func !== 'function') {
throw new TypeError('Expected a function')
}
// 只有在 wait 假值,但是不是 0, 并且浏览器支持的情况下 使用 requestAnimationFrame
const useRAF = (!wait && wait !== 0 && typeof window.requestAnimationFrame === 'function')
wait = +wait || 0
let leading = true // 第一次进入时触发
let trailing = true // 最后一次也要触发
let timer,
lastThis, // 返回函数的 this
lastArgs, // 返回函数的参数
result, // 最后的返回结果
lastCallTime; // 最后调用的时间
// 执行函数
function invokeFunc () {
let args = lastArgs
let thisArg = lastThis
result = func.apply(thisArg, args)
lastArgs = lastThis = undefined
return result
}
function shouldInvoke () { // 返回 布尔值
// 第一次
return lastCallTime === undefined
}
// 是否第一次执行
function leadingEdge () {
if(leading) invokeFunc() // 如果需要则执行函数
startTimer() // 开启一个定时器, 看下一次定时器是否到了,是否需要执行 func
}
// 开启定时器
function startTimer (pendingFunc, wait) {
if (useRAF) {
window.cancelAnimationFrame(timerId)
return window.requestAnimationFrame(pendingFunc)
}
return setTimeout(pendingFunc, wait)
}
// 清除定时器
function clearTimer (timerId) {
if (useRAF) {
return window.cancelAnimationFrame(timerId)
}
clearTimeout(timerId)
}
function debounced (...args) {
lastThis = this
lastArgs = args
const isInvoking = shouldInvoke()
if (isInvoking) {
if (timer === undefined) {
leadingEdge()
}
}
clearTimer(timer)
timer = startTimer(invokeFunc, wait)
}
return debounced
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
此时 ,函数并不完善,对于最后一次是否需要执行,没有做处理,也有一些其他的小问题,我们对它进行一个修复及处理
对于 debounce
来说,一开始先创建一个 定时器,只要 函数一直触发,到时间就什么都不做,再开一个定时器,到最终只会开一个定时器,保留最后一次
function debounce(func, wait) {
if (typeof func !== 'function') {
throw new TypeError('Expected a function')
}
// 只有在 wait 假值,但是不是 0, 并且浏览器支持的情况下 使用 requestAnimationFrame
const useRAF = (!wait && wait !== 0 && typeof window.requestAnimationFrame === 'function')
wait = +wait || 0
let leading = true // 第一次进入时触发
let trailing = true // 最后一次也要触发
let timer,
lastThis, // 返回函数的 this
lastArgs, // 返回函数的参数
result, // 最后的返回结果
lastCallTime; // 最后调用的时间
// 执行函数
function invokeFunc () {
let args = lastArgs
let thisArg = lastThis
result = func.apply(thisArg, args)
lastArgs = lastThis = undefined
return result
}
function shouldInvoke (time) {
// 现在 shouldInvoke 会传入 时间,那么首先需要拿到时差,也就是当前的时间减去上一次的时间
const timeSinceLastCall = time - lastCallTime
// 拿到时间差后进行判断,如果 大于了 wait ,那么 func 也应该执行
return lastCallTime === undefined || timeSinceLastCall >= wait
}
// 是否第一次执行
function leadingEdge () {
if(leading) invokeFunc() // 如果需要则执行函数
timer = startTimer() // 开启一个定时器, 看下一次定时器是否到了,是否需要执行 func
}
function trailingEdge () {
timer = undefined
if (trailing) {
invokeFunc()
}
}
// 计算差值
function remainingWait () {
return wait - (now - lastCallTime)
}
function timerExpired () { // 当定时器到期了,是否需要执行函数
const now = +new Date
if (shouldInvoke(now)) { // 如果需要调用
// 触发结束的方法
return trailingEdge()
}
// 如果不满足触发条件,那么就再开一个定时器
timer = startTimer(timerExpired, remainingWait(now))
}
// 开启定时器
function startTimer (pendingFunc, wait) {
if (useRAF) {
window.cancelAnimationFrame(timerId)
return window.requestAnimationFrame(pendingFunc)
}
return setTimeout(pendingFunc, wait)
}
// 清除定时器
function clearTimer (timerId) {
if (useRAF) {
return window.cancelAnimationFrame(timerId)
}
clearTimeout(timerId)
}
function debounced (...args) {
lastThis = this
lastArgs = args
const now = +new Date
const isInvoking = shouldInvoke(now)
lastCallTime = now
if (isInvoking) {
if (timer === undefined) {
leadingEdge()
}
}
}
return debounced
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
至此 基本的 debounce
就实现了,接下来就要实现 throttle
相关的东西
在 throttle
中,也就是说 执行到了一定时间后,就会触发函数, 那么就需要一个 maxWait
参数
function debounce(func, wait, options = {}) {
let maxWait
if ('maxWait' in options) maxWait = options.maxWait
if (typeof func !== 'function') {
throw new TypeError('Expected a function')
}
// 只有在 wait 假值,但是不是 0, 并且浏览器支持的情况下 使用 requestAnimationFrame
const useRAF = (!wait && wait !== 0 && typeof window.requestAnimationFrame === 'function')
wait = +wait || 0
let leading = true // 第一次进入时触发
let trailing = true // 最后一次也要触发
let timer,
lastThis, // 返回函数的 this
lastArgs, // 返回函数的参数
lastCallTime; // 最后调用的时间
let lastInvokeTime = 0
// 执行函数
function invokeFunc (time) {
let args = lastArgs
let thisArg = lastThis
lastInvokeTime = time
func.apply(thisArg, args)
lastArgs = lastThis = undefined
}
function shouldInvoke (time) {
const timeSinceLastCall = time - lastCallTime
// 得到当前时间和上一次调用时间的时间差
const timeSinceLastInvoke = time - lastInvokeTime
// 如果当前时差,大于了 throttle 时间,就执行
return lastCallTime === undefined || timeSinceLastCall >= wait || timeSinceLastInvoke >= maxWait
}
// 是否第一次执行
function leadingEdge (time) {
lastInvokeTime = time
if(leading) invokeFunc(time) // 如果需要则执行函数
timer = startTimer() // 开启一个定时器, 看下一次定时器是否到了,是否需要执行 func
}
function trailingEdge (time) {
timer = undefined
if (trailing) {
invokeFunc(time)
}
}
// 计算差值
function remainingWait () {
return wait - (now - lastCallTime)
}
function timerExpired () { // 当定时器到期了,是否需要执行函数
const now = +new Date
if (shouldInvoke(now)) { // 如果需要调用
// 触发结束的方法
return trailingEdge(now)
}
// 如果不满足触发条件,那么就再开一个定时器
timer = startTimer(timerExpired, remainingWait(now))
}
// 开启定时器
function startTimer (pendingFunc, wait) {
if (useRAF) {
window.cancelAnimationFrame(timerId)
return window.requestAnimationFrame(pendingFunc)
}
return setTimeout(pendingFunc, wait)
}
// 清除定时器
function clearTimer (timerId) {
if (useRAF) {
return window.cancelAnimationFrame(timerId)
}
clearTimeout(timerId)
}
function debounced (...args) {
lastThis = this
lastArgs = args
const now = +new Date
const isInvoking = shouldInvoke(now)
lastCallTime = now
if (isInvoking) {
if (timer === undefined) {
leadingEdge(now)
}
}
}
return debounced
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
// throttle
function throttle (func, wait) {
return debounce(func, wait, {
maxWait: wait
})
}
2
3
4
5
6
至此也就实现了 throttle 和 debounce 函数,在 lodash 源码中还对于参数的合法化做了严格的处理,对了 开始 和 结束是否执行也是通过参数传入的,具体的可以查看 lodash 源码 或者 查看我写的 lodash 源码分析 debounce (opens new window)