防抖debounce和节流throttle

什么是防抖、节流,分别解释一下?
在白纸上手写一个防抖or节流函数,自己任选(限时4分钟)
react hooks有了解吗?上机实现一个useDebounce、 useThrottle
tyepscript有了解吗?用ts再来写一遍

防抖(debounce)和节流(throttle)都是用来控制某个函数在一定时间内执行多少次的技巧,两者相似而又不同。

debounce

把触发非常频繁的事件(比如按键)合并成一次执行

debounce 又分为前缘debounce(leading) 和后缘debounce(trailing)

1
2
3
4
5
6
7
8
npm i -g lodash-cli
// /usr/local/bin/lodash -> /usr/local/lib/node_modules/lodash-cli/bin/lodash
lodash include=debounce,throttle

var debounced_version = _.debounce(doSomething, 200);
$(window).on('scroll', debounced_version);
// 如果需要的话
debounced_version.cancel();
1
2
3
4
5
6
7
8
9
export function debounce(func: Function, timeout: number) {
let timer: NodeJS.Timeout;
return (...args: any) => {
clearTimeout(timer);
timer = setTimeout(() => {
func(...args);
}, timeout);
};
}
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
function debounce(fn, ms) {
let timer;
return function(...args) {
if(timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn(...args);
timer = null;
}, ms)
}
}

// 测试用例1
export default function() {
const [counter, setCounter] = useState(0);
const handleClick = useDebounce(function() {
setCounter(counter + 1)
}, 1000)
return <div style={{ padding: 30 }}>
<Button
onClick={handleClick}
>click</Button>
<div>{counter}</div>
</div>
}

// 测试用例2
export default function() {
const [counter1, setCounter1] = useState(0);
const [counter2, setCounter2] = useState(0);
const handleClick = useDebounce(function() {
console.count('click1')
setCounter1(counter1 + 1)
}, 500)
useEffect(function() {
const t = setInterval(() => {
setCounter2(x => x + 1)
}, 500);
return clearInterval.bind(undefined, t)
}, [])
return <div style={{ padding: 30 }}>
<Button
onClick={function() {
handleClick()
}}
>click</Button>
<div>{counter1}</div>
<div>{counter2}</div>
</div>
}

// wrong
// 每次组件重新渲染,都会执行一遍所有的hooks,这样debounce高阶函数里面的timer就不能起到缓存的作用(每次重渲染都被置空)。timer不可靠,debounce的核心就被破坏了。
function useDebounce(fn, time) {
return debounce(fn, time);
}

// correct
function useDebounce(fn, delay, dep = []) {
const { current } = useRef({ fn, timer: null });
useEffect(function () {
current.fn = fn;
}, [fn]);

return useCallback(function f(...args) {
if (current.timer) {
clearTimeout(current.timer);
}
current.timer = setTimeout(() => {
current.fn.call(this, ...args);
}, delay);
}, dep)
}

// correct
function useThrottle(fn, delay, dep = []) {
const { current } = useRef({ fn, timer: null });
useEffect(function () {
current.fn = fn;
}, [fn]);

return useCallback(function f(...args) {
if (!current.timer) {
current.timer = setTimeout(() => {
delete current.timer;
}, delay);
current.fn.call(this, ...args);
}
}, dep);
}

throttle

只允许一个函数在x毫秒内执行一次
跟 debounce 主要的不同在于,throttle 保证 X 毫秒内至少执行一次

1
_.throttle(dosomething, 16)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function throttle(func, timeout) {
let ready: boolean = true;
return (...args) => {
if (!ready) {
return;
}

ready = false;
func(...args);
setTimeout(() => {
ready = true;
}, timeout);
};
}

requestAnimationFrame

可替代 throttle ,函数需要重新计算和渲染屏幕上的元素时,想保证动画或变化的平滑性,可以用它。注意:IE9 不支持。

优点

  • 动画保持 60fps(每一帧 16 ms),浏览器内部决定渲染的最佳时机
  • 简洁标准的 API,后期维护成本低

缺点

  • 动画的开始/取消需要开发者自己控制,不像 ‘.debounce’ 或 ‘.throttle’由函数内部处理。
  • 浏览器标签未激活时,一切都不会执行。
  • 尽管所有的现代浏览器都支持 rAF ,IE9,Opera Mini 和 老的 Android 还是需要打补丁。
  • Node.js 不支持,无法在服务器端用于文件系统事件。