查看原文
其他

【第1999期】深入源码剖析componentWillXXX为什么UNSAFE

卡颂 前端早读课 2020-10-15

前言

一周好快。今日早读文章由奇舞团前端工程师@卡颂投稿分享。

@卡颂,来自奇舞团,奇舞团(75team.com)是360集团最大的大前端团队

正文从这开始~~

从v16.3.0开始如下三个生命周期钩子被标记为UNSAFE。

  • componentWillMount

  • componentWillRecieveProps

  • componentWillUpdate

究其原因,有如下两点:

  • 这三个钩子经常被错误使用,并且现在出现了更好的替代方案(这里指新增的getDerivedStateFromProps与getSnapshotBeforeUpdate)。

  • React从Legacy模式迁移到Concurrent模式后,这些钩子的表现会和之前不一致。

本文会从React源码的角度剖析这两点。

同时,通过本文的学习你可以掌握React异步状态更新机制的原理。

被误用的钩子

我们先来探讨第一点,这里我们以componentWillRecieveProps举例。

我们经常在componentWillRecieveProps内处理props改变带来的影响。有些同学认为这个钩子会在每次props变化后触发。

真的是这样么?让我们看看源码。

这段代码出自updateClassInstance方法:

  1. if (

  2. unresolvedOldProps !== unresolvedNewProps ||

  3. oldContext !== nextContext

  4. ) {

  5. callComponentWillReceiveProps(

  6. workInProgress,

  7. instance,

  8. newProps,

  9. nextContext,

  10. );

  11. }

你可以从这里: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有很多字段,当前我们关注如下三个字段:

  1. const update: Update<*> = {

  2. // ...省略当前不需要关注的字段

  3. lane,

  4. payload: null,

  5. next: null

  6. };

Update由createUpdate方法返回,你可以从这里看到createUpdate的源码

  • lane:代表优先级。即图中红色节点与蓝色节点的区别。

  • payload:更新挂载的数据。对于this.setState创建的更新,payload为this.setState的传参。

  • next:与其他Update连接形成链表。

updateQueue结构如下:

  1. const queue: UpdateQueue<State> = {

  2. baseState: fiber.memoizedState,

  3. firstBaseUpdate: null,

  4. lastBaseUpdate: null,

  5. shared: {

  6. pending: null,

  7. },

  8. // 其他参数省略...

  9. };

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的优先级,数字越小优先级越高。

  1. baseState = '';


  2. A1 - B2 - C1 - D2

首次渲染时,优先级1。B D优先级不够被跳过。

为了保证更新的连贯性,第一个被跳过的Update(B)及其后面所有Update会作为第二次渲染的baseUpdate,无论他们的优先级高低,这里为B C D。

  1. baseState: ''

  2. Updates: [A1, C1]

  3. Result state: 'AC'

接着第二次渲染,优先级2。

由于B在第一次渲染时被跳过,所以在他之后的C造成的渲染结果不会体现在第二次渲染的baseState中。所以baseState为A而不是上次渲染的Result state AC。这也是为了保证更新的连贯性。

  1. baseState: 'A'

  2. Updates: [B2, C1, D2]

  3. Result state: 'ABCD'

我们发现,C同时出现在两次渲染的Updates中,他代表的状态会被更新两次。

如果有类似的代码:

  1. componentWillReceiveProps(nextProps) {

  2. if (!this.props.includes('C') && nextProps.includes('C')) {

  3. // ...do something

  4. }

  5. }

则很有可能被调用两次,这与同步更新的React表现不一致!

基于以上原因,componentWillXXX被标记为UNSAFE。

总结

由于篇幅有限,本次我们只聚焦了React源码的冰山一角。

如果想深入学习React源码,作者编写的一本开源、严谨、易懂的React源码电子书 —— React技术揭秘:https://react.iamkasong.com/

为你推荐


【第1945期】彻底搞懂React源码调度原理(Concurrent模式)


【第1801期】高德APP全链路源码依赖分析工程


【第1498期】webpack loader机制源码解析


欢迎自荐投稿,前端早读课等你来

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存