Inside Fiber: 深度解析react新的协调算法

31次阅读

共计 11447 个字符,预计需要花费 29 分钟才能阅读完成。

React 是用于建立用户交互界面的 JavaScript 库。它的核心机制是跟踪组件的状态并且更新显示到屏幕上,这个过程被称为协调(reconciliation)当组件的 state 或者 props 发生改变时,我们使用 setState 方法并且进行检查,重新渲染 UI。
React 的文档提供了对这个机制的讲解:React 元素,生命周期函数和 render 方法的作用,以及 diff 算法在子组件的应用。由 render 函数返回的 react 元素被称为“virtual DOM”。这个词早起常被用来解释 react 的工作原理,但是这个词经常引起误解,并且已经不被 react 的官方文档所使用。在这篇文章我会继续用它表示 react 的元素树。
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>
]
}
}
正如你所见,这是一个简单的组件,返回 button 和 span 两个子元素。只要一点击按钮,组件的状态就会更新,反过来组件德状态就会更新到 span 元素。在这步协调算法中 react 进行了多个步骤。比如下面是在第一次渲染和状态更新之间的 React 高级步骤:

更新 ClickCounter 组件状态中的 count 属性
重新获取并比较 ClickCounter 组件的子元素和 props
更新 span 元素的 props

在协调算法阶段还有其他活动如生命周期函数和更新 refs。所有的这些步骤在 Fiber 中统一称为“工作(work)”。这些工作的种类通常取决于 React 元素的种类。比如:对于类组件,React 需要创建一个实例,而函数式组件则不需要。正如你所知,在 React 中有许多元素类型,如类组件和函数式组件,主机元素(host elements)和 portals 等等。React 组件的类型由创造组件时函数第一个单词决定。这个函数通常是在 render 函数中创建元素。在我们开始探索 fiber 算法的前,先熟悉一下 React 内部的数据结构。
2. 从 React 元素到 Fiber 节点
Every component in React has a UI representation we can call a view or a template that’s returned from the render method. Here’s the template for our ClickCounter component:React 的每个组件都有一个由 render 函数返回的我们称为视图或者模板的 UI。下面是 ClickCounter 组件的模板:
<button key=”1″ onClick={this.onClick}>Update counter</button>
<span key=”2″>{this.state.count}</span>
2.1. React 元素
当一个模板进入 JSX 的编译器后,输出的是一些 React 元素。render 函数返回的是就是这些而不是 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 元素。接下来我们就有了属性的类别,key 和 props 来描述这个元素。这些值来自于你传给 React.createElement 函数的。请注意 React 是怎么把文字内容表现为 span 和 button 节点的子元素的。点击事件时怎么成为 button 元素的 props 的。React 还有其他属性像 refs,但这些超过了本文章讨论的内容。The React element for ClickCounter doesn’t have any props or a key:ClickCounter 不会拥有任何的 props 或者 key:
{
$$typeof: Symbol(react.element),
key: null,
props: {},
ref: null,
type: ClickCounter
}
2.2. Fiber 节点
在协调算法阶段,从 render 方法中返回的每一个 React 元素合并成一个 fiber 节点树。每一个 React 元素都有与之对应的 fiber 节点。跟 React 元素不同的是,fiber 并不是每次 render 都会重新创建的。fiber 就是保持组件状态和 DOM 结构的可变的数据结构。
我们先前讨论过 React 根据元素的种类的不同表现不同的活动。在我们的简单的应用中,对于类组件我们称 ClickCounter 为生命周期方法。对于 render 方法,span 元素组件提供了 Dom 变化的功能。所以每一个 React 元素转化成描述需要执行的活动的 Fiber 节点。fiber 结构同时也对追踪,安排,暂停和遗弃这些功能提供了方便的方法。
当 React 元素第一次转化成 fiber 节点时,React 在 createFiberFromTypeAndProps 方法中将元素的数据编译成 fiber。在接下来的更新中,React 会重复使用 fiber 节点,并更新需要更新的元素的对应的 fiber 节点。React 还会根据 key 转化节点层级或者删除在 render 方法中没有返回的 React 元素的对应节点。
因为 React 的每个 fiber 都有对应的 React 元素,同时又有这些元素组成的树,所以我们也有 fiber 树,在我们简单的应用中它是这样的:
所有的 fiber 节点通过由 child,sibling 和 return 组成的 fiber 节点连接的列表。
3. Current and work in progress trees
在首次渲染之后,React 生成一颗表示应用状态并用于渲染 UI 的 fiber 树。这颗树通常被称为 current。当 React 开始进行更新工作时会生成一颗称为 workInProgress 的树,这颗树表示将要显示到屏幕上的状态。
fibers 上展示的所有的工作都是来自于 workInProgress 树的。当 React 检查 current 树,每个存在的 fiber 节点都会生成一个替代的节点,这些节点构成 workInProgress 树这些节点是由 render 函数返回的 React 元素生成的。一旦更新和相关的工作都完成了,React 将会有另一颗树准备显示到屏幕上。当 workInProgress 树显示到屏幕上时就变成了 current 树。
React 的核心准则之一就是连续性。React 通常是一次性更新 DOM 的——它不会显示部分结果。workInProgress 树不会在用户前显示就如同是草稿一样,所以 React 可以先对所有的组件进行检查更新,然后改变 DOM 结构。In the sources you’ll see a lot of functions that take fiber nodes from both the current and workInProgress trees. Here’s the signature of one such function: 在源代码里面你可以看到有许多方法会从 current 树和 workInProgress 树上拿取 fiber 节点。下面就是这样的一个方法:

