【第1999期】深入源码剖析componentWillXXX为什么UNSAFE
前言
一周好快。今日早读文章由奇舞团前端工程师@卡颂投稿分享。
@卡颂,来自奇舞团,奇舞团(75team.com)是360集团最大的大前端团队
正文从这开始~~
从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,
);
}
你可以从这里:https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberClassComponent.new.js#L1034看到这段源码
其中callComponentWillReceiveProps方法会调用componentWillRecieveProps。
可以看到,是否调用的关键是比较unresolvedOldProps与 unresolvedNewProps是否全等,以及context是否变化。
其中unresolvedOldProps为组件上次更新时的props,而unresolvedNewProps则来自ClassComponent调用this.render返回的JSX中的props参数。
可见他们的引用是不同的。所以他们全等比较为false。
基于此原因,每次父组件更新都会触发当前组件的componentWillRecieveProps。
想想你是否也曾误用过?
模式迁移
让我们再看第二个原因:
React从Legacy模式迁移到Concurrent模式后,这些钩子的表现会和之前不一致。
我们先了解下什么是模式?不同模式有什么区别?
从Legacy到Concurrent
从React15升级为React16后,源码改动如此之大,说React被重构可能更贴切些。
正是由于变动如此之大,使得一些特性在新旧版本React中表现不一致,这里就包括上文谈到的三个生命周期钩子。
为了让开发者能平稳从旧版本迁移到新版本,React推出了三个模式:
legacy模式 -- 通过ReactDOM.render创建的应用会开启该模式。这是当前React使用的方式。这个模式可能不支持一些新功能。
blocking模式 -- 通过ReactDOM.createBlockingRoot创建的应用会开启该模式。开启部分concurrent模式特性,作为迁移到concurrent模式的第一步。
concurrent模式 -- 通过ReactDOM.createRoot创建的应用会开启该模式。面向未来的开发模式。
你可以从这里:https://zh-hans.reactjs.org/docs/concurrent-mode-adoption.html#why-so-many-modes看到不同模式的特性支持情况
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的源码
lane:代表优先级。即图中红色节点与蓝色节点的区别。
payload:更新挂载的数据。对于this.setState创建的更新,payload为this.setState的传参。
next:与其他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技术揭秘:https://react.iamkasong.com/
为你推荐
【第1945期】彻底搞懂React源码调度原理(Concurrent模式)
欢迎自荐投稿,前端早读课等你来