关于函数节流和函数去抖的实现

前言

周末在研究关于函数节流和函数去抖这两大性能优化必备的工具,开始觉得它们之间很相似,但仔细去研究,发现它们是有很大的区别的,下面我就来总结下什么是函数节流,什么是函数去抖。

先谈谈函数去抖

函数去抖能解决什么?

如果有一个事件,会连续得触发你指定的回调函数,非常密集地触发,比如window.onscrollwindow.onresize这类事件,如果你的回调函数非常重,有很多逻辑,还有对dom的处理,这是非常影响性能而造成卡顿的,我在做项目时也曾经遇到过。那么,函数去抖就可以登场了,所谓去抖,就是你指定的回调函数,在被监听密集地触发时,它可以不断地去阻止回调函数触发,直到监听事件停止时,也就是在window.onscroll情况下,不再滚动了,然后你指定的回调函数会在定好的延迟时间后触发。

比如, 让window.onscroll事件来监听触发一个简单的回调:

1
2
3
4
5
6
function print() {
console.log('hello');
}
window.onscroll = function() {
print();
}

此时,你在滚动浏览器窗口时,’hello’会不停地在打印,直到你停止滚动。
一个console.log()还好,如果是很密集的,很复杂的print回调的话,那很可能会卡崩浏览器,然后gg=。=,这种情况肯定要阻止啊~~

然后函数去抖就来了~~

简单的函数去抖是这样的

1
2
3
4
5
6
7
8
function debounce(method, context) {
// 清除上次的定时器
clearTimeout(method.tId);
method.tId = setTimeout(function() {
method.call(context);
},1000)
}
// 通过不断去清除上次事件回调产生的定时器和生成定时器,直至停止滚动事件时,最后一次生成的定时器就触发了指定的回调

复杂的函数去抖实现方法

源码来自underscore
,源码解读大部分参考了这里,也加上了自己的理解。在文中,我学到了很多,作者写得也非常用心,很棒。

附上代码

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
/*
* 函数去抖(连续事件触发结束后只触发一次)
* 整体思路 (我的个人理解)
* 用timeout去控制函数的执行过程
* 先去判断是否为立即执行,是的话直接执行,在later函数中会阻止再次执行
* 用前后滚动事件触发时的时间戳差,去判断是否要执行func,如果时间戳的差等于wait或大于wait,如果是,就执行函数
*/
var _debounce = function(func, wait, immediate) {
var timeout, args, context, timestamp, result;
var later = function () {
// 定时器设置回调later方法触发的时间,和连续事件触发的最后一次时间戳的间隔
// 如果间隔为wait,或者大于wait,则触发事件
var last = new Date().getTime() - timestamp;
console.log(last);
// 时间间隔last在[0,wait)中
// 还没到触发的点,继续设置定时器
if(last < wait && last >= 0) {
timeout = setTimeout(later, wait - last);// wait - last可以少执行很多次
}else {
// 到了触发的时间点
// 重置timeout
timeout = null;
// 判断是否立即触发
if(!immediate) {
// 非立即触发
// 执行回调函数
result = func.apply(context, args);
if(!timeout) {
context = args = null;
}
}
}
}
// 闭包返回函数
return function () {
context = this;
args = arguments;
// 每次触发函数,更新时间戳
timestamp = new Date().getTime();
// timeout判断很重要,它是判断是否首次触发的重要字段
var callNow = immediate && !timeout;
// 首次timeout为肯定为null
if(!timeout) {
// 此分支只执行一次
timeout = setTimeout(later, wait);
}
// 立即触发
if(callNow) {
result = func.apply(context, args);
// 解除引用
context = args = null;
}
return result;
}
}
function print() {
console.log('hello world');
}
window.onscroll = _debounce(print,1000);

我的个人理解已经写在函数顶部注释上了

那么现在,你搞懂函数去抖了么?

关于函数节流

