React是一个用于构建界面的JavaScript库。它的核心是跟踪组件状态变化并将更新后的状态更新到屏幕上。在React中,咱们把这个过程称为 reconciliation (协调)。经过调用setState方法,React检查状态或属性是否已更改,并在UI层上更新。html
首先看一个简单的例子:node
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> ] } }
这是一个简单的计数器的例子, 点击按钮,组件的状态就会在处理程序中更新,组件的状态的更新反过来会引发span元素的内容更新。react
下面是在协调阶段,React内部会有各类活动。如下是计数器在协调阶段所作的操做:git
协调期间还会执行其余活动,例如调用生命周期方法,更新ref。这些活动在Fiber架构中统称为"work"(工做)。work的类型取决于React元素的类型。React元素有多种类型,好比类组件,函数组件,Portals,DOM节点等。而React元素类型则是由React.createElement的第一个参数决定。React.createElement函数在render建立元素中调用。github
<button key="1" onClick={this.onClick}>Update counter</button> <span key="2">{this.state.count}</span>
JSX在通过编译后,会获得以下的结果。这是render方法真正返回的结果算法
class ClickCounter { ... render() { return [ React.createElement( 'button', { key: '1', onClick: this.onClick }, 'Update counter' ), React.createElement( 'span', { key: '2' }, this.state.count ) ] } }
React.createElement函数会返回以下的结果:vim
[ { $$typeof: Symbol(react.element), type: 'button', key: "1", props: { children: 'Update counter', onClick: () => { ... } } }, { $$typeof: Symbol(react.element), type: 'span', key: "2", props: { children: 0 } } ]
而对于组件<ClickCounter>
的元素,它没有props和key:数组
{ $$typeof: Symbol(react.element), key: null, props: {}, ref: null, type: ClickCounter }
在协调期间,render方法返回的React元素会被合并到Fiber节点树之中。每个React元素都有对应的Fiber节点。与React元素不一样,Fiber不会在每一次渲染的时候从新建立。Fiber会保存组件的状态和DOM。架构
前面讨论过,根据不一样的React元素类型,会执行不一样的活动。例如,对于Class组件会调用生命周期方法以及render方法。而对于DOM节点,它执行DOM mutation。所以,每一个React元素都被转换为相应类型的Fiber节点。节点描述了须要完成的"work"。app
能够将Fiber节点看做一种数据解构,表示一个work单元。,Fiber架构还提供了一种跟踪、调度、暂停和停止work的方法。
React会在首次将React元素转换为Fiber节点时,使用createFiberFromTypeAndProps
函数建立Fiber节点。在更新阶段会复用Fiber节点,并使用React元素上的数据更新Fiber节点上的属性。亦或者移动,删除Fiber节点。
React源码中的ChildReconciler函数包含了Fiber节点中全部的work
React会为每个React元素建立一个Fiber节点,咱们会获得一个Fiber节点树
Fiber节点树是经过链表的形式存储的,每个Fiber都拥有child(第一个子节点的引用),sibling(第一个兄弟节点的引用)和return(父节点的引用),来表示层级关心。更多内容请参考这篇文章React Fiber为何使用链表来设计组件树
在第一次渲染完成后,React会生成一个Fiber树。该树映射了应用程序的状态,这颗树被称为current tree
。当应用程序开始更新时,React会构建一个workInProgress tree
, workInProgress tree
映射了将来的状态。
全部的"work"都是在workInProgress tree
上的Fiber节点上进行的。当React开始遍历current tree
时,它会为每个现有的Fiber节点建立一个备份(alternate字段),alternate节点构成了workInProgress tree
。当全部更新和相关的"work"完成。workInProgress tree
会刷新到屏幕上。workInProgress tree
此时变为了current tree
。
React的核心原则之一是"一致性", 它老是一次性更新DOM, 不会显示部分结果. workInProgress就是一个用户不可见的"草稿", React在它上面处理全部组件, 处理完成后将它再刷新到界面上.
在React的源码中,有不少从workInProgress tree
和current tree
中获取Fiber节点的函数。好比下面这个函数签名
function updateHostComponent(current, workInProgress, renderExpirationTime) { ... }
workInProgress tree
的Fiber节点拥有current tree
对应节点的引用。反之亦然。
咱们能够将React组件视为使用state和props计算UI的函数。其余的活动,好比手动修改DOM,调用生命周期都应该被视做一种反作用。在React的文档中也说起了这一点
你以前可能已经在 React 组件中执行过数据获取、订阅或者手动修改过 DOM。咱们统一把这些操做称为“反作用”(side-effects),或者简称为“做用”(effects)。由于它们会影响其余组件,而且在渲染期间没法完成。
大多数state和props的更新都会致使反作用。应用effects是一种work类型。所以Fiber节点是一种跟踪更新和effects的便捷机制,每个Fiber节点都有与之相关联的effects。它们被编码在effectTag字段之中。
Fiber中的effects定义了处理更新以后须要作的"work"。对于DOM元素,"work"包含了添加,更新,删除。对于类组件,包括了更新ref,调用componentDidMount和componentDidUpdate生命周期方法。还有其余effects对应于其余类型的Fibber。
React处理更新很是快。为了达到更好的性能水平,采用了一些有趣的技术。其中之一就是将具备effects的Fiber节点,构建为线性列表,以方便快速迭代。迭代线性列表要比迭代树快的多,由于不须要迭代没有side-effects的节点。
effects list的目的是是标记出具备DOM更新,或其它与之关联的其余effects的节点。effects list是finishedWork树的子集。在workInProgress tree
和current tree
中使用nextEffect属性连接在一块儿。
丹·阿布拉莫夫(Dan Abramov)将Effects list提供了一个比喻。将Fiber想象成一颗圣诞树,用圣诞灯将全部有效的节点链接在一块儿。
为了可视化这一点,让咱们想象下面的Fiber树,其中高亮显示的节点有一些“work”要作。
例如,咱们的更新致使将c2插入到DOM中,d2和c1更改属性,b2触发生命周期方法。 Effects list列表将把它们连接在一块儿,这样React就能够遍历时跳过其余节点。
能够看到具备effects的节点如何连接在一块儿。当遍历节点时,React使用firstEffect指针肯定列表的起始位置。上图的Effects list能够用下图表示
React应用都有一个或者多个充当容器的DOM元素
const domContainer = document.querySelector('#container'); ReactDOM.render(React.createElement(ClickCounter), domContainer);
React会为容器建立FiberRoot对象,可使用容器的DOM引用访问Fiber root对象:
// Fiber root对象 const fiberRoot = query('#container')._reactRootContainer._internalRoot
Fiber root是React保留对Fiber树引用的地方,Fiber树存储在Fiber root对象的current属性中
// Fiber树 const hostRootFiberNode = fiberRoot.current
Fiber树的第一个节点是一种特殊的类型节点,叫作HostRoot。它在内部建立,是最顶层组件的父组件。经过HostRoot节点的stateNode属性能够访问FiberRoot节点.
// Fiber root对象 const fiberRoot = query('#container')._reactRootContainer._internalRoot // hostRoot const hostRootFiberNode = fiberRoot.current // true hostRootFiberNode.stateNode === fiberRoot
咱们能够从HostRoot来访问和探索整个Fiber树。或者能够经过组件的实例中得到单个Fiber节点
compInstance._reactInternalFiber
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(记录与之关联的effects), nextEffect(链接具备effects的Fiber节点使其成为线性节点)
保留对class组件实例的引用, DOM节点或其余与Fiber节点相关联的React元素类实例的引用。通常来讲, 咱们能够说这个属性被用于保存与当前Fiber相关的本地状态。
定义与此Fiber节点相关联的函数或者类。对于class组件,type属性指向构造函数。对于DOM元素,type属性指向HTML标记。我常常用这个字段来判断这个Fiber节点与那个元素相关。
定义Fiber节点的类型。在协调期间使用它肯定须要作的"work"。如以前所述"work"取决于React元素的类型。createFiberFromTypeAndProps函数将React元素映射成相对应的Fiber节点类型。
在咱们的例子中。ClickCounter的tag为1,表示为ClassComponent。span的tag为5,标记为HostComponent。
state更新和回调,DOM更新的队列。
用于建立输出Fiber的state。在处理更新的时候,它映射的是当前界面上呈现的state。
在上一次渲染过程当中用来建立输出的Fiber props。
已经更新后的Fiber props。须要用于子组件和DOM元素。
一组children中的惟一标示。帮助React肯定那些发生了更改,新增或删除。更详细的解释在这里
完整的Fiber结构, 能够在这里看到,在上面的说明省略了不少的字段好比child,sibling并return。这三个字段是构成链表树结构的关键。以及expirationTime、childExpirationTime和mode,这些字段是特定于Scheduler的。
React分两个阶段执行work:render(渲染)和 commit(提交)
在render(渲染)阶段,React将更新应用于经过setState或React.render调度的组件, 并找出须要在UI中更新的内容。
若是是初始渲染,React将为render方法返回的每个元素建立新的Fiber节点。在以后的更新中,将从新使用和更新现有的Fiber节点。
render阶段会构建一个带有side-effects(反作用)的Fiber节点树。effects描述了下一个commit(提交)阶段须要完成的“work”。在commit(提交)阶段,React会使用标记有effects的Fiber节点并将其应用于实例上。遍历Effects list执行DOM更新和其余对用户可见的更改。
请切记,render阶段的工做是能够异步执行的,React根据可用时间处理一个或者多个Fiber节点。当发生一些更重要的事情时,React会中止并保存已完成的工做。等重要的事情处理完成后,React从中断处继续完成工做。可是有时可能会放弃已经完成的工做,从顶层从新开始。此阶段执行的工做是对用户是不可见的,所以能够实现暂停。可是在commit(提交)阶段始终是同步的它会产生用户可见的变化, 例如DOM的修改. 这就是React须要一次性完成它们的缘由。
调用生命周期函数使用React的“work”之一。在render阶段调用这些生命周期方法:
因为render阶段不会产生DOM更新之类的反作用,所以React能够异步地对组件进行异步处理更新(甚至可能在多个线程中进行)。可是带有UNSAFE_前缀的生命周期函数经常会被误用,开发者会把反作用添加到这些生命周期函数中。这可能会致使异步渲染出现问题
在commit阶段调用这些生命周期方法,这些生命周期方法在commit阶段执行,因此它们可能包含反作用并涉及DOM更新。
协调算法使用renderRoot函数从最顶层的HostRoot节点开始,跳过已经处理过的节点,直到找到work未完成的节点为止。例如, 当在组件树深处调用setState方法, React 从顶部开始快速的跳过全部父级节点直接得到调用setState方法的组件。
全部的Fiber节点在render阶段都会在WorkLoop中被处理。这是循环同步部分的实现:
function workLoop(isYieldy) { if (!isYieldy) { while (nextUnitOfWork !== null) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork); } } else {...} }
在上面的代码中,nextUnitOfWork保持了对workInProgress tree
中一个有工做要处理的Fiber节点的引用。在React遍历Fiber树时,会使用nextUnitOfWork判断是否有未完成"work"的Fiber节点。当节点处理完成“work”后,nextUnitOfWork会指向下一个Fiber节点的引用或者为null。当nextUnitOfWork为null时,React会退出WorkLoop,并准备进入到commit阶段。
有四个主要的方法用于遍历树,并启动或完成工做:
为了演示如何使用它们。请查看下面遍历Fiber树的演示动画。演示动画中使用了这些函数的简化实现。咱们能够经过演示看到,首先处理子节点的“work”,而后处理父节点的“work”。
使用直线链接表明同级,使用折线链接的代笔子级
逐步拆分下React遍历Fiber树的过程(首先处理子节点的“work”,而后处理父节点的“work”):
这个是视频的链接, 从概念上将"begin"当作进入组件,“complete”当作离开组件。
咱们首先看下beginWork和performUnitOfWork这两个函数:
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 tree中接收一个Fiber节点。而后调用beginWork开始处理Fiber节点的work。为了演示,这里只是log了Fiber节点的name字段表示work已经完成。函数beginWork老是返回指向循环中下一个子节点或null。
若是有下一个子节点, 它将在workLoop函数中分配给nextUnitOfWork。若是没有子节点,React就知道了到达了分支的结尾。就会完成当前Fiber节点的work。React会执行它兄弟节点的工做,最后回溯到父节点。这是在completeUnitOfWork中完成的。
function completeUnitOfWork(workInProgress) { while (true) { let returnFiber = workInProgress.return; let siblingFiber = workInProgress.sibling; nextUnitOfWork = completeWork(workInProgress); if (siblingFiber !== null) { // 若是有同级,则返回它。以继续执行同级的工做 return siblingFiber; } else if (returnFiber !== null) { // 回溯到上一级 workInProgress = returnFiber; continue; } else { // 已经到了root节点 return null; } } } function completeWork(workInProgress) { console.log('work completed for ' + workInProgress.name); return null; }
当workInProgress节点没有子节点时,会进入此函数。在完成当前Fiber的工做后,会检查是否有兄弟节点。若是有,返回同级的兄弟节点的指针,分配给nextUnitOfWork。React将会从兄弟节点开始工做。只有处理完子节点全部分支以后, 才会回溯到父节点(全部子节点处理完成后,才会回溯到父节点)。
从实现能够看出,completeUnitOfWork主要用于迭代,主要工做都是beginWork和completeWork函数中进行的。
这里是完整的示例(beginWork,performUnitOfWork,completeUnitOfWork,completeWork的简易实现)
// 首先构建链表树 const a1 = {name: 'a1', child: null, sibling: null, return: null}; const b1 = {name: 'b1', child: null, sibling: null, return: null}; const b2 = {name: 'b2', child: null, sibling: null, return: null}; const b3 = {name: 'b3', child: null, sibling: null, return: null}; const c1 = {name: 'c1', child: null, sibling: null, return: null}; const c2 = {name: 'c2', child: null, sibling: null, return: null}; const d1 = {name: 'd1', child: null, sibling: null, return: null}; const d2 = {name: 'd2', child: null, sibling: null, return: null}; a1.child = b1; b1.sibling = b2; b2.sibling = b3; b2.child = c1; b3.child = c2; c1.child = d1; d1.sibling = d2; b1.return = b2.return = b3.return = a1; c1.return = b2; d1.return = d2.return = c1; c2.return = b3; // 当前的指针是a1 let nextUnitOfWork = a1; workLoop(); // 开始工做循环 function workLoop() { while (nextUnitOfWork !== null) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork); } } function performUnitOfWork(workInProgress) { let next = beginWork(workInProgress); if (next === null) { next = completeUnitOfWork(workInProgress); } return next; } function beginWork(workInProgress) { log('work performed for ' + workInProgress.name); return workInProgress.child; } function completeUnitOfWork(workInProgress) { while (true) { let returnFiber = workInProgress.return; let siblingFiber = workInProgress.sibling; nextUnitOfWork = completeWork(workInProgress); if (siblingFiber !== null) { return siblingFiber; } else if (returnFiber !== null) { workInProgress = returnFiber; continue; } else { return null; } } } function completeWork(workInProgress) { log('work completed for ' + workInProgress.name); return null; } function log(message) { let node = document.createElement('div'); node.textContent = message; document.body.appendChild(node); }
提交阶段从completeRoot开始。这是React更新DOM,调用getSnapshotBeforeUpdate,componentDidMount,componentDidUpdate,componentWillUnmount等生命周期的地方。
React进入这一阶段时,有两颗树(workInProgress tree和current tree)以及effects list。current tree
表示了当前屏幕上呈现的状态。render阶段遍历current tree
时会生成另外一颗树,在源码中被称为finishWork或workInProgress,表示将来须要在屏幕上呈现的状态。workInProgress tree
和current tree
结构相似。
调试时,如何获取current tree
以及workInProgress tree
?
// current tree // 从容器对象上获取FiberRoot对象 const fiberRoot = query('#container')._reactRootContainer._internalRoot // 获取current tree const currentTree = fiberRoot.current // 获取workInProgress tree const workInProgressTree = fiberRoot.current.alternate
提交(commit)阶段,主要执行commitRoot函数,执行如下的操做:
workInProgress tree
设置为current tree
。在调用getSnapshotBeforeUpdate方法后,React将commit,Fiber树中全部的反作用。分为两步:
第一步,执行全部的DOM插入,更新,删除和ref卸载。而后将workInProgress tree
设置为current tree
树。这是在第一步完成以后,第二步以前完成的。所以在componentWillUnmount生命周期方法在执行期间,状态依然是更新以前的。而componentDidMount/componentDidUpdate执行时的状态是更新以后的。第二步,执行其余生命周期方法和ref回调,这些方法做为单独的过程被执行。
commitRoot方法的预览:
function commitRoot(root, finishedWork) { // 用来执行getSnapshotBeforeUpdate commitBeforeMutationLifecycles() // 用户更新DOM,以及执行componentWillUnmount commitAllHostEffects(); root.current = finishedWork; // 调用componentDidUpdate和componentDidMount生命周期的地方 commitAllLifeCycles(); }
这些子函数,内部都包含了一个循环。循环遍历effects list,并检查effects的类型。当发现类型和子函数的目的相同时,就应用它。
遍历effects list,并检查节点是否具备Snapshot effect的源代码:
function commitBeforeMutationLifecycles() { while (nextEffect !== null) { const effectTag = nextEffect.effectTag; if (effectTag & Snapshot) { const current = nextEffect.alternate; commitBeforeMutationLifeCycles(current, nextEffect); } nextEffect = nextEffect.nextEffect; } }
若是是class组件,调用getSnapshotBeforeUpdate生命周期方法。
commitAllHostEffects是React执行DOM更新的地方。React会把componentWillUnmount做为commitDeletion删除过程当中的一部分。
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是React调用全部剩余生命周期方法componentDidUpdate和componentDidMount的地方。
React源码很复杂,Max Koretskyi的这篇文章内容也不少,全部总结下这篇博客的要点:
current tree
。当开始更新时,React会构建一个workInProgress tree
。current tree
表明了当前的状态,workInProgress tree
表明了将来的状态。workInProgress tree
会被设置为current tree
。current tree
时,会为每个Fiber节点建立一个alternate字段,alternate字段保存了Fiber节点的备份,alternate字段上保存的备份Fiber节点构成了workInProgress tree
。query('#container')._reactRootContainer._internalRoot
。fiberRoot.current
。workInProgress tree
, fiberRoot.current.alternate
。workInProgress tree
设置为current tree
树。