防抖是前端业务常用的工具函数,也是前端面试的高频问题。平时面试候选人,手写防抖人人都会,但是稍做修改就有小伙伴进坑送命。本文介绍了如何在react hooks中实现防抖。
防抖(debounce)是前端经常用到的一个工具函数,也是我在面试中必问的一个问题。团队内部推广React hooks以后,我在面试中也加入了相关的题目。如何实现一个useDebounce
这个看起来很基础的问题,实际操作起来却让很多背代码的小伙伴漏出马脚。
问题的安排往往是这样的:
围绕一个主题不断切换考察点,这样一轮下来,轻松又流畅,同时可以试探出很多信息。
实际情况是,很多候选人在第3题就卡住了,不得不说很可惜。
一个经典的防抖函数可能是这样的:
function debounce(fn, ms) { let timer; return function(...args) { if (timer) { clearTimeout(timer) } timer = setTimeout(() => { fn(...args) timer = null; }, ms); } } 复制代码
先提供测试用例:
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> } 复制代码
很多小伙伴会想当然的就改成这样:
function useDebounce(fn, time) { return debounce(fn, time); } 复制代码
简单、优雅,还复用了刚才的代码,测试一下,看起来并没有什么问题:
1-7292504.gif
但是这个代码如果放上生产环境,你会被用户锤死。
真的吗?
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> } 复制代码
2-7292504.gif
当引入一个自动累加counter2就开始出问题了。这时很多候选人就开始懵了,有的候选人会尝试分析原因。只有深刻理解react hooks在重渲染时的工作原理才能快速定位到问题(事实上出错不要紧,能够快速定位问题的小伙伴才是我们苦苦寻找的)。
有的候选人开启胡乱调试大法,慌忙修改setCounter1:
const handleClick = useDebounce(function() { console.count('click1') setCounter1(x => x + 1) }, 500) 复制代码
当然结果依然错误,而且暴漏了自己对react hooks特性不够熟悉的问题……
有的候选人猜到是重渲染缓存的问题,于是写成这样:
function useDebounce(fn, delay) { return useCallback(debounce(fn, delay), []) } 复制代码
在配合setCounter1(x => x + 1)
修改的情况下,可以得到正确的结果。但并没有正确解决问题。依然是错误的。有兴趣的读者可以复现一下这个现象,思考一下为什么,欢迎留言讨论。
我们在useDebounce里面加个log
function useDebounce(fn, time) { console.log('usedebounce') return debounce(fn, time); } 复制代码
3-7292504.gif
控制台开始疯狂的输出log。看到这里,很多读者就明白了。如果是前面表现稍好的候选人,我可以提示到此。
每次组件重新渲染,都会执行一遍所有的hooks,这样debounce高阶函数里面的timer就不能起到缓存的作用(每次重渲染都被置空)。timer不可靠,debounce的核心就被破坏了。
修复这个问题可以有很多办法。比如利用React组件的缓存机制:
function useDebounce(fn, delay) { const { current } = useRef({}); function f(...args) { if (current.timer) { clearTimeout(current.timer); } current.timer = setTimeout(fn.bind(undefined, ...args), delay); } return f; } 复制代码
就可以实现一个可靠的useDebounce。
同理我们直接给出useThrottle的代码:
export function useThrottle(fn, delay) { const { current } = useRef({}); function f(...args) { if (!current.timer) { current.timer = setTimeout(() => { delete current.timer; }, delay); fn(...args); } } return f; } 复制代码
使用react hooks可以帮助我们把一些常用的状态逻辑沉淀下来。同时,react hooks引入生产项目的初期要格外留意写法和原理的差异所带来的隐患。不然就跟上面的候选人一样大意失荆州……
分析一下这道题易错的原因:
由于太多人挂在这个问题上,我决定把它分享出来,希望可以帮到大家。