我觉得,函数节流比函数去抖要难一些,不过也不难理解,慢慢来是可以的。函数节流和函数去抖的区别是,函数节流是事件监听中,有一个回调被密集地触发时,函数节流就是可以把这个回调变为在固定的时间段间断地执行,也就是不希望回调执行得太频繁,而是希望减少回调的调用频率,在指定的时间段调用。听起来是有点绕,来看看demo

同样,在滚动事件中

1
2
3
4
5
6
function print() {
console.log('hello');
}
window.onscroll = function() {
print();
}

print函数会不断地被触发,函数节流可以做到让这个回调在你指定的时间段内分批触发,而不是密集地去触发。

同样,我参考了underscore源码,和源码解读,也加上了自己的一些理解

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
/* 函数节流
* 函数节流是指,使得连续的函数执行,变为固定时间段间断地执行
* options.leading === false, 首次不触发回调,就是事件发生的那瞬间,如滚动事件
* options.trailing === false, 事件结束后,是否立即结束(还在等待中的回调)
* 整体思路:
* 1.用前后2次滚动事件触发的时间差remain, 来判断是否 <= wait,来触发回调函数
* 当 trailing === false 是通过这种方法
* 2.用 timeout = setTimout 来判断执行当滚动事件停止后,触发回调函数
* 当 leading === false 是通过这种方法
*/
var _throttle = function (func, wait, options) {
var context, args, result;
// setTimeout 的 handler
var timeout = null;
// 上一次执行回调的时间戳
var previous = 0;
// 如果没有传入 options 参数
// 则将 options 参数设置为空对象
if( !options )
options = {};
var later = function() {
// 如果 options.leading === false
// 则每次触发回调后将 previous 设置为0
// 否则设置为当前时间戳
// console.log('到了没')
previous = options.leading === false ? 0 : Date.now();
timeout = null;
result = func.apply(context, args);
if(!timeout)
context = args = null;
}
return function() {
// 记录当前时间戳
var now = Date.now();
// 第一次执行回调 ( 此时 previous 为 0, 之后 previous 的值为上一次时间戳)
// 并且如果程序设定第一个回调不是立即执行的 ( options.leading === false )
// 则将 previous 值,设为 now 的时间戳 ( 第一次触发 )
if(!previous && options.leading === false) {
previous = now;
}
// 距离下次触发 func 还需要等待的时间
// now 和 previous 的差值如果 大于或者等于wait,则触发
var remaining = wait - (now - previous);
// console.log(remaining);
context = this;
args = arguments;
// 如果间隔时间到了(remaining <= 0),即触发方法
// 如果没有传入{leading:false}, 且第一次触发回调,立即触发
// 此时 previous 为0, wait - (now - previous) 也满足 <= 0
// {trailing: false}情况下 通过计算前后时间间隔的值来判断是否执行回调
if(remaining <= 0 || remaining > wait) {
if(timeout) {
clearTimeout(timeout);
timeout = null;
}
// 修改为当前的时间
previous = now;
// 触发方法
// 包含首次触发
// console.log('执行了啊')
result = func.apply(context, args);
if(!timeout)
context = args = null;
} else if (!timeout && options.trailing !== false) {
// 最后一次需要触发的情况
// 如果存在一个定时器,则不会进入该if分支
// console.log('有没有执行?')
timeout = setTimeout(later, remaining);
}
// 回调返回值
return result;
}
}
function print() {
console.log('hey')
}
window.onscroll = _throttle(print,1000)

underscore增加了几个功能,如是否立即执行,和是否阻止(滚动)事件结束后的回调,分别用{leading:false}和{trailing:false}来判断

函数节流和函数去抖的运用场景

此处参考了这里

函数节流

  • keyup搜索联想
  • 监听滚动事件是否到了底部,自动加载更多
  • onmousemove, 拖拽

等等…

函数去抖

  • 提交表单,多次点确定提交时不用发送多次
  • 监听resize的统计函数

总结

多看源码,多思考,多敲。

文章作者: Rao Jinwei
文章链接: http://shooterblog.site/2017/12/10/关于函数节流和函数去抖的实现/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明来自 Rao Jinwei's Blog