防抖(debounce)是前端经常用到的一个工具函数,也是在面试中常常被问到的一个问题。
防抖函数
经典的防抖函数:
function debounce(fn, ms) {
let timer;
return function(...args) {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
fn(...args)
timer = null;
}, ms);
}
}
react hooks版的防抖函数
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>
}
实际使用的时候,是这样的
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>
}
当引入一个自动累加counter2就开始出问题了。这时很多候选人就开始懵了,有的候选人会尝试分析原因。只有深刻理解react hooks在重渲染时的工作原理才能快速定位到问题(事实上出错不要紧,能够快速定位问题的小伙伴才是我们苦苦寻找的)。
你可能会这样修改
const handleClick = useDebounce(function() {
console.count('click1')
setCounter1(x => x + 1)
}, 500)
还可能这样修改
function useDebounce(fn, delay) {
return useCallback(debounce(fn, delay), [])
}
在配合setCounter1(x => x + 1)
修改的情况下,可以得到正确的结果。
但显然你仔细测试一下,还是没有真正的解决问题。
那么,问题在哪里呢?
function useDebounce(fn, time) {
console.log('usedebounce') // 打印这里看一下
return debounce(fn, time);
}
控制台开始疯狂的输出log...,大脑在飞快的思考。
每次组件重新渲染,都会执行一遍所有的hooks,这样debounce高阶函数里面的timer就不能起到缓存的作用(每次重渲染都被置空)。timer不可靠,debounce的核心就被破坏了。
这才是优化
// 加入缓存机制
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)
}
//
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);
}
react hooks可以帮助我们把一些常用的状态逻辑沉淀下来,你学会了吗?