You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
这个限制在开发中也确实会时常影响到我们的开发体验,比如函数组件中出现 if 语句提前 return 了,后面又出现 Hook 调用的话,React 官方推的 eslint 规则也会给出警告。
functionApp(){if(xxx){returnnull;}// ❌ React Hook "useState" is called conditionally. // React Hooks must be called in the exact same order in every component render.useState();return'Hello'}
React 官网介绍了 Hook 的这样一个限制:
这个限制在开发中也确实会时常影响到我们的开发体验,比如函数组件中出现 if 语句提前 return 了,后面又出现 Hook 调用的话,React 官方推的 eslint 规则也会给出警告。
其实是个挺常见的用法,很多时候满足某个条件了我们就不希望组件继续渲染下去。但由于这个限制的存在,我们只能把所有 Hook 调用提升到函数的顶部,增加额外开销。
由于 React 的源码太复杂,接下来本文会以原理类似但精简很多的 Preact 的源码为切入点来调试、讲解。
限制的原因
这个限制并不是 React 团队凭空造出来的,的确是由于 React Hook 的实现设计而不得已为之。
以 Preact 的 Hook 的实现为例,它用数组和下标来实现 Hook 的查找(React 使用链表,但是原理类似)。
可以看出,每次 Hook 的调用都对应一个全局的 index 索引,通过这个索引去当前运行组件
currentComponent
上的_hooks
数组中查找保存的值,也就是 Hook 返回的[state, useState]
那么假如条件调用的话,比如第一个
useState
只有 0.5 的概率被调用:在 Preact 第一次渲染组件的时候,假设
Math.random()
返回的随机值是0.6
,那么第一个 Hook 会被执行,此时组件上保存的_hooks
状态是:用图来表示这个查找过程是这样的:
假设第二次渲染的时候,
Math.random()
返回的随机值是0.3
,此时只有第二个 useState 被执行了,那么它对应的全局currentIndex
会是 0,这时候去_hooks[0]
中拿到的却是first
所对应的状态,这就会造成渲染混乱。没错,本应该值为
second
的 value,莫名其妙的被指向了first
,渲染完全错误!以这个例子来看:
结果是这样:
破解限制
有没有办法破解限制呢?
如果要破解全局索引递增导致的 bug,那么我们可以考虑换种方式存储 Hook 状态。
如果不用下标存储,是否可以考虑用一个全局唯一的 key 来保存 Hook,这样不是就可以绕过下标导致的混乱了吗?
比如
useState
这个 API 改造成这样:这样,通过
_hooks['key']
来查找,就无所谓前序的 Hook 出现的任何意外情况了。也就是说,原本的存储方式是:
改造后:
注意,数组本身就支持对象的 key 值特性,不需要改造
_hooks
的结构。改造源码
来试着改造一下 Preact 源码,它的 Hook 包的位置在 hooks/src/index.js 下,找到
useState
方法:它的底层调用了
useReducer
,所以新增加一个key
参数透传下去:useReducer
原本是通过全局索引去获取 Hook state:改造成兼容版本,有 key 的时候优先传入 key 值:
最后改造一下
getHookState
方法:这里设计成传入
key
值的时候,初始化就不往数组里push
新状态,而是直接通过下标写入即可,原本的取状态的写法hooks._list[index]
本身就支持通过key
从数组上取值,不用改动。至此,改造就完成了。
来试试新用法:
自动编译
事实上 React 团队也考虑过给每次调用加一个
key
值的设计,在 Dan Abramov 的 为什么顺序调用对 React Hooks 很重要? 中已经详细解释过这个提案。多重的缺陷导致这个提案被否决了,尤其是在遇到自定义 Hook 的时候,比如你提取了一个
useFormInput
:然后在组件中多次调用它:
此时这个通过
key
寻找 Hook state 的方式就会发生冲突。但我的想法是,能不能借助 babel 插件的编译能力,实现编译期自动为每一次 Hook 调用都注入一个
key
,伪代码如下:
生成这样的代码:
key 的生成策略可以是随机值,也可以是注入一个 Symbol,这个无所谓,保证运行时期不会改变即可。也许有一些我没有考虑周到的地方,对此有任何想法的同学都欢迎加我微信 sshsunlight 讨论,当然单纯的交个朋友也没问题,大佬或者萌新都欢迎。
总结
本文只是一篇探索性质的文章:
其实本意是帮助大家更好的理解 Hook。
我并不希望 React 取消掉这些限制,我觉得这也是设计的取舍。
如果任何子函数,任何条件表达式中都可以调用 Hook,代码也会变得更加难以理解和维护。
如果你真的希望更加灵活的使用类似的 Hook 能力,Vue3 底层响应式收集依赖的原理就可以完美的绕过这些限制,但更加灵活的同时也一定会无法避免的增加更多维护风险。
感谢大家
欢迎关注 ssh,前端潮流趋势、原创面试热点文章应有尽有。
记得关注后加我好友,我会不定期分享前端知识,行业信息。2021 陪你一起度过。
The text was updated successfully, but these errors were encountered: