深入源码剖析componentWillXXX为什么UNSAFE

编者按:本文作者苏畅,奇舞团前端开发工程师。

从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,
  );
}

你可以从这里1看到这段源码

其中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创建的应用会开启该模式。面向未来的开发模式。

你可以从这里2看到不同模式的特性支持情况

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方法返回,你可以从这里3 看到 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方法返回,你可以从这里4看到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技术揭秘5

Github地址:https://github.com/BetaSu/just-react

文内链接

  1. https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberClassComponent.new.js#L1034

  2. https://zh-hans.reactjs.org/docs/concurrent-mode-adoption.html#why-so-many-modes

  3. https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactUpdateQueue.old.js#L189

  4. https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactUpdateQueue.new.js#L157

  5. https://react.iamkasong.com/

关于奇舞周刊

《奇舞周刊》是360公司专业前端团队「奇舞团」运营的前端技术社区。关注公众号后,直接发送链接到后台即可给我们投稿。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值