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有一个子元素c1
Let’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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。