13

一起围观由React Hooks防抖引发的面试翻车现场

 4 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzA5NzkwNDk3MQ%3D%3D&%3Bmid=2650591633&%3Bidx=1&%3Bsn=a21e2f331e1eabdf1dc49d952e5e0f8e
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

防抖是前端业务常用的工具,也是前端面试的高频问题。平时面试候选人,手写防抖人人都会,但是稍做修改就有小伙伴进坑送命。本文介绍了如何在react hooks中实现防抖。

背景

防抖(debounce)是前端经常用到的工具函数,也是我在面试中必问的一个问题。团队内部推广React Hooks以后,我在面试中也加入了相关的题目。如何实现 useDebounce。 这个看起来很基础的问题,实际操作起来却让很多小伙伴漏出马脚。

面试的设计往往是这样的:

  1. 什么是防抖、节流,分别解释一下?

  2. 在白纸上手写一个防抖or节流函数,自己任选(限时4分钟)

  3. react hooks有了解吗?上机实现一个useDebounce、useThrottle,自己任选

  4. typescript有了解吗?用ts再来写一遍

  5. 聊一聊用到防抖、节流的业务场景

围绕一个主题不断切换考察点,这样一轮下来,轻松又流畅,同时可以试探出很多信息。

实际情况是,很多候选人在第3题就卡住了,不得不说很可惜。

场景还原

写一个防抖函数

一个经典的防抖函数可能是这样的:

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>

}

很多小 伙伴 会想当然的就改成这样:

function useDebounce(fn, time) {

return debounce(fn, time);

}

简单、优雅,还复用了刚才的代码,测试一下,看起来并没有什么问题:

N326veF.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>

}

auQ77rm.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) 修改的情况下,可以得到看似正确的结果。但并没有正确解决问题(再说你也不能通过“ 改用例 ”的方式来修bug呀:joy:)。依然是错误的。有兴趣的读者可以复现一下这个现象,思考一下为什么, 欢迎留言讨论

问题出在哪里?

我们在useDebounce里面加个log

function useDebounce(fn, time) {

console.log('usedebounce')

return debounce(fn, time);

}

RfM7Bzm.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引入生产项目的初期要格外留意 写法和原理的差异 所带来的隐患。不然就像上面的候选人一样大意失荆州……

分析一下这道题 易错原因:

  • 马虎大意。debounce很简单,react hooks也不难,万万没想到结合起来就有坑

  • 心态崩坏。面试场景下,遇到没有见过的问题,无法冷静分析。

  • 对react hooks理解不够深刻,踩坑不多

  • 对debounce也不够熟悉,有背代码的嫌疑

由于太多人挂在这个问题上,我决定不再用作面试题,并把它分享出来,希望可以帮到大家。 关于防抖还有些坑呢? 欢迎留言讨论


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK