防抖是前端业务经常使用的工具函数,也是前端面试的高频问题。平时面试候选人,手写防抖人人都会,可是稍作修改就有小伙伴进坑送命。本文介绍了如何在react hooks中实现防抖。javascript
防抖(debounce)是前端常常用到的一个工具函数,也是我在面试中必问的一个问题。团队内部推广React hooks之后,我在面试中也加入了相关的题目。如何实现一个useDebounce
这个看起来很基础的问题,实际操做起来却让不少背代码的小伙伴漏出马脚。前端
问题的安排每每是这样的:java
围绕一个主题不断切换考察点,这样一轮下来,轻松又流畅,同时能够试探出不少信息。react
实际状况是,不少候选人在第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 ui
可是这个代码若是放上生产环境,你会被用户锤死。
真的吗?
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, 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)
}
复制代码
就能够实现一个可靠的useDebounce。
同理咱们直接给出useThrottle的代码:
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能够帮助咱们把一些经常使用的状态逻辑沉淀下来。同时,react hooks引入生产项目的初期要格外留意写法和原理的差别所带来的隐患。否则就跟上面的候选人同样大意失荆州……
分析一下这道题易错的缘由:
因为太多人挂在这个问题上,我决定把它分享出来,但愿能够帮到你们。