componentWillUpdate 会在 render 前被触发,它和 componentWillMount 类似,允许你在里面做一些不涉及真实 DOM 操作的准备工作
这个是肯定不可以的,会造成死循环,当然这是显而易见的,但是还有其他原因
为什么呢?
这个方法在react16中被标记为了UNSAFE,与之一样的还有 componentWillMount,componentWillRecieveProps 为啥呢?
究其原因,有如下两点:
- 这三个钩子经常被错误使用,并且现在出现了更好的替代方案(这里指新增的getDerivedStateFromProps与getSnapshotBeforeUpdate)。
- React从Legacy模式迁移到Concurrent模式后,这些钩子的表现会和之前不一致
为了让开发者能平稳从旧版本迁移到新版本,React推出了三个模式:
- legacy模式 – 通过ReactDOM.render创建的应用会开启该模式。这是当前React使用的方式。这个模式可能不支持一些新功能。
- blocking模式 – 通过 ReactDOM.createBlockingRoot创建的应用会开启该模式。开启部分concurrent模式特性,作为迁移到concurrent模式的第一步。
- concurrent模式 – 通过ReactDOM.createRoot创建的应用会开启该模式。面向未来的开发模式。
concurrent模式相较我们当前使用的legacy模式最主要的区别是将同步的更新机制重构为异步可中断的更新
updated
在React源码中,每次发起更新都会创建一个Update对象,同一组件的多个Update(会以链表的形式保存在updateQueue中,首先了解下他们的数据结构。
Update有很多字段,当前我们关注如下三个字段
const update: Update<*> = {
// ...省略当前不需要关注的字段
lane,
payload: null,
next: null
};
- lane:代表优先级。即图中红色节点与蓝色节点的区别。
- payload:更新挂载的数据。对于this.setState创建的更新,payload为this.setState的传参。
- next:与其他Update连接形成链表。
updateQueue结构如下:
const queue: UpdateQueue<State> = {
baseState: fiber.memoizedState,
firstBaseUpdate: null,
lastBaseUpdate: null,
shared: {
pending: null,
},
// 其他参数省略...
};
- baseState:更新基于哪个state开始。上图中版本控制的例子中,高优bug修复后提交master,其他commit基于master分支继续开发。这里的master分支就是baseState。
- firstBaseUpdate与lastBaseUpdate:更新基于哪个Update开始,由firstBaseUpdate开始到lastBaseUpdate结束形成链表。这些Update是在上次更新中由于优先级不够被留下的
- shared.pending:本次更新的单或多个Update形成的链表。
其中baseUpdate + shared.pending会作为本次更新需要执行的Update
一次更新
在某个组件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。
getSnapshotBeforeUpdate
getSnapshotBeforeUpdate() 在最近一次渲染输出(提交到 DOM 节点)之前调用。它使得组件能在发生更改之前从 DOM 中捕获一些信息(例如,滚动位置)。此生命周期的任何返回值将作为参数传递给 componentDidUpdate()
从Reactv16开始,componentWillXXX钩子前增加了UNSAFE_前缀。
究其原因,是因为Stack Reconciler重构为Fiber Reconciler后,render阶段的任务可能中断/重新开始,对应的组件在render阶段的生命周期钩子(即componentWillXXX)可能触发多次。
这种行为和Reactv15不一致,所以标记为UNSAFE_。
那为什么getSnapshotBeforeUpdate就可以呢?
那是因为getSnapshotBeforeUpdate是在commit阶段内的before mutation阶段调用的,由于commit阶段是同步的,所以不会遇到多次调用的问题
getDerivedStateFromProps
getDerivedStateFromProps 会在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。它应返回一个对象来更新 state,如果返回 null 则不更新任何内容
请注意,不管原因是什么,都会在每次渲染前触发此方法。这与 UNSAFE_componentWillReceiveProps 形成对比,后者仅在父组件重新渲染时触发,而不是在内部调用 setState 时
为什么父组件重新渲染时时触发?
让我们看看源码,这段代码出自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,而不是每次props变化后触发