从v16.3.0
开始如下三个生命周期钩子被标记为UNSAFE
。
componentWillMount
componentWillRecieveProps
componentWillUpdate
究其原因,有如下两点:
这三个钩子经常被错误使用,并且现在出现了更好的替代方案(这里指新增的getDerivedStateFromProps
与getSnapshotBeforeUpdate
)。
React
从Legacy
模式迁移到Concurrent
模式后,这些钩子的表现会和之前不一致。
本文会从React
源码的角度剖析这两点。
同时,通过本文的学习你可以掌握React异步状态更新机制
的原理。
我们先来探讨第一点,这里我们以componentWillRecieveProps
举例。
我们经常在componentWillRecieveProps
内处理props
改变带来的影响。有些同学认为这个钩子会在每次props
变化后触发。
真的是这样么?让我们看看源码。
这段代码出自updateClassInstance
方法:
if ( unresolvedOldProps !== unresolvedNewProps || oldContext !== nextContext ) { callComponentWillReceiveProps( workInProgress, instance, newProps, nextContext, ); } 复制代码
你可以从这里看到这段源码
其中callComponentWillReceiveProps
方法会调用componentWillRecieveProps
。
可以看到,是否调用的关键是比较unresolvedOldProps
与 unresolvedNewProps
是否全等,以及context
是否变化。
其中unresolvedOldProps
为组件上次更新时的props
,而unresolvedNewProps
则来自ClassComponent
调用this.render
返回的JSX
中的props
参数。
可见他们的引用
是不同的。所以他们全等比较
为false
。
基于此原因,每次父组件更新都会触发当前组件的componentWillRecieveProps
。
想想你是否也曾误用过?
让我们再看第二个原因:
React
从Legacy
模式迁移到Concurrent
模式后,这些钩子的表现会和之前不一致。
我们先了解下什么是模式?不同模式有什么区别?
从React15
升级为React16
后,源码改动如此之大,说React
被重构可能更贴切些。
正是由于变动如此之大,使得一些特性在新旧版本React
中表现不一致,这里就包括上文谈到的三个生命周期钩子。
为了让开发者能平稳从旧版本迁移到新版本,React
推出了三个模式:
legacy模式
-- 通过ReactDOM.render
创建的应用会开启该模式。这是当前React
使用的方式。这个模式可能不支持一些新功能。blocking模式
-- 通过ReactDOM.createBlockingRoot
创建的应用会开启该模式。开启部分concurrent
模式特性,作为迁移到concurrent
模式的第一步。concurrent模式
-- 通过ReactDOM.createRoot
创建的应用会开启该模式。面向未来的开发模式。你可以从这里看到不同模式的特性支持情况
concurrent模式
相较我们当前使用的legacy模式
最主要的区别是将同步的更新机制重构为异步可中断的更新。
接下来我们来探讨React
如何实现异步更新
,以及为什么异步更新
情况下钩子的表现和同步更新
不同。
我们可以用代码版本控制
类比更新机制
。
在没有代码版本控制
前,我们在代码中逐步叠加功能。一切看起来井然有序,直到我们遇到了一个紧急线上bug(红色节点)。
为了修复这个bug,我们需要首先将之前的代码提交。
在React
中,所有通过ReactDOM.render
创建的应用都是通过类似的方式更新状态。
即所有更新
同步执行,没有优先级
概念,新来的高优更新
(红色节点)也需要排在其他更新
后面执行。
当有了代码版本控制
,有紧急线上bug需要修复时,我们暂存当前分支的修改,在master分支
修复bug并紧急上线。
bug修复上线后通过git rebase
命令和开发分支
连接上。开发分支
基于修复bug的版本继续开发。
在React
中,通过ReactDOM.createBlockingRoot
和ReactDOM.createRoot
创建的应用在任务未过期情况下会采用异步的方式更新状态。
高优更新
(红色节点)中断正在进行中的低优更新
(蓝色节点),先完成渲染流程。
待高优更新
完成后,低优更新
基于高优更新
的部分
或者完整
结果重新更新。
在React
源码中,每次发起更新
都会创建一个Update
对象,同一组件的多个Update
(如上图所示的A -> B -> C)会以链表
的形式保存在updateQueue
中。
首先了解下他们的数据结构
。
Update
有很多字段,当前我们关注如下三个字段:
const update: Update<*> = { // ...省略当前不需要关注的字段 lane, payload: null, next: null }; 复制代码
Update
由createUpdate
方法返回,你可以从这里看到createUpdate
的源码
红色
节点与蓝色
节点的区别。this.setState
创建的更新
,payload
为this.setState
的传参。Update
连接形成链表。updateQueue
结构如下:
const queue: UpdateQueue<State> = { baseState: fiber.memoizedState, firstBaseUpdate: null, lastBaseUpdate: null, shared: { pending: null, }, // 其他参数省略... }; 复制代码
UpdateQueue
由initializeUpdateQueue
方法返回,你可以从这里看到initializeUpdateQueue
的源码
baseState
:更新
基于哪个state
开始。上图中版本控制
的例子中,高优bug修复后提交master
,其他commit
基于master
分支继续开发。这里的master
分支就是baseState
。firstBaseUpdate
与lastBaseUpdate
:更新
基于哪个Update
开始,由firstBaseUpdate
开始到lastBaseUpdate
结束形成链表。这些Update
是在上次更新
中由于优先级
不够被留下的,如图中A B C
。shared.pending
:本次更新的单或多个Update
形成的链表。其中baseUpdate
+ shared.pending
会作为本次更新需要执行的Update
。
了解了数据结构
,接下来我们模拟一次异步中断更新
,来揭示本文探寻的秘密 —— componentWillXXX
为什么UNSAFE
。
在某个组件updateQueue
中存在四个Update
,其中字母
代表该Update
要更新的字母,数字
代表该Update
的优先级,数字越小优先级
越高。
baseState = ''; A1 - B2 - C1 - D2 复制代码
首次渲染时,优先级
1。B D
优先级不够被跳过。
为了保证更新
的连贯性,第一个被跳过的Update
(B
)及其后面所有Update
会作为第二次渲染的baseUpdate
,无论他们的优先级
高低,这里为B C D
。
baseState: '' Updates: [A1, C1] Result state: 'AC' 复制代码
接着第二次渲染,优先级
2。
由于B
在第一次渲染时被跳过,所以在他之后的C
造成的渲染结果不会体现在第二次渲染的baseState
中。所以baseState
为A
而不是上次渲染的Result state AC
。这也是为了保证更新
的连贯性。
baseState: 'A' Updates: [B2, C1, D2] Result state: 'ABCD' 复制代码
我们发现,C
同时出现在两次渲染的Updates
中,他代表的状态
会被更新两次。
如果有类似的代码:
componentWillReceiveProps(nextProps) { if (!this.props.includes('C') && nextProps.includes('C')) { // ...do something } } 复制代码
则很有可能被调用两次,这与同步更新
的React
表现不一致!
基于以上原因,componentWillXXX
被标记为UNSAFE
。
由于篇幅有限,本次我们只聚焦了React
源码的冰山一角。
如果想深入学习React
源码,在此向你推荐开源
、严谨
、易懂
的React源码电子书 —— React技术揭秘
同时可以加微信(iamkasong)拉你进源码交流群
和小伙伴们一起交流React源码
。
React Contributor
在线答疑。