function updateHostComponent(current, workInProgress, renderExpirationTime) {…}
4. 副作用
我们可以把 React 组件想象为用来计算 state 和 props 并展示 UI 界面的函数。任何其他的像改变 DOM 和使用生命周期函数可以被称作副作用,或者简单的称为作用。作用也在文档里被提及:
你可能之前通过网络获取或者订阅获取数据,又或者在 React 组件里手动的改变 DOM。我们称之为副作用,因为他们会影响其他的组件并且不能再渲染期间完成。
你可以看到大部分的 state 和 props 是如何因为更新产生副作用的。由于使用副作用也是工作的一种,fiber 节点也是更重副作用的有效机制。每一个节点可以拥有与他联系的副作用,他们被编码到一个叫做作用标签(effectTag)的地方。所以 Fiber 里的副作用定义了在更新完成之后实例需要做的工作。对于 Dom 元素这些工作由增加,更新或者删除元素组成。对于类组件,则是需要更新 refs 和发起 componentDidMount 和 componentDidUpdate 生命周期函数。还有其他类型的 fibers 联系的副作用。
5. 作用列表
React 更新过程非常快,React 通过采取一些有趣的技术到达这样的高性能。其中之一就是创建了一个带 effects 的线性列表的 fiber 节点,可以快速迭代。迭代线性列表比树结构快的多,不需要花费时间在没有副作用的节点上了。这个列表的目的是标记具有 DOM 更新和其他作用相联系的节点。这个列表是 finishedWork tree 的子集,并且使用 nextEffect 属性代替在 current 和 workInProgress 树种使用的子属性。
Dan Abramov 对 effect 树举了个例子。他喜欢把 effect 树想象为一颗圣诞树,用圣诞灯把所有的 effect 节点联系起来。让我们来看下面这张图,高亮显示的是需要工作的 fiber 节点。比如,我们的更新把 c2 插入到 Dom 结构中,改变 d2 和 c1 的属性,触发 b2 的生命周期的属性。effect 列表会把他们连起来,那么 react 后面的处理就可以跳过其他节点了:
你可以看到有作用的节点是如何连在一起的。为了遍历这些节点,React 使用 firstEffect 来指出这个列表从哪里开始,就像下面这样:
6. fiber 树的跟节点
Every React application has one or more DOM elements that act as containers. In our case it’s the div element with the ID container. 每一个 React 应用都有一个或者多个 Dom 元素用来作为容器。在我们的例子就是带有 ID 属性的 div 元素。
const domContainer = document.querySelector(‘#container’);
ReactDOM.render(React.createElement(ClickCounter), domContainer);
React creates a fiber root object for each of those containers. You can access it using the reference to the DOM element:React 为每一个容器都创建了一个 fiber 根对象。你可以通过这些参考看到这个 DOM 元素:
const fiberRoot = query(‘#container’)._reactRootContainer._internalRoot
这个 fiber 跟对象就是 React 控制 fiber 树的参考。它保存在 fiber 根对象中的 current 属性中。
const hostRootFiberNode = fiberRoot.current
fiber 树开始于一个特别的 fiber 种类就是 HostRoot。它有内部创建并且就像是其他组件的父元素。HostRoot 元素节点可以通过 stateNode 属性返回 FiberRoot:

fiberRoot.current.stateNode === fiberRoot; // true
你可以通过 fiber 根对象探索 fiber 树,或者可以对一个组件的 fiber 节点像下面那样操作:
compInstance._reactInternalFiber
7.Fiber 节点的结构
Let’s now take a look at the structure of fiber nodes created for the ClickCounter component 让我们看看有 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 元素的:
{
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:是一个组件的实例,与 fiber 相联系的 Dom 节点或其他 React 元素种类。

type:定义了与 fiber 相关的函数或种类。对于类组件,他指向构造函数,对于 Dom 元素它指向 HTML 标签。我经常使用它了解一个 fiber 节点相关的元素。

tag:定义了 fiber 的类型。它被用在协调算法中决定应该去做那个工作单元。就像之前提过的,工作单元的种类由 React 元素决定。createFiberFromTypeAndProps 函数映射了 React 元素对 fiber 种类。在我们的应用中,ClickCounte 的 tag 属性是 1,这代表是类组件而 span 元素是 5 代表了 HostComponent(需要改变外观的组件)。

updateQueue:保存着状态更新,回调函数和 Dom 更新的队列。

memoizedState:保存 fiber 用来创建输出的状态。当进行更新时,它反映了现在在屏幕上渲染的内容。

memoizedProps:保存先前渲染时的 props。

pendingProps:在 React 元素中已经从最新数据更新的,需要传递给子组件和 Dom 元素的 props。

key:帮助 React 识别在一组子元素中哪一个元素被改变,增加和删除的唯一标记。

你可以在这里发现一个复杂的节点,我已经省略了先前已经解释过的一些属性。而像 expirationTime,childExpirationTime 和 mode 与调度有关。
8.General algorithm
React 的工作可以分为两个阶段:render 和 commit。
在第一个 render 阶段,React 组件通过 setState 和 React.render 方法分辨哪些需要在 UI 中更新。如果是首次渲染,则 React 会为每一个从 render 函数中返回的元素创造一个新的 fiber 节点。React 会对于其他的已经存在的 React 元素再次使用并更新。这个阶段的结果就是生成一颗带有副作用的 fiber 节点数。这个阶段的描述的 effetcs 要在接下来的 commit 阶段完成。
我们要明白在 render 阶段的工作可以是异步的。React 可以根据可用的时间来进行一个或多个工作单元,然后停止并保存已完成的工作单元,进行其他的事件处理。当其他的事件处理完成后再执行剩下的工作单元。不过有时候,可能需要丢弃已经完成的工作单元,从头开始。这些暂停使得界面可以对用户的操作进行反应,例如 dOM 更新。然而,commit 阶段则是同步的,这是因为这个阶段的工作改变了呈现给用户的界面。Calling lifecycle methods is one type of work performed by React. Some methods are called during the render phase and others during the commit phase. Here’s the list of lifecycles called when working through the first render phase: 生命周期函数就是 React 的一种工作类型。一些方法在 render 阶段被使用,另外一些在 commit 阶段被使用。下面是在 render 阶段使用的生命周期函数的列表:

[不安全]componentWillMount (将被弃用了)
[不安全]componentWillReceiveProps (将被弃用了)
getDerivedStateFromProps
shouldComponentUpdate
[不安全]componentWillUpdate (将被弃用了)
render

正如你所见,从 16.3 版本开始一些以前的生命周期函数在 render 阶段被标记为不安全。它们现在在文档中称为以前(legacy)的生命周期函数。你对这其中的原因好奇嘛?
就像我们刚刚学习的所说的,在 render 阶段并不产生像 DOM 更新这样的副作用,React 可以进行异步更新(甚至可以多线程工作)。然而在上面标记不安全的生命周期函数通常被误解以及被不合理的使用。开发者通常把有副作用的代码放到这些函数中,可能引起新的异步的渲染的问题。
下面是在 commit 阶段执行的生命周期函数:

getSnapshotBeforeUpdate
componentDidMount
componentDidUpdate
componentWillUnmount

因为在这些函数在 commit 阶段同步执行,所以他们包含了副作用并且与 DOM 密切相关。
9. Render 阶段
协调算法总是用 renderRoot 函数从最开始的 HostRott fiber 节点开始。然而 React 会跳开已经处理过的 fiber 节点知道发现有未处理的工作单元。比如,你在组件树的深处使用 setState,React 会从第一个组件开始,但是会快速的跳过父组件达到调用 setState 方法的组件。
工作循环的主要步骤
所有的 fiber 节点都在工作循环中进行。下面是一个同步循环的实现:
function workLoop(isYieldy) {
if (!isYieldy) {
while (nextUnitOfWork !== null) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
} else {…}
}
在上面的代码中,nextUnitOfWork 保存了从 workInProgress 树中的fiber节点要做的工作。当react遍历fiber树的时候,它使用这个变量来发现是否有另外有未完成工作的fiber节点。当当前的fiber节点运行结束时,这个变量要么保存了下个fiber节点或者null。到那个时候React准备进行commit了。There are 4 main functions that are used to traverse the tree and initiate or complete the work: 有四个主要的函数被用到遍历树和初始化或者完成工作单元:

performUnitOfWork
beginWork
completeUnitOfWork
completeWork

来看看下面的动画来演示在遍历fiber树时这些函数是怎么工作的。我简化了这些函数在这个demo中的运行过程。每个函数都在一个fiber节点运行,当React你可以看见现在活动中fiber节点的改变。你可以清楚的看见算法是怎样从一个分支到另一个分支的。它首先从子元素,然后到父元素。
注意垂直的直线连接表示兄弟元素,横向的连接代表父子,比如,b1没有子元素,而被b2有一个子元素c1Let’s start with the first two functions performUnitOfWork and beginWork: 让我们先从 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 开始工作。这个函数将会启动所有的需要fiber运行的活动。为了达到演示的目的,我们简单的到引出fiber的名字来表明这个工作已经完成。beginWork 总是返回指向下一个字元素的指针或者null。
如果有下一个子元素,在 workLoop 函数中赋值给 nextUnitOfWork 这个变量。若果没有子元素,React 就知道到达了这个分支的底部元素,那么就可以完成现在这个节点。一旦当前这个节点结束时,就会开始兄弟元素或回到的父元素的工作单元。这项工作在 completeUnitOfWork 函数中完成:
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;
}
You can see that the gist of the function is a big while loop. React gets into this function when a workInProgress node has no children. After completing the work for the current fiber, it checks if there’s a sibling. If found, React exits the function and returns the pointer to the sibling. It will be assigned to the nextUnitOfWork variable and React will perform the work for the branch starting with this sibling. It’s important to understand that at this point React has only completed work for the preceding siblings. It hasn’t completed work for the parent node. Only once all branches starting with child nodes are completed does it complete the work for the parent node and backtracks. 你可以看见这个函数其实是一个大的完整的循环。
10 .Commit phase
这个阶段由 completeRoot 函数开始。在这个阶段 React 更新 Dom 和触发 mutation 生命周期函数。当进入这个阶段,React 有两个数结构和 effect 列表。第一个树结构是现在渲染到屏幕上的 state。另外一个是在 render 阶段用来替换的是结构。它在源代码中被称为 finishedWork 或者 workInProgress。
然后说道 effects 列表——finishedWork tree 与 nextEffect 指针相关联的节点的子集。记住 effects 列表是 render 阶段的成果。整个 render 阶段的意义是决定哪些节点需要插入,更新,或者删除,哪些组件需要调用他们的生命周期函数。这就是 effect 要告诉我们的。
在 commit 阶段运行的主要函数是 commitRoot。下面的基本上就是它的工作:
- 在有快照 effect 的标签的节点上调用 getSnapshotBeforeUpdate 生命周期函数。

在有删除 effect 的标签的节点上调用 componentWillUnmount 生命周期函数。
实现所有的 DOM 的插入,更新和删除。
把 finishedWork 树设置为当前树结构。
列表项目
在有 Placement effect 的标签的节点上调用 componentDidMount 生命周期。
在有更新 effect 的标签的节点上调用 componentDidUpdate 生命周期。

11. DOM 更新
commitAllHostEffects 是 React 进行 Dom 更新的函数。这个函数通过下面的操作进行 Dom 更新:
function commitAllHostEffects() {
switch (primaryEffectTag) {
case Placement: {
commitPlacement(nextEffect);

}
case PlacementAndUpdate: {
commitPlacement(nextEffect);
commitWork(current, nextEffect);

}
case Update: {
commitWork(current, nextEffect);

}
case Deletion: {
commitDeletion(nextEffect);

}
}
}
commitAllLifecycles 是调用 componentDidUpdate 和 componentDidMount 生命周期函数的方法。
asdasd

正文完
 0