流程概览
在首次执行 ReactDOM.render 时
- 创建一个FiberRootNode节点以及RootFiber节点,并且将FiberRootNode的current属性指向RootFiber(这两者之间的关系在之前有提到,这里不多赘述)。

- 进入到render阶段,此阶段会生成一棵 workInProgress Fiber 树,并且在构建此树的过程中会尝试通过Fiber节点的
alternate属性复用旧节点的属性。 - 构建 workInProgress树 完毕后,进入 commit阶段 ,此阶段会根据workInProgress树来进行应用DOM节点的更新。
在后续的 ReactDOM.render 或者组件更新的过程中,由于已经存在FiberRootNode与RootFiber,上述过程的第1步会被忽略,直接进行第2,3步的操作。
JSX的本质
在React当中,JSX会被babel插件 babel-transform-react-jsx 编译为以 React.createElement 方法创建的对象。此方法的内容如下:
export function createElement(type, config, children) {let propName;// Reserved names are extractedconst props = {};let key = null;let ref = null;let self = null;let source = null;if (config != null) {if (hasValidRef(config)) {ref = config.ref;}if (hasValidKey(config)) {key = '' + config.key;}self = config.__self === undefined ? null : config.__self;source = config.__source === undefined ? null : config.__source;// Remaining properties are added to a new props objectfor (propName in config) {if (hasOwnProperty.call(config, propName) &&!RESERVED_PROPS.hasOwnProperty(propName)) {props[propName] = config[propName];}}}// Children can be more than one argument, and those are transferred onto// the newly allocated props object.const childrenLength = arguments.length - 2;if (childrenLength === 1) {props.children = children;} else if (childrenLength > 1) {const childArray = Array(childrenLength);for (let i = 0; i < childrenLength; i++) {childArray[i] = arguments[i + 2];}props.children = childArray;}// Resolve default propsif (type && type.defaultProps) {const defaultProps = type.defaultProps;for (propName in defaultProps) {if (props[propName] === undefined) {props[propName] = defaultProps[propName];}}}return ReactElement(type,key,ref,self,source,ReactCurrentOwner.current,props,);}
此方法的任务主要如下:
- 获取config参数中的ref和key属性
- 将其余 非保留属性 复制到变量props中
- 将传入的children参数转换成数组
- 获取默认参数赋值给变量props
- 返回
ReactElement函数的调用结果
而 ReactElement 的代码如下:
const ReactElement = function(type, key, ref, self, source, owner, props) {const element = {// This tag allows us to uniquely identify this as a React Element$$typeof: REACT_ELEMENT_TYPE,// Built-in properties that belong on the elementtype: type,key: key,ref: ref,props: props,// Record the component responsible for creating this element._owner: owner,};return element;};
本质上就是返回一个 $$typeof 属性为 REACT_ELEMENT_TYPE 的对象,此对象含有 type 、 key 、 ref 、 props 等相关属性。
由此我们可以得出结论:
JSX在React当中本质为一个 ReactElement 对象,只不过在通过 ReactElement 函数创建前,会首先经过 React.createElement 方法对传入的参数进行 预处理 。
JSX与Fiber树的关系
在流程概览中说到,无论是初始化还是后续更新,React都会生成一棵workInProgress Fiber树。那么创建workInProgress Fiber节点的 依据就是JSX对应的ReactElement对象(以下称JSX对象) ,创建时会对比JSX对象和 current Fiber树 中对应的Fiber节点,根据对比的结果生成新的 workInProgress Fiber节点 。
render阶段工作流程
之前说到,旧版的协调器为 Stack Reconciler ,而新版的协调器为 Fiber Reconciler 。旧版的之所以称之为Stack Reconciler,是因为它是通过 函数递归调用 的方式进行深入优先遍历,数据结构是栈,这种遍历方式是无法中断的。而新版的Fiber Reconciler,其数据结构为链表,可以通过循环的方式进行深入优先遍历,这种遍历方式是可以被中断的。
而由于Fiber树的遍历也是深入优先遍历,因此 对于Fiber树的每个节点来说,都会存在“递”和“归”两个阶段 ,以下从分别从这两个阶段进行分析,而两阶段将分别从mount时和update时进行分析。
“递”阶段(beginWork)
递阶段的主要函数为 beginWork 。
function beginWork(current: Fiber | null, // 当前的Fiber节点workInProgress: Fiber, // 正在被构建的Fiber节点renderLanes: Lanes, // Scheduler 调度相关): Fiber | null {}
beginWork内有两部分的处理逻辑,如果 current !== null 为true,则进入 update 的处理逻辑,反之则进入mount的处理逻辑。
mount处理逻辑
这部分会通过 swtich(workInProgress.tag) 根据不同的Fiber节点类型进入不同的处理逻辑。比较常用的类型为:
- html元素节点:HostComponent
- 文本节点:HostText
- 函数组件:FunctionComponent
- 类组件:ClassComponent
全部类型可在 ReactWorkTag.js 中找到。
除了HostText外,其余三种类型最终都会调用 reconcileChildren 函数。这个函数会生成workInProgress Fiber节点的子Fiber节点,对应内容如下:
export function reconcileChildren(current: Fiber | null,workInProgress: Fiber,nextChildren: any,renderLanes: Lanes,) {if (current === null) {workInProgress.child = mountChildFibers(workInProgress,null,nextChildren,renderLanes,);} else {workInProgress.child = reconcileChildFibers(workInProgress,current.child,nextChildren,renderLanes,);}}
其中,函数的nextChildren参数即为JSX节点,mount时会进入 mountChildFibers 逻辑,update时会进入 reconcileChildFibers 逻辑。定义如下:
export const reconcileChildFibers = ChildReconciler(true);export const mountChildFibers = ChildReconciler(false);
可以知道这两个函数共用一套逻辑,不同的是传入 ChildReconciler 的shouldTrackSideEffects 参数,ChildReconciler会根据此参数来判定当前是否需要处理副作用(DOM节点的增删改等等),处理副作用的具体方法就是 标记effectTag 。
update处理逻辑
进行update时,构建workInProgress Fiber树时会通过 current.alternate 得到对应的根节点,然后在此节点的基础上 复用 current Fiber树的若干属性,包括其child节点(这个复用的过程在 createWorkInProgress 中),workInProgress Fiber树将会在此的基础上进行修改。
如果Fiber节点 没有发生变化(参数、context没变化)而且 判定Fiber节点没有需要的更新 (这部分待完成),那么beginWork的逻辑会走向 bailoutOnAlreadyFinishedWork 这个函数,即复用已完成的工作,包括复用对应的current Fiber的子节点,然后直接返回子Fiber,不会对当前Fiber节点做任何处理。
如果Fiber节点发生变化,那么就会走mount时同样的逻辑,进行自身节点的更新以后,最终走向 reconcileChildren 逻辑生成子Fiber节点,进入下一轮循环。
总结
无论是mount阶段还是update阶段的处理逻辑,其最终目标都是要 生成当前Fiber节点的子节点,然后返回 。这个子节点有可能是直接复用,也有可能是重新生成,生成子节点对应的函数为 reconcileChildren 。
“归”阶段(completeWork)
此阶段的主要函数为 completeWork
completeWork函数内mount和update走同样的逻辑,都是根据不同类型的workInProgress Fiber节点(tag属性),走不同的逻辑。
mount处理逻辑
针对HostComponent的Fiber节点,主要的任务有三:
- 生成新的DOM节点 (
createInstance) - 添加子Fiber节点对应的DOM节点 (
appendAllChildren) - 为DOM节点添加DOM属性 (
finalizeInitialChildren)
值得一提的是 appendAllChildren ,其代码如下:
appendAllChildren = function(parent: Instance,workInProgress: Fiber,needsVisibilityToggle: boolean,isHidden: boolean,) {// We only have the top Fiber that was created but we need recurse down its// children to find all the terminal nodes.let node = workInProgress.child;while (node !== null) {if (node.tag === HostComponent || node.tag === HostText) {appendInitialChild(parent, node.stateNode);} else if (enableFundamentalAPI && node.tag === FundamentalComponent) {appendInitialChild(parent, node.stateNode.instance);} else if (node.tag === HostPortal) {// If we have a portal child, then we don't want to traverse// down its children. Instead, we'll get insertions from each child in// the portal directly.} else if (node.child !== null) {// 针对函数组件/类组件等深层嵌套的情况,会逐步深入得到HostComponent// 然后将DOM节点全部插入到parent中node.child.return = node;node = node.child;continue;}if (node === workInProgress) {return;}while (node.sibling === null) {if (node.return === null || node.return === workInProgress) {return;}// 从深层嵌套的函数组件或者类组件中返回node = node.return;}node.sibling.return = node.return;node = node.sibling;}};
根据我们之前提到的Fiber数据结构可知,Fiber的子节点是以 链表 的形式存在。所以在这个函数中会通过循环遍历链表的形式来遍历所有的子Fiber节点,其中指向下一个节点的属性为 sibling 。一个值得注意的细节是 node.sibling.return = node.return; 这行代码,Fiber节点的其他子节点(非第一个)是在这里完成添加 return 。
由于每个HostComponent都会执行appendAllChildren操作,这就保证了每个HostComponent完成completeWork后其stateNode是一棵完整的DOM树,包含所有子Fiber对应的DOM节点。
update处理逻辑
对于 HostText ,如果已经存在对应的stateNode,那么就会直接进入到 标记effectTag 的环节,如果前后Text不一致,就会将workInProgress Fiber的effectTag属性打上Update的标签
对于 HostComponent ,则会进入到对属性的处理中,处理过程在 diffProperties 中,此函数会返回一个包含属性修改内容的数组 updatePayload ,其偶数项为propKey,奇数项为propValue,然后赋值给workInProgress Fiber的updateQueue属性,在commit阶段会根据这个属性进行DOM节点的修改。此外,同样需要给workInProgress Fiber打上Update的effectTag。
为了避免在commit阶段重复遍历Fiber树,React在此阶段通过将有effectTag的Fiber节点以 链表 的形式组织起来。具体代码如下(在completeUnitOfWork函数中):
if (returnFiber !== null &&// Do not append effects to parents if a sibling failed to complete(returnFiber.flags & Incomplete) === NoFlags) {// Append all the effects of the subtree and this fiber onto the effect// list of the parent. The completion order of the children affects the// side-effect order.if (returnFiber.firstEffect === null) {returnFiber.firstEffect = completedWork.firstEffect;}if (completedWork.lastEffect !== null) {if (returnFiber.lastEffect !== null) {returnFiber.lastEffect.nextEffect = completedWork.firstEffect;}returnFiber.lastEffect = completedWork.lastEffect;}// If this fiber had side-effects, we append it AFTER the children's// side-effects. We can perform certain side-effects earlier if needed,// by doing multiple passes over the effect list. We don't want to// schedule our own side-effect on our own list because if end up// reusing children we'll schedule this effect onto itself since we're// at the end.const flags = completedWork.flags;// Skip both NoWork and PerformedWork tags when creating the effect// list. PerformedWork effect is read by React DevTools but shouldn't be// committed.if (flags > PerformedWork) {if (returnFiber.lastEffect !== null) {returnFiber.lastEffect.nextEffect = completedWork;} else {returnFiber.firstEffect = completedWork;}returnFiber.lastEffect = completedWork;}}
一个Fiber节点有 firstEffect 和 lastEffect 两个属性,分别指向 副作用链表 的头和尾。这个链表包含着当前Fiber节点中所有有副作用的子节点。上面的处理逻辑主要的工作就是将当前Fiber节点的副作用链表及当前Fiber节点整合到父Fiber节点的副作用链表当中。下面来分析细节:
- 将当前Fiber节点的副作用链表 拼接 到父Fiber节点的副作用链表末尾(9-17行)
- 如果当前Fiber节点有副作用,那么同样拼接到父Fiber节点的副作用链表的末尾
通过副作用链表的层层向上传递,最终将在rootFiber节点中获得一条包含Fiber树中所有副作用的副作用链表。
总结
- mount处理逻辑:生成DOM节点,添加DOM属性,添加子Fiber的DOM节点
- update处理逻辑:给Fiber打上相关的effectTag,并且整合到父Fiber节点的副作用链表中,以此逐层传递到rootFiber节点当中
双缓存机制的运作流程
在Fiber架构概览当中说过React的双缓存指的是两棵树 current Fiber 树和 workInProgress Fiber 树。
双缓存的运行可以分为三个阶段:
- 首屏渲染
- 第一次更新
- 第二次更新
以下代码为例:
function App() {const [num, setNum] = useState(0)const onClick = () => {setNum(state => state + 1)}return (<div onClick={onClick}><p>{num}</p></div>);}
首屏渲染

上述图片中,左侧为current Fiber树,右侧为workInProgress Fiber树。
首先,React会通过 createWorkInProgress 函数依据current Fiber的rootFiber 创建 workInProgress Fiber树的rootFiber,此外还会再两者之间通过 alternate 属性建立相互连接。
然后,由于是首屏渲染,剩下的Fiber节点没法根据current Fiber创建,所以剩余的Fiber节点都是通过其他的函数新建的(createFiberFromElement等等)。
值得注意的是,针对只有一个文本节点的节点,React并不会为那个文本节点独立创建一个新的Fiber。
第一次更新

在第一次更新中,current Fiber树即为首屏渲染中创建的workInProgress Fiber树。
由于已经存在对应的current Fiber节点,所以此次更新会使用 createWorkInProgress 来 创建 App 以及 p 的Fiber节点,在两者之间建立连接,而由于rootFiber已经存在,所以会被 复用 。
第二次更新

第二次更新中,依然会使用 createWorkInProgress 获取Fiber节点,此次过程中,由于所有节点都已经存在,所以构建过程中所有节点都能 复用
总结
从上面的三个阶段可以得知,双缓存机制真正完全生效是在第二次更新及以后,因为此次更新后两个Fiber树的结构才构建完全。
