函数防抖与节流

函数防抖

函数防抖就是让某个函数在上一次执行后,满足等待某个时间内不再触发此函数后再执行,而在这个等待时间内再次触发此函数,等待时间会重新计算。

当用户在进行一系列的操作的时候例如输入一串字符,这时如果需求中需要我们去按照用户的输入做请求,比如联想输入时,有可能就会导致多次函数的调用,而事实上我们只需要最后的那个值去发请求,函数防抖就可以解决这个问题。

undersource.js

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
26
27
28
29
30
31
_.debounce = function(func, wait, immediate) {
var timeout, args, context, timestamp, result

var later = function() {
var last = _.now() - timestamp //_.now(),一个优化的方式来获得一个当前时间的整数时间戳

if (last < wait && last >= 0) {
timeout = setTimeout(later, wait - last)
} else {
timeout = null
if (!immediate) {
result = func.apply(context, args)
if (!timeout) context = args = null
}
}
}

return function() {
context = this
args = arguments
timestamp = _.now()
var callNow = immediate && !timeout
if (!timeout) timeout = setTimeout(later, wait)
if (callNow) {
result = func.apply(context, args)
context = args = null
}

return result
}
}

源码中 debounce 函数中的 later 是 setTimeout 中的回调函数,last 是调用后过了多少时间,如果 last 比 await 小就递归调用 later,等待时间重新计算。
如果设置了 immediate 就会立即触发一次函数,之后再计算时间。

另一种实现

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 这个是用来获取当前时间戳的
function now() {
return +new Date()
}
/**
* 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行
*
* @param {function} func 回调函数
* @param {number} wait 表示时间窗口的间隔
* @param {boolean} immediate 设置为ture时,是否立即调用函数
* @return {function} 返回客户调用函数
*/
function debounce(func, wait = 50, immediate = true) {
let timer, context, args

// 延迟执行函数
const later = () =>
setTimeout(() => {
// 延迟函数执行完毕,清空缓存的定时器序号
timer = null
// 延迟执行的情况下,函数会在延迟函数中执行
// 使用到之前缓存的参数和上下文
if (!immediate) {
func.apply(context, args)
context = args = null
}
}, wait)

// 这里返回的函数是每次实际调用的函数
return function(...params) {
// 如果没有创建延迟执行函数(later),就创建一个
if (!timer) {
timer = later()
// 如果是立即执行,调用函数
// 否则缓存参数和调用上下文
if (immediate) {
func.apply(this, params)
} else {
context = this
args = params
}
// 如果已有延迟执行函数(later),调用的时候清除原来的并重新设定一个
// 这样做延迟函数会重新计时
} else {
clearTimeout(timer)
timer = later()
}
}
}

函数节流

函数节流就是让某个函数每间隔一段时间执行防止多次调用

简单版本

1
2
3
4
5
6
function throttle(method, context,wait=100) {
clearTimeout(methor.tId);
method.tId = setTimeout(function(){
method.call(context);
}, wait);
}
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
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
/**
* underscore 节流函数,返回函数连续调用时,func 执行频率限定为 次 / wait
*
* @param {function} func 回调函数
* @param {number} wait 表示时间窗口的间隔
* @param {object} options 如果想忽略开始函数的的调用,传入{leading: false}。
* 如果想忽略结尾函数的调用,传入{trailing: false}
* 两者不能共存,否则函数不能执行
* @return {function} 返回客户调用函数
*/
_.throttle = function(func, wait, options) {
var context, args, result
var timeout = null
// 之前的时间戳
var previous = 0
// 如果 options 没传则设为空对象
if (!options) options = {}
// 定时器回调函数
var later = function() {
// 如果设置了 leading,就将 previous 设为 0
// 用于下面函数的第一个 if 判断
previous = options.leading === false ? 0 : _.now()
// 置空一是为了防止内存泄漏,二是为了下面的定时器判断
timeout = null
result = func.apply(context, args)
if (!timeout) context = args = null
}
return function() {
// 获得当前时间戳
var now = _.now()
// 首次进入前者肯定为 true
// 如果需要第一次不执行函数
// 就将上次时间戳设为当前的
// 这样在接下来计算 remaining 的值时会大于0
if (!previous && options.leading === false) previous = now
// 计算剩余时间
var remaining = wait - (now - previous)
context = this
args = arguments
// 如果当前调用已经大于上次调用时间 + wait
// 或者用户手动调了时间
// 如果设置了 trailing,只会进入这个条件
// 如果没有设置 leading,那么第一次会进入这个条件
// 还有一点,你可能会觉得开启了定时器那么应该不会进入这个 if 条件了
// 其实还是会进入的,因为定时器的延时
// 并不是准确的时间,很可能你设置了2秒
// 但是他需要2.2秒才触发,这时候就会进入这个条件
if (remaining <= 0 || remaining > wait) {
// 如果存在定时器就清理掉否则会调用二次回调
if (timeout) {
clearTimeout(timeout)
timeout = null
}
previous = now
result = func.apply(context, args)
if (!timeout) context = args = null
} else if (!timeout && options.trailing !== false) {
// 判断是否设置了定时器和 trailing
// 没有的话就开启一个定时器
// 并且不能不能同时设置 leading 和 trailing
timeout = setTimeout(later, remaining)
}
return result
}
}