原作者: Max Koretskyi · 15 March, 2020 ·【19min阅读时间】 原文链接:indepth.dev/inside-fibe…
本文主要带领读者深入了解react的新特性Fiber。一起来学习新调和算法中的两个关键阶段。 我们会深入了解React是如何更新state,props和处理children的。
前言
React是一个用来构建UI界面的Javascript 库。它的核心机制在于追踪组件状态的变换并把更新后的状态映射到UI上。在React中我们把这个过程叫做reconciliation(调解)。当我们调用了setState方法,React会检测state或者props是否发生改变并且决定是否重新渲染组件。
关于协调算法机制,react文档提供了一份很棒的高度概述的说明来讲React元素,生命周期方法和render方法以及应用到组件子类的diffing算法在其中所扮演的角色。从 render
方法返回的不可变的React元素就是我们通常所说的“虚拟DOM”。 “虚拟DOM”这个术语早期帮助人们来理解React,但也造成了一些困惑,所以在React的官方文档中不再使用这个术语了。所以在本文章中,我会叫它React元素树。
除了React元素树,React还有一表示内部实例(组件,DOM节点等等)的树,用来保持状态。从16.0版本开始, React推出了一套新的关于这棵内部状态树的实现方法和算法(被称作fiber). 如果你想知道引入Fiber有什么优势,请阅读 React如何并为何在Fiber中使用链表。
这是教你了解React内部原理的系类的第一篇文章。本文中,我会带你深入了解与新协调算法有关的一些重要的概念和数据结构。等我们有了足够的背景知识,我们将会探索算法和主要的用来遍历和处理fiber树的方法。之后该系列的文章会展示React如何使用算法实现初始的渲染和处理state和props的更新的。在之后,我们会学习scheduler,子元素协调过程以及构建effect list 的过程。
我将会给你一些比较前沿的知识。我鼓励你去阅读并理解我们当前所用到的React背后原理,感受它的奇妙之处。如果你考虑想要为React源码贡献的话,那么这个系列的文章会成为一个很好的指南。我很相信逆向工程,所以本文有很多链接到React16.6.0的源码链接。
无疑,本文有很多内容。所以即使你不能立刻理解这些内容,也不必感觉到有压力。 多花点时间,一切都是值得的。请注意,你不必使用过React,本文只是单纯地讲些Reac的内部实现。
基础知识
接下来,全文讲解都会使用以下的demo: 一个数字累加器,点击按钮,数组增加1.
以下是代码实现:
class ClickCounter extends React.Component {
constructor(props) {
super(props);
this.state = {count: 0};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState((state) => {
return {count: state.count + 1};
});
}
render() {
return [
<button key="1" onClick={this.handleClick}>Update counter</button>,
<span key="2">{this.state.count}</span>
]
}
}
例子很简单,ClickCounter组件的render函数里返回了两个元素button和span。只要我们点击button,组件状态会更新,从而页面的上的span元素的数字会累加。
在调解过程中,react执行了一系列的活动。以下列出了在demo第一次渲染和状态更新后,react一系列的高阶操作。 -更新ClickCounter组件的state count -检索并且比较ClickCounter的子类和子类属性 -更新span元素的属性 还有一些其他的活动也在调解的过程中执行比如调用时间周期的方法或者更新refs。所有这些活动在fiber中统一被成为work。work的类型通常依据React元素类型的不同而不同。例如,对于一个class类型的组件,React需要创建一个实例,而function类型的组件则不需要。如你所知,在react中我们有许多类型的元素:class类型组件,function类型组件,宿主组件(dom 节点), portals 等等。React 元素类型是根据被传入createElement函数的第一个参数来定义的。createElement函数通常用在render方法里用来创建一个元素。
在我们开始解释这些活动和探索fiber核心算法之前,先让我们来熟悉一下React源码中使用的数据类型。
从React元素到Fiber节点
React中的每一个组件都对应着一个UI呈现,我们把它叫做view或者template(模板),它是从render方法中返回的。以下是我们ClickCounter组件的模板。
<button key="1" onClick={this.onClick}>Update counter</button>
<span key="2">{this.state.count}</span>
React元素
当一份模板经由JSX编译器编译后,我们得到了一系列的React元素。React组件的render方法实际返回的就是这些React元素而不是HTML。我们并不一定非要写JSX,所以ClickCounter组件的render方法也可以写成:
class ClickCounter {
...
render() {
return [
React.createElement(
'button',
{
key: '1',
onClick: this.onClick
},
'Update counter'
),
React.createElement(
'span',
{
key: '2'
},
this.state.count
)
]
}
}
render方法会调用React.createElement方法,然后我们就会得到如下的数据结构:
[
{
$$typeof: Symbol(react.element),
type: 'button',
key: "1",
props: {
children: 'Update counter',
onClick: () => { ... }
}
},
{
$$typeof: Symbol(react.element),
type: 'span',
key: "2",
props: {
children: 0
}
}
]
React用$$typeof 属性来唯一识别React元素。type,key,props用来描述该元素。这些值都是传入React.creaateElement参数。我们可以注意到,React在以children的形式来表示文字内容的,并且onClick事件也是button元素的props之一。这里还有一些其他的属性比如ref,暂不在本文讨论范围中。
ClickCounter 的react 元素是没有任何props和key的:
{
$$typeof: Symbol(react.element),
key: null,
props: {},
ref: null,
type: ClickCounter
}
Fiber 节点
在reconciliation过程中,每一个render方法返回的react元素都会被合并到fiber节点树。每一个react元素都对应着一个fiber节点。不像react元素,fiber节点不会在每次一次渲染的时候被重新创建。fiber是不可变的数据结构,其中包含着组件的状态和DOM。
正如我们上文所说的那样,根据React 元素类型的不同,fiber会执行不同的活动。比如,ClickCounter会调用声明周期函数和render函数,然而,对于span的宿主组件(DOM) 会执行DOM变化。所以,简而言之,每一个React元素都会被转化为一个有着相应类型的fiber节点。不同类型意味着需要执行不同的操作。
你可以把fiber想成一种数据结构,它代表着要做的work,也就是,a unit of work。此外,Fiber还提供了一种简单的方法去追踪,安排,暂停和终止work。
当一个React元素第一次被转化为一个fiber节点的时候,React把该元素的数据传入到createFiberFromTypeAndProps 函数来生成一个fiber。在最终的更新结果里,react会复用fiber节点并且根据React元素的数据只更新需要更新的属性。根据key属性,React可能会从层级中移除这个节点如果render函数不再返回该React元素。
查看 ChildReconciler 方法,这里列出了React对现存的fiber节点的一系列操作以及对应方法。
React为每一个React元素都创建了一个fiber节点,因此我们获得了一个与React节点树相对应的fiber节点树。 在我们的demo中,ClickCounter的fiber节点看起来如下:
所有的fiber节点都通过一个有着child,sibiling 和return属性的链表链接起来。如果想知道更多细节,可以查看这篇文章: The how and why on React’s usage of linked list in Fiber
current树和workInProgress树
第一次渲染之后,React会得到一个反映着应用状态的fiber树,以此来渲染UI。这棵树通常被称作Current。当React渲染的时候,会创建一个workInProgress
树,这棵fiber树反应的是未来的UI状态。
根据 workInProgress
树上所有的fiber节点,所有的work都会被执行。当然React遍历current tree的时候,对每一个现存的fiber节点都会创建一个与之对应的节点用来构成workInProgress
树。这个节点是根据render方法中返回的React元素来创建的。一旦更新被执行并且所有相关的work都完成,React将会有另一棵备用树时刻准备呈现UI到屏幕上。一旦 workInProgress
树在屏幕上渲染出来了,它就成为了 current
树。
一致性是React的核心原则之一。React总是一次性更新DOM -- 它不会展示部分的结果。 workInProgress
树就像是一份草稿--它对用户不可见,所以React可以先处理所有的组件,之后再把最终的结果呈现到屏幕上。
在源码中,你会看到很多方法既从 current
树又从 workInProgress
树 获取fiber节点。这是其中一个这类函数的方法签名。
function updateHostComponent(current, workInProgress, renderExpirationTime) {...}
每一个fiber节点的alternate域都存着一个索引,指向另一棵树上的相对应的备用节点。current 树上的节点可以指向 workInProgress
树,反之亦然。
Side-effects
我们可以把React中的组件看作一个使用state和props计算UI呈现的函数。每一个如改变DOM或者调用生命周期的动作都应该被看作是一种side-effect(副作用)。或者,简单说,一种effect(影响)。Effects 在React官方文档中有如下介绍:
你很可能曾经在React组件中操作过数据的获取,订阅,或者人工改变DOM。我们把这些操作称为side-effects,也就是副作用,或者简称effects(影响)。因为他们可能影响其他的组件并且不能在渲染中结束。
你可以看到state和props的更新是如何造成side-effects的。并且由于应用effects是一类work,一个fiber节点是除了updates之外的一种便捷机制,可用来跟踪effects。每一个fiber节点可以有与之相应的effects。他们在
effectTag
域被编码。所以Fiber 中的effects主要定义了update执行过后,需要在实例上完成的work。对于宿主组件(DOM 元素),work包含了增加,更新和移除元素。对于class组件,React可能需要去更新refs并且调用
componentDidMount
和componentDidUpdate
生命周期方法。 还有一些其他的effects对应着不同类型的fiber节点。
Effects list
React处理updates非常快并且为之采用了一些有意思的技巧。其中一个技巧就是建立了一个有着effects的线性fiber节点list,以此实现快速遍历。遍历线性list比起遍历一棵树要快的多。并且没有必要花时间在没有side-effect 的节点上。
这个list的目的是标记有DOM更新或者其他effects的节点。这个list是 finishedWork tree
的一个子集并且用
nextEffect
属性表示。而不是像在current
树和 workInProgress
树用child
属性来表示。
Dan Abramov 给出了一个对于effects list的比喻。他把effects list认为是一个圣诞树🎄,这棵圣诞树用书上的灯把所有的有影响的节点 绑定在一起。为了可视化这种比喻,让我们把下图想象成一个充满了fiber节点的树,那些高亮的节点代表有一些work需要完成。举个例子,我们的更新导致:C2即将被插入DOM, d2和c1即将改变属性,b2即将调用一个生命周期方法。这个effect list将会把他们链接到一起,这样,React就可以跳过稍后跳过其他节点:
你可以看到带有effects的节点是如何连接到一起的。当遍历节点的时候,React用了 firstEffect指针来标记list从何处开始。所以上图可以简化为下图所示:
fiber树的根 (Root of the fiber tree)
每一个React应用都有一个或者多个作为container的DOM元素。在我们的例子里,id为container的div元素就是我们的container。
const domContainer = document.querySelector('#container');
ReactDOM.render(React.createElement(ClickCounter), domContainer);
React 为每一个container都创建了一个fiber root 对象。你可以用如下方法来访问它:
const fiberRoot = query('#container')._reactRootContainer._internalRoot
这个fiber root就是React保存到fiber树的引用的地方。这份引用被存在了fiber root的current 属性上。
const hostRootFiberNode = fiberRoot.current
fiber树的根fiber节点是有一个非常特殊的类型,叫做HostRoot
。它是内部创建的并且作为你最高层级组件的父组件。可以通过HostRoot fiber
节点的stateNode属性返回到FiberRoot
fiberRoot.current.stateNode === fiberRoot; // true
你可以通过访问最高层级的fiber root的 HostRoot
fiber节点探索fiber树。或者你可以以如下方式从组件实例上获取一个独立的fiber节点:
compInstance._reactInternalFiber
Fiber节点的构造 (Fiber node structure)
让我们现在来看看 ClickCounter
组件的fiber节点结构:
{
stateNode: new ClickCounter,
type: ClickCounter,
alternate: null,
key: null,
updateQueue: null,
memoizedState: {count: 0},
pendingProps: {},
memoizedProps: {},
tag: 1,
effectTag: 0,
nextEffect: null
}
还有 span DOM
元素的fiber节点结构:
{
stateNode: new HTMLSpanElement,
type: "span",
alternate: null,
key: "2",
updateQueue: null,
memoizedState: null,
pendingProps: {children: 0},
memoizedProps: {children: 0},
tag: 5,
effectTag: 0,
nextEffect: null
}
fiber节点上有很多的属性。alternate
, effectTag
和 nextEffect
的用意已经在之前讲过了。现在我们来看看其他属性是干嘛的。
stateNode
stateNode属性存放着组件类实例的,或者DOM 节点的或者其他关于fiber节点的React元素类型的引用。总的来说,这个属性就是用来存放一个fiber节点的本地state的。
type
type定义了fiber节点是一个函数组件或者类组件。对于类组件,type 指向构造函数。对于DOM元素,它描述了HTMLtag。我通常用这个属性来了解与该fiber节点相关联的元素是什么。
tag
tag定义了fiber的类型。tag用在协调算法里决定做什么类型的work。正如之前提到的,对于不同类型的React元素,work也是不同的。createFiberFromTypeAndProps 方法把一个React元素转换为相应类型的fiber节点。在我们的例子里, ClickCounter
的tag属性为1,也就意味着 ClassComponent
。对于 span
元素,它的tag是5,代表 HostComponent
.
updateQueue
一个有着状态更新,回调函数和DOM更新的队列。
memoizedState
memoizedState 是用来创建输出的fiber的state。当处理更新时,它反映了当前渲染在屏幕上的state。
pendingProps
pendingProps是fiber的props,用来在之前的渲染基础上创建输出。
key
key属性是有着一组子元素的独一无二的标记,可以帮助React搞清楚哪些元素变化了,哪些元素被添加到list或者移除出list。这涉及到React的“lists and keys”功能。
你可以在这里看到一个完整的fiber节点的结构。在上边的解释中我省略了一堆属性。特别是,我跳过了构建起一棵树的数据结构的指针child
, sibling
和 return
。想要了解更多这部分的内容,可以查阅之前的文章。并且有一类属性,如:expirationTime
, childExpirationTime
和 mode
是专门用于Scheduler
的。
General algorithm
React在两个主要的阶段执行work:render阶段和commit阶段。
在第一个render时期,React根据setState和React。render来执行组件更新并且弄清哪些更新需要被作用到UI上。如果这是首次render,React给每一个render方法返回的元素创建一个新的fiber节点。在之后的更新中,现存的React元素的fiber节点会被复用并且更新。这个时期结束后,会获得一棵标记了side-effects的fiber节点。这些effects描述了在接下来的commit
阶段需要完成的work。在render阶段,React获得一个标注了effects的fiber树并把这些effects应用到实例上。在这个时期,会遍历effect list并且执行DOM更新和一些其他的用户可见的改变。
有一点需要很重要,就是我们需要知道在第一次render时期的work可以被异步执行。 根据时间是否宽裕,React可以处理一个或多个fiber节点,然后把work暂存投入到某些其他事项中去。然后,React再回来做它未完成的work。但有些时候,React需要放弃一些已完成的部分work再从头开始做。之所以这些work可以被暂停是因为再这个阶段所做的work并不造成用户可见的改变。相反,接下来要介绍的commit
阶段,永远都是同步的。这是因为在这个阶段所做的work会产生用户可见的更新。比如,DOME更新。这就是为什么React会把这些work单独放到一个阶段去做。
调用声明周期函数是React会执行的work之一。一些方法会被在 render
阶段调用,另一些方法则会在 commit
阶段调用。这里列出了一些会在 render
阶段调用的声明周期方法:
- [UNSAFE_]componentWillMount (deprecated)
- [UNSAFE_]componentWillReceiveProps (deprecated)
- getDerivedStateFromProps
- shouldComponentUpdate
- [UNSAFE_]componentWillUpdate (deprecated)
- render
正如你知道的,一些陈旧的需要在 render
阶段被执行的生命周期方法已经被标记了 UNSAFE
(从React16.3开始)。这些方法在文档中已被成为遗留生命周期了。他们再16.x版本会被作废,并且没有 UNSAFE 前缀的生命周期会被从17.0版本移除。你可以在这里了解更多建议的迁移方法。
你是否对移除的原因很好奇呢?
这是因为render
阶段 并不产生如DOM更新这样的side-effects. React 可以对组件进行异步更新处理。(可能甚至是多线程来处理)。然而打上了 UNSAFE
标的生命周期通常会被误解并有些错误使用。开发人员比较倾向于在这些生命周期函数里做一些有着side-effects的操作。这种做法可能会在新的异步渲染方法中造成一些问题。尽管只有没有标记 UNSAFE
前缀的生命周期方法会被移除,但即便有着 UNSAFE
标,他们也会在即将到来的Concurrent Mode(并发模式,你可以选择不使用)中导致一些问题。
这些是在commit
阶段执行的一些列生命周期方法:
- getSnapshotBeforeUpdate
- componentDidMount
- componentDidUpdate
- componentWillUnmount
因为这些方法是在同步的commit
阶段执行,他们可能会包含一些side-effects并且操作DOM。
好了,我们现在有了足够的知识背景来一窥generalized algorithm(统一算法)。看看它是如何遍历树并且执行work的。让我们一探究竟吧。
Render 阶段 (Render phase)
协调算法是调用 renderRoot 方法从最高层级的 HostRoot
fiber 节点开始的。然而,React会跳过已经处理过的fiber节点,找到有着未完成work的fiber节点。比如, 如果你在组件树深层调用 setState
方法,React会从root节点开始但它会很快跳过父级,径直找到调用 setState
方法的组件。
work loop 的关键部分 (Main steps of the work loop)
所有的fiber节点都在work loop里被处理。这里是loop的同步部分:
function workLoop(isYieldy) {
if (!isYieldy) {
while (nextUnitOfWork !== null) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
} else {...}
}
上边的代码,nextUnitOfWork
保存了去到有work需要完成的 workInProgress
树上的fiber节点的引用。当React遍历树上的fiber节点的时候,他会用这个变量记录还有这未完成work的fiber节点。当当前节点被处理后,这个变量要么包含着去到下个fiber节点的引用要么指向null。如果是null,那么React会退出work loop并且准备commit所有变化。
这里有4个主要的用来遍历树,初始化或结束work的方法:
通过看下边关于遍历fiber树的动画,我们来演示一下这些方法如何被应用的。对这几个方法,我用了简化的实现方法来展示我们的demo。每一个方法都把一个fiber节点当做入参并处理它。并且当React沿着树向下遍历的时候,你可以看到被激活的fiber节点在变化。你可以清楚地看到,算法是如何从一个树杈走到另一个的。他会先完成子节点的work,之后才会移到父级。(深度优先DFS遍历)
注意径直的连结代表着兄弟节点,弯曲的连结代表着子节点。比如,b1没有子节点,b2的只有一个子节点,c1。
此视频,你可以暂停回访并观察现在的节点和方法的状态。抽象来说,你可以把begin(开始)看作走进一个组件,complete(完成)看作走出该组件。这里,有详细的例子和代码实现。 我们开始从performUnitOfWork
和 beginWork
方法讲起:
function performUnitOfWork(workInProgress) {
let next = beginWork(workInProgress);
if (next === null) {
next = completeUnitOfWork(workInProgress);
}
return next;
}
function beginWork(workInProgress) {
console.log('work performed for ' + workInProgress.name);
return workInProgress.child;
}
performUnitOfWork
方法接受一个workInProgress
树的fiber节点并且通过调用beginWork
函数开始。
beginWork
函数里会完成改fiber节点所需要完成的所有work。为了简化演示,我们只是在此处console.log了一下。beginWork
函数总是会返回一个指向下一个子节点或者null的指针。
如果这里有下一个子节点,该子节点会被赋值给 workLoop
方法里的nextUnitOfWork
变量。然而,如果没有子节点,React知道自己遍历到了树杈的末尾所以他会结束当前节点。
function completeUnitOfWork(workInProgress) {
while (true) {
let returnFiber = workInProgress.return;
let siblingFiber = workInProgress.sibling;
nextUnitOfWork = completeWork(workInProgress);
if (siblingFiber !== null) {
// If there is a sibling, return it
// to perform work for this sibling
return siblingFiber;
} else if (returnFiber !== null) {
// If there's no more work in this returnFiber,
// continue the loop to complete the parent.
workInProgress = returnFiber;
continue;
} else {
// We've reached the root.
return null;
}
}
}
function completeWork(workInProgress) {
console.log('work completed for ' + workInProgress.name);
return null;
}
你可以看到这个方法的核心是有个大大的while
循环。当 workInProgress
的fiber节点没有子节点的时候,React就会进入这个方法里。当当前fiber节点完成了work后,他会检查是否存在兄弟节点。如果有,React会退出该方法并且返回兄弟节点的指针。兄弟节点会被赋值给nextUnitOfWork
变量, React将会开始执行以该兄弟开始的节点的分支。需要重点理解的是,在这个时候,之前的兄弟节点只有已完成的work,父节点还未完成work。只有当所有分支/树杈上的子节点完成了,才会开始回溯到父级节点去完成work。
正如你可以从代码实现里看到的那样, completeUnitOfWork
在迭代里用的比较多。然而主要的活动发生在beginWork
和 completeWork
方法里。在接下来的文章里,我们会学习在ClickCounter
组件和span节点上,React的beginWork
和 completeWork
都做了什么。
Commit 阶段 (Commit phase)
这个阶段是由 completeRoot 方法开启的。commit 阶段你就是React更新DOM并且调用之前和之后的更改生命周期方法。
当React进入这个阶段,会有2棵树和一个effects list。第一棵树代表了屏幕上目前的渲染状态。还有一棵备用树在render阶段构建出来。第二棵树在源码中被称为finishedWork
或者 workInProgress
,它代表了即将被反映在屏幕上的未来状态。这棵备用树通过 child
和 sibling
指针被连接到第一棵树。
之后,会有一个effects list --- 它是finishedWork
树节点上的一个子集,每个effects list上的节点通过nextEffect
指针链接起来。我们之前讲过,effects list是render阶段的产物。render阶段的主要目标就是确定哪一个节点需要被插入,更新,或者删除。并且哪个组件需要调用它的生命周期方法。这些就是由effects list来告诉React的。effects list上的所有节点就是commit阶段需要遍历的节点。
为了可以debug,current 树可以通过fiber root的current属性被访问到。通过访问current tree的HostFiber节点的alternate属性,我们可以访问到finishedWork树。
在commit阶段主要跑的方法就是 commitRoot. 它主要做了下边的事:
- 在有着
Snapshot effect
标记的节点调用getSnapshotBeforeUpdate
生命周期方法 - 在有着
Deletion effect
标记的节点调用componentWillUnmount
生命周期方法 - 完成所有的DOM 插入,更新和删除
- 把
finishedWork
更新为current
树 - 在有着
Placement effect
标记的节点调用componentDidMount
生命周期方法 - 在有着
Update effect
标记的节点调用componentDidUpdate
生命周期方法
在调用 pre-mutation(前置变化)方法getSnapshotBeforeUpdate
, React会commit树上所有的side-effects。这个操作有两个阶段,第一个阶段完成所有的DOM(host) 插入,更新,删除和ref的unmount。然后React把finishedWork
树赋值给FiberRoot。把workInProgress
树标记为current
tree。这一系列的操作都在commit阶段的第一周期,所有之前的树在componentWillUnmount
阶段还是current
树,在componentDidMount/Update
阶段,finishedWork
树成为current
树。在第二个阶段React调用了所有的生命周期方法和ref的回调。这些方法被单独拎出来执行,以至于所有的替换,更新和删除操作在整棵树上被执行。
下边列出的就是执行我们上一段落所讲的所有操作的方法:
function commitRoot(root, finishedWork) {
commitBeforeMutationLifecycles()
commitAllHostEffects();
root.current = finishedWork;
commitAllLifeCycles();
}
它的每一个子方法都对effects-list的节点做了遍历,检查effects类型。然后对应的方法会执行对应的effect标记的节点。
前置变化的生命周期方法 (Pre-mutation lifecycle methods)
从以下的示例可以看出,代码遍历了effects树并且检查每一个节点是否有 Snapshot
effect:
function commitBeforeMutationLifecycles() {
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
if (effectTag & Snapshot) {
const current = nextEffect.alternate;
commitBeforeMutationLifeCycles(current, nextEffect);
}
nextEffect = nextEffect.nextEffect;
}
}
对于一个类组件,有 Snapshot
effect意味着需要调用getSnapshotBeforeUpdate
生命周期方法。
DOM 更新
commitAllHostEffects 是React 执行DOM更新的方法。这个方法主要定义了需要对一个node进行操作的类型并且执行这种类型的操作。
function commitAllHostEffects() {
switch (primaryEffectTag) {
case Placement: {
commitPlacement(nextEffect);
...
}
case PlacementAndUpdate: {
commitPlacement(nextEffect);
commitWork(current, nextEffect);
...
}
case Update: {
commitWork(current, nextEffect);
...
}
case Deletion: {
commitDeletion(nextEffect);
...
}
}
}
有意思的是,在commitDeletion
方法中, React调用了componentWillUnmount
方法作为删除过程中的一部分。
后置变化的生命周期方法 (Post-mutation lifecycle methods)
React 在 commitAllLifecycles 方法里调用所有剩余的生命周期方法的地方: componentDidUpdate
和 componentDidMount
。
终于讲完了。如果有对这片文章有什么想法或者有什么疑问,欢迎在评论区留言。欢迎浏览本系列的下一篇文章: 深入理解state和props在React中的更新。我也写了很多其他的文章来深入讲解scheduler,子类协调过程和effect lists如何构建的。我也在计划出一个视频,以本文章内容为基础来展示如何debug应用。