概念

函数节流 throttle

如果将水龙头拧紧直到水是以水滴的形式流出,那你会发现每隔一段时间,就会有一滴水流出.也就是会说预先设定一个执行周期,当调用动作的时刻大于等于执行周期则执行该动作,然后进入下一个新周期

函数去抖 debounce

滚动条不停的拖动, 只有停下的时候再去执行事件响应, 而不是每次触发onscroll都去执行它.也就是说当调用动作n毫秒后,才会执行该动作,若在这n毫秒内又调用此动作则将重新计算执行时间

JavaScript实现

民用级别实现方式

简洁明了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var debounce = function(idle, action){
var last;
return function(){
var ctx = this, args = arguments;
clearTimeout(last); //如果在timeout内调用, 则清除timeout重新计时
last = setTimeout(function(){
action.apply(ctx, args);
}, idle);
}
}

var throttle = function(delay, action){
var last;
return function(){
var curr = +new Date(); // '+' 转换为Number类型
if (curr - last > delay){ // 超过间隔执行并重新设置上次执行时间
action.apply(this, arguments);
last = curr;
}
}
}

军用级别实现方式[underscore1.8.3版本源码]

实现原理与民用级实现相同, 增加了trailing edge模式, 使用场景更多, 逻辑更加严密

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
_.debounce = function(func, wait, immediate) {
var timeout, result;
var later = function(context, args) {
timeout = null;
if (args) result = func.apply(context, args); //传参数时才调用(即immediate为true的首次调用时, 只把timeout清除而不调用)
};

var debounced = restArgs(function(args) { //restArgs将数组参数, 转换为不定参数的形式
if (timeout) clearTimeout(timeout); //先清除timeout
if (immediate) { //如果是leading edge[第一次调用时直接执行]
var callNow = !timeout; //timeout为假值时是未调用过的状态, callNow设为true
timeout = setTimeout(later, wait);
if (callNow) result = func.apply(this, args); //直接调用
} else { //trailing edge, 第一次调用设置timeout
timeout = _.delay(later, wait, this, args);
}
return result;
});

debounced.cancel = function()
clearTimeout(timeout); //清除timeout待执行函数
timeout = null; //清除timeout句柄
};

return debounced;
};

_.throttle = function(func, wait, options) {
var timeout, context, args, result;
var previous = 0;
if (!options) options = {};
var later = function() {
previous = options.leading === false ? 0 : _.now(); //leading edge设置上次调用时间为1970年
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};

var throttled = function() {
var now = _.now();
if (!previous && options.leading === false) previous = now; //trailing edge模式,调用时计时,remaining始终=wait
var remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0 || remaining > wait) { //remaining > wait是指系统时间被修改到过去, 也会执行
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(context, args); //leading edge直接调用
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) { //如果有计时器进行中, 不执行
timeout = setTimeout(later, remaining); //trailing edge调用时, 计算出距离下次可调用的时间间隔并设置定时
}
return result;
};
throttled.cancel = function() {
clearTimeout(timeout);
previous = 0;
timeout = context = args = null;
};
return throttled;
};

骨灰级别实现方式[lodash4.14.2源码]

此种实现方式的debounce兼具throttle的功能, 封装合理,较之于lodash3.10.*的代码, 更加容易阅读.

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
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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
function debounce(func, wait, options) {
var lastArgs,
lastThis,
maxWait,
result,
timerId,
lastCallTime,
lastInvokeTime = 0,
leading = false,
maxing = false,
trailing = true;

if (typeof func != 'function') {
throw new TypeError(FUNC_ERROR_TEXT);
}
wait = toNumber(wait) || 0;
if (isObject(options)) {
leading = !!options.leading;
maxing = 'maxWait' in options;
maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait;
trailing = 'trailing' in options ? !!options.trailing : trailing;
}

function invokeFunc(time) {
var args = lastArgs,
thisArg = lastThis;

lastArgs = lastThis = undefined;
lastInvokeTime = time;
result = func.apply(thisArg, args);
return result;
}

function leadingEdge(time) {
// Reset any `maxWait` timer.
lastInvokeTime = time;
// Start the timer for the trailing edge.
timerId = setTimeout(timerExpired, wait);
// Invoke the leading edge.
return leading ? invokeFunc(time) : result;
}

function remainingWait(time) {
var timeSinceLastCall = time - lastCallTime,
timeSinceLastInvoke = time - lastInvokeTime,
result = wait - timeSinceLastCall;

return maxing ? nativeMin(result, maxWait - timeSinceLastInvoke) : result;
}

function shouldInvoke(time) {
var timeSinceLastCall = time - lastCallTime,
timeSinceLastInvoke = time - lastInvokeTime;

// Either this is the first call, activity has stopped and we're at the
// trailing edge, the system time has gone backwards and we're treating
// it as the trailing edge, or we've hit the `maxWait` limit.
return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
(timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait));
}

function timerExpired() {
var time = now();
if (shouldInvoke(time)) {
return trailingEdge(time);
}
// Restart the timer.
timerId = setTimeout(timerExpired, remainingWait(time));
}

function trailingEdge(time) {
timerId = undefined;

// Only invoke if we have `lastArgs` which means `func` has been
// debounced at least once.
if (trailing && lastArgs) {
return invokeFunc(time);
}
lastArgs = lastThis = undefined;
return result;
}

function cancel() {
if (timerId !== undefined) {
clearTimeout(timerId);
}
lastInvokeTime = 0;
lastArgs = lastCallTime = lastThis = timerId = undefined;
}

function flush() {
return timerId === undefined ? result : trailingEdge(now());
}

function debounced() {
var time = now(),
isInvoking = shouldInvoke(time);

lastArgs = arguments;
lastThis = this;
lastCallTime = time;

if (isInvoking) {
if (timerId === undefined) {
return leadingEdge(lastCallTime);
}
if (maxing) {
// Handle invocations in a tight loop.
timerId = setTimeout(timerExpired, wait);
return invokeFunc(lastCallTime);
}
}
if (timerId === undefined) {
timerId = setTimeout(timerExpired, wait);
}
return result;
}
debounced.cancel = cancel;
debounced.flush = flush;
return debounced;
}

function throttle(func, wait, options) {
var leading = true,
trailing = true;

if (typeof func != 'function') {
throw new TypeError(FUNC_ERROR_TEXT);
}
if (isObject(options)) {
leading = 'leading' in options ? !!options.leading : leading;
trailing = 'trailing' in options ? !!options.trailing : trailing;
}
return debounce(func, wait, {
'leading': leading,
'maxWait': wait,
'trailing': trailing
});
}

应用

  • 在Web前端 resize, sroll, mousemove, mousedrag等事件触发时的频率很高, 如果注册的事件中有耗时的DOM操作, 或ajax等, 往往需要去抖或去抖来提升性能, 减少不必要的调用.

  • 比如点击按钮时向后台发送ajax请求, 在一定时间内重复点击不应该再次发送, 这可以用节流实现

  • 还有页面滚动触发的业务逻辑操作, 应当在滚动停下时再去执行, 这可以用去抖实现. 而是否延迟响应可以通过配置leading edgetrailing edge实现.