为什么别人的 hooks 里面有那么多的 ref
前言
最近因为一些原因对
useCallback有了一些新的认知,然后遇到一个朋友说他面试的时候被问到如何实现一个倒计时的hooks的时候,想到了这个问题,突然的茅塞顿开,对react中的hooks有了新的了解,所以有了这篇文章,记录一下我个人的想法。
在学习一些开源的库的时候,很容易发现开源库中 hooks 里面会写很多的 ref 来存储hooks的参数。
使用了 ref 之后,使用变量的地方就需要 .current 才能拿到变量的值,这比我直接使用变量肯定是变得麻烦了。对于有代码洁癖的人来说,这肯定是很别扭的。
但是在开源库的 hooks 中频繁的使用了 ref,这肯定不是一个毫无原因的点,那么究竟是什么原因,让开源库也不得不使用 .current 去获取变量呢?
useCallback
先跑个题,什么时候我们需要使用 useCallback 呢?
每个人肯定有每个人心中的答案,我来讲讲我的心路历程吧。
第一阶段-这是个啥
刚开始学react的时候,写函数式组件,我们定义函数的时候,肯定是不会有意识的把这个函数使用 useCallback 包裹。就这样写了一段时间的代码。
突然有一天我们遇到了一个问题,useEffect无限调用,找了半天原因,原来是因为我们还不是很清楚useEffect依赖的概念,把使用到的所有的变量一股脑的塞到了依赖数组里面,碰巧,我们这次的依赖数组里面有一个函数,在react每一次渲染的时候,函数都被重新创建了,导致我们的依赖每一次都是新的,然后就触发了无限调用。
百度了一圈,原来使用 useCallback 缓存一下这个函数就可以了,这样useEffect中的依赖就不会每一次都是一个新值了。
小总结: 在这个阶段,我们第一次使用 useCallback ,了解到了它可以缓存一个函数。
第二阶段-可以缓存
可以缓存就遇到了两个点:
- 缓存是吧,不会每一次都重新创建是吧,这样是不是性能就能提高了!那我把我所有用到的函数都使用
useCallback缓存一下。 react每一次render的时候会导致子组件重新渲染,使用memo可以缓存这个子组件,在父组件更新的时候,会浅层的比较子组件的props,所以传给子组件的函数就需要使用缓存useCallback起来,那么父组件中定义函数的时候图方便,一股脑的都使用useCallback缓存。
小总结: 在这里我们错误的认为了缓存就能够帮助我们做一些性能优化的事情,但是因为还不清楚根本的原因,导致我们很容易就滥用 useCallback
第三阶段-缓存不一定是好事
在这个阶段,写react也有一段时间了,我们了解到处处缓存其实还不如不缓存,因为缓存的开销不一定就比每一次重新创建函数的开销要小。
在这里肯定也是看了很多介绍 useCallback的文章了,推荐一下下面的文章
how-to-use-memo-use-callback,这个是全英文的,掘金有翻译这篇文章的,「好文翻译」。
小总结: 到这里我们就大概的意识到了,处处使用useCallback可能并不是我们想象的那样,对正确的使用useCallback有了一定的了解
总结
那么究竟在何时应该使用useCallback呢?
- 我们知道
react在父组件更新的时候,会对子组件进行全量的更新,我们可以使用memo对子组件进行缓存,在更新的时候浅层的比较一下props,如果props没有变化,就不会更新子组件,那如果props中有函数,我们就需要使用useCallback缓存一下这个父组件传给子组件的函数。 - 我们的
useEffect中可能会有依赖函数的场景,这个时候就需要使用useCallback缓存一下函数,避免useEffect的无限调用
是不是就这两点呢?那肯定不是呀,不然就和我这篇文章的标题联系不起来了吗。
针对useEffect这个hooks补充一点react官方文档里面有提到,建议我们使用自定义的 hooks 封装 useEffect。
- 那使用
useCallback的第三个场景就出现了,就是我们在自定义hooks需要返回函数的时候,建议使用useCallback缓存一下,因为我们不知道用户拿我们返回的函数去干什么,万一他给加到他的useEffect的依赖里面不就出问题了嘛。
一个自定义hook的案例
实现一个倒计时
hooks
需求介绍
我们先简单的实现一个倒计时的功能,就模仿我们常见的发短息验证码的功能。页面效果

app.jsx中

MessageBtn.jsx中

功能比较简单,按钮点击的时候创建了一个定时器,然后时间到了就清除这个定时器。
现在把 MessageBtn 中倒计时的逻辑写到一个自定义的hooks里面。
useCountdown
把上面的一些逻辑抽取一下,useCountdown主要接受一个倒计时的时长,返回当前时间的状态,以及一个开始倒计时的函数

这里的start函数用了useCallback,因为我们不能保证用户的使用场景会不会出问题,所以我们包一下
升级 useCountdown
现在我们期望useCountdown支持两个函数,一个是在倒计时的时候调用,一个是在倒计时结束的时候调用
预期的使用是这样的,通过一个配置对象传入 countdownCallBack函数和onEnd

改造 useCountdown
count定义 0 有点歧义,0 不能准确的知道是一开始的 0 还是倒计时结束的 0,所以还需要加一个标志位来表示当前是结束的 0- 使用
useEffect监听count的变化,变化的时候触发对应的方法
实现如下, 新增了红框的内容

提出问题
那么,现在就有一个很严重的问题,onEnd和 countdownCallBack这两个函数是外部传入的,我们要不要把他放到我们自定义hook的useEffect依赖项里面呢
我们不能保证外部传入的变量一定是一个被useCallback包裹的函数,那么肯定就不能放到useEffect依赖项里面。
如何解决这个问题呢?
答案就是使用useRef。(兜兜转转这么久才点题 (╥╯^╰╥))
用之前我们可以看一下成熟的方案是什么
比如ahooks里面的useLatest和useMemoizedFn的实现
useLatest源码

useMemoizedFn源码,主要看圈起来的地方就好了,本质也是用useRef记录传入的内容

ok,我们使用一下 useLatest 改造一下我们的useCountdown,变动点被红框圈起来了

总结
其实这篇文章的核心点有两个
- 带着大家重新的学习了一下
useCallback的使用场景。(useMemo类似) - 编写自定义
hooks时候,我们需要注意一下外部传入的参数,以及我们返回给用户的返回值,核心点是决不相信外部传入的内容,以及绝对要给用户一个可靠的返回值。
链接:https://juejin.cn/post/7271643757640007680
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。