hello~亲爱的观众老爷们大家好~最近负责重构某个内部系统,既然是内部系统,那当然可以尽情搞事情,分析需求后决定采用 React
最新版本进行重构。既然是最新的版本,那当然是使用 Hooks
进行开发了。开发的过程并非一帆风顺,但也算是踩过不少坑也重新爬出来了,小结后有了这篇文章~
注意~这篇文章是实践相关的讨论,如果不太清楚 Hooks
的同学,可能这篇文章不太适合你。同时,我在实践的过程中,使用了若干 React
官方不建议的模式,本文主要是对此进行讨论,希望对你编写基于 Hooks
的 React
代码时有所帮助~以下是正文:
在循环,条件中调用 Hook
React
的官网在 Hook
规则 一节中,有这么一段话:
不要在循环,条件或嵌套函数中调用
Hook
, 确保总是在你的React
函数的最顶层调用他们。
然而,我在实际的开发过程中,(为了省事)经常违背了这条原则。先理解一下为何官方不建议在循环,条件或嵌套函数中调用 Hook
。函数组件内的 Hook
是基于链表进行注册的,也就是一个有着固定顺序的序列,如下所示:
Hook1 ⟶️ Hook2 ⟶️ Hook3 ...⟶️... HookN
复制代码
假设 Hook2
处于判断条件之中,一旦 condition
修改,执行的顺序就会发生改变:
if (condition) {
useHook2();
}
执行顺序变为:Hook1 ⟶️ Hook3 ...⟶️... HookN
复制代码
顺序会发生偏移,从而导致 React
内部报错。
大致了解代码原理后,就能理解为何官方不建议在循环或条件中使用 Hook
。然而,这个问题的根本原因是顺序产生了变化,因而导致 bug 的出现。那如果我们能保证执行顺序呢?比如:在本地开发环境中需要增加一些调试代码,但不希望线上出现对应的代码,一般我们会这么写:
if (process.env.NODE_ENV === 'development') {
//do sth...
}
复制代码
发布正式代码时,这段调试代码会被打包工具正确去除。由于环境变量是固定的(同一环境之中基本上不会产生变化),因而即使 Hook
处于条件判断之中,函数中 Hook
的顺序是固定的,使用是并不会产生问题:
function Test() {
let test;
let setTest;
if (process.env.NODE_ENV === 'development') {
[test, setTest] = useState(1); // eslint-disable-line
}
// 添加 eslint-disable-line,是因为 ESLint 在开发环境中会检测 `Hook` 是否在合理的上下文之中(即不在条件判断或循环之中),不然会报错并停止渲染
return (
<div>
<p>{test}</p>
<button onClick={() => setTest(test + 1)}>click</button>
</div>
)
}
复制代码
在循环条件中同理(只要循环次数固定,也不会有问题)。只要确定 Hook
的顺序不变,未尝不可在条件或循环中使用,只要你清楚知道自己在干什么~
依赖欺骗与精确依赖
无论是 React
的文档还是 Hooks
相关的文章,很多都建议我们对 Hook
的依赖数组诚实,如实填写 Hook
内的变量,以避免 bug 的产生:
useEffect(() => {
reportToService(userName, userId, ...)
}, [userName, userId, ...])
复制代码
然而,这并不该是死板的教条~想象一下上述代码的场景,这是一段上报数据到服务器的逻辑。如果用户登出后,用户名之类的信息必然产生变化,那么 useEffect
会重新执行,假设这是一段统计 PV 的代码,那就存在重复统计的问题。因而,只要你清楚知道自己在干什么,依赖项其实是可以根据实际情况填写的,没必要强行将全部用到的变量填进去。
而精确依赖,就是 React
不可变数据的一个体现,某程度上说,应该说是避免错误填写依赖,考虑以下例子:
function Test() {
const [test, setTest] = useState({
key1: 1,
key2: 2
});
useEffect(() => {
console.log(test.key2);
}, [test]);
return (
<div>
<p>{test.key1}</p>
<button onClick={() => setTest(test => {
return {
...test,
key1: test.key1 + 1
}
})}>
click
</button>
</div>
)
}
复制代码
以上例子可以正常运行,ESLint
也不会报错。然而当我们不断点击按钮时,useEffect
会不断执行,打印出 test.key2
的值。这是由于按钮点击后, test
是一个新的对象,因而 useEffect
的依赖项发生了变化,于是不断被执行。但这是毫无意义的,我们真正想监听的的是 test.key2
,因而依赖的项应该精准地填写为 test.key2
,修改后 useEffect
只会在 test.key2
变化后才会执行。
精确依赖是一个很小的细节,但经常会导致重复执行 Hook
,在定位类似问题时不妨先检查一下依赖了错误的变量。
Hook
既不是 setState
,也不是生命周期函数
由于我很长一段时间没写 React
,因而一开始写 Hook
的时候还是带着浓重的 class
色彩,基本可以说只是将 class
“翻译”成 function
而已。
在使用 useState
的时候,往里面丢一个很大的对象,模仿之前 state
的写法,但这是典型的反模式。尽管数据的封装是必须的,如将用户相关的数据统合在 userInfo
对象之中,然而将全部内容像之前的 class
一样,放在一个 state
中,是不可取的,会导致这个 Hook
变得相当繁重,也不利于逻辑复用,违背了 Hook
最初的目的。
Hook
不是 setState
还是比较好理解,但 Hook
不是生命周期函数就不是那么好习惯了。例如我们经常在 componentWillUnmount
中解除定时器、解绑事件等等:
componentWillUnmount() {
clearTimeout(timer);
removeEventListener('click', handler);
...
}
复制代码
然而,在 Hook
中,把解除定时器、解绑事件等操作全部写到一个 useEffect
中去,并不是最佳实践。通过上文我们了解到,尽量要做到精确依赖,避免不必要的开销。而从逻辑复用的角度而言,将代码按照功能拆开,更有利于复用。因而我们应该写成:
useEffect(() => {
const timer = setTimeout(() => {
...
});
return () => {
clearTimeout(timer);
}
}, []);
useEffect(() => {
element.addEventListener('click', handler);
return () => {
element.removeEventListener('click', handler);
}
}, []);
...
复制代码
class
中 每个生命周期函数只有一个,而 function
中相应的 Hooks
可以有多个~分拆有利于代码清晰与逻辑复用。
小议性能
性能是个很大的话题,在 Hooks
中性能相关的问题,基本可以独立一篇文章来写了~由于 React
遵循的是不可变数据,因此它的更新是批量的:
按照最佳实践,我们应该使用 useMemo
、 useCallback
等等进行缓存,避免重复生成变量而导致无意义的重算 Virtual DOM。但喜欢“捣乱”的我,稍微提出一点反模式以做抛砖引玉之用——我们的应用可能没那么大以至于需要全面考虑性能。
使用 Hook
进行开发时,为了保证变量不变,需要使用很多 useMemo
、 useCallback
之类的钩子,为了极致的性能每个变量与方法都要被这些钩子包囊。有时候感觉就像是不断地书写模板代码,回忆起之前被 Redux
支配的恐惧了么~ Hook
也有点这个感觉。
要明确一点,性能确实很重要。但也要明白,对于一般的前端应用而言,在 1ms 还是 10ms 内完成 diff
,其实意义是不大的。按照经验而言,低端手机百万级别内的运算,并不会产生明显的卡顿。然而 DOM
就相当慢了,低端机大概 6000 个 DOM
重新渲染,就会产生非常明显的卡顿。因而,就个人的角度而言,保持应用的高性能值得称道,但不建议在中小的应用中追求极致性能,在碰到性能瓶颈时再进行优化也未尝不可。
小结
以上就是本文的全部内容啦!这是我在项目中使用 Hook
进行开发后的一点思考,毕竟“尽信书,不如无书”,只有通过实践才能掌握新的技术。
以上是个人的一点浅见,感谢各位看官大人看到这里。知易行难,希望本文对你有所帮助~谢谢!