图解React图解React
  • 原理解析
  • 高频算法
  • 面试题
⌘ K
基本概念
宏观包结构
两大工作循环
高频对象
运行核心
启动过程
reconciler 运作流程
优先级管理
调度原理
fiber 树构造(基础准备)
fiber 树构造(初次创建)
fiber 树构造(对比更新)
fiber 树渲染
状态管理
状态与副作用
Hook 原理(概览)
Hook 原理(状态Hook)
Hook 原理(副作用Hook)
context 原理
交互
合成事件
Copyright © 2023 | Powered by dumi

fiber 树构造(对比更新)

在前文fiber 树构造(初次创建)一文的介绍中, 演示了fiber树构造循环中逐步构造fiber树的过程. 由于是初次创建, 所以在构造过程中, 所有节点都是新建, 并没有复用旧节点.

本节讨论对比更新这种情况(在Legacy模式下进行分析). 在阅读本节之前, 最好对fiber 树构造(初次创建)有一些了解, 其中有很多相似逻辑不再重复叙述, 本节重点突出对比更新与初次创建的不同之处.

本节示例代码如下(codesandbox 地址):

import React from 'react';
class App extends React.Component {
state = {
list: ['A', 'B', 'C'],
};
onChange = () => {
this.setState({ list: ['C', 'A', 'X'] });
};
componentDidMount() {
console.log(`App Mount`);
}
render() {
return (
<>
<Header />
<button onClick={this.onChange}>change</button>
<div className="content">
{this.state.list.map((item) => (
<p key={item}>{item}</p>
))}
</div>
</>
);
}
}
class Header extends React.PureComponent {
render() {
return (
<>
<h1>title</h1>
<h2>title2</h2>
</>
);
}
}
export default App;

在初次渲染完成之后, 与fiber树相关的内存结构如下(后文以此图为基础, 演示对比更新过程):

更新入口

前文reconciler 运作流程中总结的 4 个阶段(从输入到输出), 其中承接输入的函数只有scheduleUpdateOnFiber(源码地址).在react-reconciler对外暴露的 api 函数中, 只要涉及到需要改变 fiber 的操作(无论是首次渲染或对比更新), 最后都会间接调用scheduleUpdateOnFiber, scheduleUpdateOnFiber函数是输入链路中的必经之路.

3 种更新方式

如要主动发起更新, 有 3 种常见方式:

  1. Class组件中调用setState.
  2. Function组件中调用hook对象暴露出的dispatchAction.
  3. 在container节点上重复调用render(官网示例)

下面列出这 3 种更新方式的源码:

setState

在Component对象的原型上挂载有setState(源码链接):

Component.prototype.setState = function (partialState, callback) {
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

在fiber 树构造(初次创建)中的beginWork阶段, class 类型的组件初始化完成之后, this.updater对象如下(源码链接):

const classComponentUpdater = {
isMounted,
enqueueSetState(inst, payload, callback) {
// 1. 获取class实例对应的fiber节点
const fiber = getInstance(inst);
// 2. 创建update对象
const eventTime = requestEventTime();
const lane = requestUpdateLane(fiber); // 确定当前update对象的优先级
const update = createUpdate(eventTime, lane);
update.payload = payload;
if (callback !== undefined && callback !== null) {
update.callback = callback;
}
// 3. 将update对象添加到当前Fiber节点的updateQueue队列当中
enqueueUpdate(fiber, update);
// 4. 进入reconciler运作流程中的`输入`环节
scheduleUpdateOnFiber(fiber, lane, eventTime); // 传入的lane是update优先级
},
};

dispatchAction

此处只是为了对比dispatchAction和setState. 有关hook原理的深入分析, 在hook 原理章节中详细讨论.

在function类型组件中, 如果使用hook(useState), 则可以通过hook api暴露出的dispatchAction(源码链接)来更新

function dispatchAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
) {
// 1. 创建update对象
const eventTime = requestEventTime();
const lane = requestUpdateLane(fiber); // 确定当前update对象的优先级
const update: Update<S, A> = {
lane,
action,
eagerReducer: null,
eagerState: null,
next: (null: any),
};
// 2. 将update对象添加到当前Hook对象的updateQueue队列当中
const pending = queue.pending;
if (pending === null) {
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
// 3. 请求调度, 进入reconciler运作流程中的`输入`环节.
scheduleUpdateOnFiber(fiber, lane, eventTime); // 传入的lane是update优先级
}

重复调用 render

import ReactDOM from 'react-dom';
function tick() {
const element = (
<div>
<h1>Hello, world!</h1>
<h2>It is {new Date().toLocaleTimeString()}.</h2>
</div>
);
ReactDOM.render(element, document.getElementById('root'));
}
setInterval(tick, 1000);

对于重复render, 在React 应用的启动过程中已有说明, 调用路径包含updateContainer-->scheduleUpdateOnFiber

故无论从哪个入口进行更新, 最终都会进入scheduleUpdateOnFiber, 再次证明scheduleUpdateOnFiber是输入阶段的必经函数(参考reconciler 运作流程).

构造阶段

逻辑来到scheduleUpdateOnFiber函数:

// ...省略部分代码
export function scheduleUpdateOnFiber(
fiber: Fiber, // fiber表示被更新的节点
lane: Lane, // lane表示update优先级
eventTime: number,
) {
const root = markUpdateLaneFromFiberToRoot(fiber, lane);
if (lane === SyncLane) {
if (
(executionContext & LegacyUnbatchedContext) !== NoContext &&
(executionContext & (RenderContext | CommitContext)) === NoContext
) {
// 初次渲染
performSyncWorkOnRoot(root);
} else {
// 对比更新
ensureRootIsScheduled(root, eventTime);
}
}
mostRecentlyUpdatedRoot = root;
}

对比更新与初次渲染的不同点:

  1. markUpdateLaneFromFiberToRoot函数, 只在对比更新阶段才发挥出它的作用, 它找出了fiber树中受到本次update影响的所有节点, 并设置这些节点的fiber.lanes或fiber.childLanes(在legacy模式下为SyncLane)以备fiber树构造阶段使用.
function markUpdateLaneFromFiberToRoot(
sourceFiber: Fiber, // sourceFiber表示被更新的节点
lane: Lane, // lane表示update优先级
): FiberRoot | null {
// 1. 将update优先级设置到sourceFiber.lanes
sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane);
let alternate = sourceFiber.alternate;
if (alternate !== null) {
// 同时设置sourceFiber.alternate的优先级
alternate.lanes = mergeLanes(alternate.lanes, lane);
}
// 2. 从sourceFiber开始, 向上遍历所有节点, 直到HostRoot. 设置沿途所有节点(包括alternate)的childLanes
let node = sourceFiber;
let parent = sourceFiber.return;
while (parent !== null) {
parent.childLanes = mergeLanes(parent.childLanes, lane);
alternate = parent.alternate;
if (alternate !== null) {
alternate.childLanes = mergeLanes(alternate.childLanes, lane);
}
node = parent;
parent = parent.return;
}
if (node.tag === HostRoot) {
const root: FiberRoot = node.stateNode;
return root;
} else {
return null;
}
}

markUpdateLaneFromFiberToRoot

下图表示了markUpdateLaneFromFiberToRoot的具体作用:

  • 以sourceFiber为起点, 设置起点的fiber.lanes
  • 从起点开始, 直到HostRootFiber, 设置父路径上所有节点(也包括fiber.alternate)的fiber.childLanes.
  • 通过设置fiber.lanes和fiber.childLanes就可以辅助判断子树是否需要更新(在下文循环构造中详细说明).

  1. 对比更新没有直接调用performSyncWorkOnRoot, 而是通过调度中心来处理, 由于本示例是在Legacy模式下进行, 最后会同步执行performSyncWorkOnRoot.(详细原理可以参考React 调度原理(scheduler)). 所以其调用链路performSyncWorkOnRoot--->renderRootSync--->workLoopSync与初次构造中的一致.

在renderRootSync中:

function renderRootSync(root: FiberRoot, lanes: Lanes) {
const prevExecutionContext = executionContext;
executionContext |= RenderContext;
// 如果fiberRoot变动, 或者update.lane变动, 都会刷新栈帧, 丢弃上一次渲染进度
if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
// 刷新栈帧, legacy模式下都会进入
prepareFreshStack(root, lanes);
}
do {
try {
workLoopSync();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
executionContext = prevExecutionContext;
// 重置全局变量, 表明render结束
workInProgressRoot = null;
workInProgressRootRenderLanes = NoLanes;
return workInProgressRootExitStatus;
}

进入循环构造(workLoopSync)前, 会刷新栈帧(调用prepareFreshStack)(参考fiber 树构造(基础准备)中栈帧管理).

此时的内存结构如下:

注意:

  • fiberRoot.current指向与当前页面对应的fiber树, workInProgress指向正在构造的fiber树.
  • 刷新栈帧会调用createWorkInProgress(), 使得workInProgress.flags和workInProgress.effects都已经被重置. 且workInProgress.child = current.child. 所以在进入循环构造之前, HostRootFiber与HostRootFiber.alternate共用一个child(这里是fiber(<App/>)).

循环构造

回顾一下fiber 树构造(初次创建)中的介绍. 整个fiber树构造是一个深度优先遍历(可参考React 算法之深度优先遍历), 其中有 2 个重要的变量workInProgress和current(可参考fiber 树构造(基础准备)中介绍的双缓冲技术):

  • workInProgress和current都视为指针
  • workInProgress指向当前正在构造的fiber节点
  • current = workInProgress.alternate(即fiber.alternate), 指向当前页面正在使用的fiber节点.

在深度优先遍历中, 每个fiber节点都会经历 2 个阶段:

  1. 探寻阶段 beginWork
  2. 回溯阶段 completeWork

这 2 个阶段共同完成了每一个fiber节点的创建(或更新), 所有fiber节点则构成了fiber树.

function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
// ... 省略部分无关代码
function performUnitOfWork(unitOfWork: Fiber): void {
// unitOfWork就是被传入的workInProgress
const current = unitOfWork.alternate;
let next;
next = beginWork(current, unitOfWork, subtreeRenderLanes);
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// 如果没有派生出新的节点, 则进入completeWork阶段, 传入的是当前unitOfWork
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
}

注意: 在对比更新过程中current = unitOfWork.alternate;不为null, 后续的调用逻辑中会大量使用此处传入的current.

探寻阶段 beginWork

beginWork(current, unitOfWork, subtreeRenderLanes)(源码地址).

function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
const updateLanes = workInProgress.lanes;
if (current !== null) {
// 进入对比
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (
oldProps !== newProps ||
hasLegacyContextChanged() ||
(__DEV__ ? workInProgress.type !== current.type : false)
) {
didReceiveUpdate = true;
} else if (!includesSomeLane(renderLanes, updateLanes)) {
// 当前渲染优先级renderLanes不包括fiber.lanes, 表明当前fiber节点无需更新
didReceiveUpdate = false;
switch (
workInProgress.tag
// switch 语句中包括 context相关逻辑, 本节暂不讨论(不影响分析fiber树构造)
) {
}
// 当前fiber节点无需更新, 调用bailoutOnAlreadyFinishedWork循环检测子节点是否需要更新
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
}
// 余下逻辑与初次创建共用
// 1. 设置workInProgress优先级为NoLanes(最高优先级)
workInProgress.lanes = NoLanes;
// 2. 根据workInProgress节点的类型, 用不同的方法派生出子节点
switch (
workInProgress.tag // 只列出部分case
) {
case ClassComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes);
case HostComponent:
return updateHostComponent(current, workInProgress, renderLanes);
case HostText:
return updateHostText(current, workInProgress);
case Fragment:
return updateFragment(current, workInProgress, renderLanes);
}
}

bailout逻辑 {#bailout}

bail out英文短语翻译为解救, 纾困, 在源码中, bailout用于判断子树节点是否完全复用, 如果可以复用, 则会略过 fiber 树构造.

与初次创建不同, 在对比更新过程中, 如果是老节点, 那么current !== null, 需要进行对比, 然后决定是否复用老节点及其子树(即bailout逻辑).

  1. !includesSomeLane(renderLanes, updateLanes)这个判断分支, 包含了渲染优先级和update优先级的比较(详情可以回顾fiber 树构造(基础准备)中优先级相关解读), 如果当前节点无需更新, 则会进入bailout逻辑.
  2. 最后会调用bailoutOnAlreadyFinishedWork:
    • 如果同时满足!includesSomeLane(renderLanes, workInProgress.childLanes), 表明该 fiber 节点及其子树都无需更新, 可直接进入回溯阶段(completeUnitOfWork)
    • 如果不满足!includesSomeLane(renderLanes, workInProgress.childLanes), 意味着子节点需要更新, clone并返回子节点.
// 省略部分无关代码
function bailoutOnAlreadyFinishedWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
// 渲染优先级不包括 workInProgress.childLanes, 表明子节点也无需更新. 返回null, 直接进入回溯阶段.
return null;
} else {
// 本fiber虽然不用更新, 但是子节点需要更新. clone并返回子节点
cloneChildFibers(current, workInProgress);
return workInProgress.child;
}
}

注意: cloneChildFibers内部调用createWorkInProgress, 在构造fiber节点时会优先复用workInProgress.alternate(不开辟新的内存空间), 否则才会创建新的fiber对象.

updateXXX函数

updateXXX函数(如: updateHostRoot, updateClassComponent 等)的主干逻辑与初次构造过程完全一致, 总的目的是为了向下生成子节点, 并在这个过程中调用reconcileChildren调和函数, 只要fiber节点有副作用, 就会把特殊操作设置到fiber.flags(如:节点ref,class组件的生命周期,function组件的hook,节点删除等).

对比更新过程的不同之处:

  1. bailoutOnAlreadyFinishedWork
    • 对比更新时如果遇到当前节点无需更新(如: class类型的节点且shouldComponentUpdate返回false), 会再次进入bailout逻辑.
  2. reconcileChildren调和函数
    • 调和函数是updateXXX函数中的一项重要逻辑, 它的作用是向下生成子节点, 并设置fiber.flags.
    • 初次创建时fiber节点没有比较对象, 所以在向下生成子节点的时候没有任何多余的逻辑, 只管创建就行.
    • 对比更新时需要把ReactElement对象与旧fiber对象进行比较, 来判断是否需要复用旧fiber对象.

注: 本节的重点是fiber树构造, 在对比更新过程中reconcileChildren()函数实现的diff算法十分重要, 但是它只是处于算法层面, 对于diff算法的实现,在React 算法之调和算法中单独分析.

本节只需要先了解调和函数目的:

  1. 给新增,移动,和删除节点设置fiber.flags(新增,移动: Placement, 删除: Deletion)
  2. 如果是需要删除的fiber, 除了自身打上Deletion之外, 还要将其添加到父节点的effects链表中(正常副作用队列的处理是在completeWork函数, 但是该节点(被删除)会脱离fiber树, 不会再进入completeWork阶段, 所以在beginWork阶段提前加入副作用队列).

回溯阶段 completeWork

completeUnitOfWork(unitOfWork)函数(源码地址)在初次创建和对比更新逻辑一致, 都是处理beginWork 阶段已经创建出来的 fiber 节点, 最后创建(更新)DOM 对象, 并上移副作用队列.

在这里我们重点关注completeWork函数中, current !== null的情况:

// ...省略无关代码
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
const newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
case HostComponent: {
// 非文本节点
popHostContext(workInProgress);
const rootContainerInstance = getRootHostContainer();
const type = workInProgress.type;
if (current !== null && workInProgress.stateNode != null) {
// 处理改动
updateHostComponent(
current,
workInProgress,
type,
newProps,
rootContainerInstance,
);
if (current.ref !== workInProgress.ref) {
markRef(workInProgress);
}
} else {
// ...省略无关代码
}
return null;
}
case HostText: {
// 文本节点
const newText = newProps;
if (current && workInProgress.stateNode != null) {
const oldText = current.memoizedProps;
// 处理改动
updateHostText(current, workInProgress, oldText, newText);
} else {
// ...省略无关代码
}
return null;
}
}
}
updateHostComponent = function (
current: Fiber,
workInProgress: Fiber,
type: Type,
newProps: Props,
rootContainerInstance: Container,
) {
const oldProps = current.memoizedProps;
if (oldProps === newProps) {
return;
}
const instance: Instance = workInProgress.stateNode;
const currentHostContext = getHostContext();
const updatePayload = prepareUpdate(
instance,
type,
oldProps,
newProps,
rootContainerInstance,
currentHostContext,
);
workInProgress.updateQueue = (updatePayload: any);
// 如果有属性变动, 设置fiber.flags |= Update, 等待`commit`阶段的处理
if (updatePayload) {
markUpdate(workInProgress);
}
};
updateHostText = function (
current: Fiber,
workInProgress: Fiber,
oldText: string,
newText: string,
) {
// 如果有属性变动, 设置fiber.flags |= Update, 等待`commit`阶段的处理
if (oldText !== newText) {
markUpdate(workInProgress);
}
};

可以看到在更新过程中, 如果 DOM 属性有变化, 不会再次新建 DOM 对象, 而是设置fiber.flags |= Update, 等待commit阶段处理(源码链接).

过程图解

针对本节的示例代码, 将整个fiber树构造过程表示出来:

构造前:

在上文已经说明, 进入循环构造前会调用prepareFreshStack刷新栈帧, 在进入fiber树构造循环之前, 保持这这个初始化状态:

performUnitOfWork第 1 次调用(只执行beginWork):

  • 执行前: workInProgress指向HostRootFiber.alternate对象, 此时current = workInProgress.alternate指向当前页面对应的fiber树.
  • 执行过程:
    • 因为current !== null且当前节点fiber.lanes不在渲染优先级范围内, 故进入bailoutOnAlreadyFinishedWork逻辑
    • 又因为fiber.childLanes处于渲染优先级范围内, 证明child节点需要更新, 克隆workInProgress.child节点.
    • clone之后, 新fiber节点会丢弃旧fiber上的标志位(flags)和副作用(effects), 其他属性会继续保留.
  • 执行后: 返回被clone的下级节点fiber(<App/>), 移动workInProgress指向子节点fiber(<App/>)

performUnitOfWork第 2 次调用(只执行beginWork):

  • 执行前: workInProgress指向fiber(<App/>)节点, 且current = workInProgress.alternate有值
  • 执行过程:
    • 当前节点fiber.lanes处于渲染优先级范围内, 会进入updateClassComponent()函数
    • 在updateClassComponent()函数中, 调用reconcileChildren()生成下级子节点.
  • 执行后: 返回下级节点fiber(<Header/>), 移动workInProgress指向子节点fiber(<Header/>)

performUnitOfWork第 3 次调用(执行beginWork和completeUnitOfWork):

  • beginWork执行前: workInProgress指向fiber(<Header/>), 且current = workInProgress.alternate有值
  • beginWork执行过程:
    • 当前节点fiber.lanes处于渲染优先级范围内, 会进入updateClassComponent()函数
    • 在updateClassComponent()函数中, 由于此组件是PureComponent, shouldComponentUpdate判定为false,故进入bailoutOnAlreadyFinishedWork逻辑.
    • 又因为fiber.childLanes不在渲染优先级范围内, 证明child节点也不需要更新
  • beginWork执行后: 因为完全满足bailout逻辑, 返回null. 所以进入completeUnitOfWork(unitOfWork)函数, 传入的参数unitOfWork实际上就是workInProgress(此时指向fiber(<Header/>))

  • completeUnitOfWork执行前: workInProgress指向fiber(<Header/>)
  • completeUnitOfWork执行过程: 以fiber(<Header/>)为起点, 向上回溯

completeUnitOfWork第 1 次循环:

  1. 执行completeWork函数: class类型的组件无需处理.
  2. 上移副作用队列: 由于本节点fiber(header)没有副作用(fiber.flags = 0), 所以执行之后副作用队列没有实质变化(目前为空).
  3. 向上回溯: 由于还有兄弟节点, 把workInProgress指向下一个兄弟节点fiber(button), 退出completeUnitOfWork.

performUnitOfWork第 4 次调用(执行beginWork和completeUnitOfWork):

  • beginWork执行过程: 调用updateHostComponent

    • 本示例中button的子节点是一个直接文本节点,设置nextChildren = null(源码注释的解释是不用在开辟内存去创建一个文本节点, 同时还能减少向下遍历).
    • 由于nextChildren = null, 经过reconcileChildren阶段处理后, 返回值也是null
  • beginWork执行后: 由于下级节点为null, 所以进入completeUnitOfWork(unitOfWork)函数, 传入的参数unitOfWork实际上就是workInProgress(此时指向fiber(button)节点)

  • completeUnitOfWork执行过程: 以fiber(button)为起点, 向上回溯

completeUnitOfWork第 1 次循环:

  1. 执行completeWork函数
    • 因为fiber(button).stateNode != null, 所以无需再次创建 DOM 对象. 只需要进一步调用updateHostComponent()记录 DOM 属性改动情况
    • 在updateHostComponent()函数中, 又因为oldProps === newProps, 所以无需记录改动情况, 直接返回
  2. 上移副作用队列: 由于本节点fiber(button)没有副作用(fiber.flags = 0), 所以执行之后副作用队列没有实质变化(目前为空).
  3. 向上回溯: 由于还有兄弟节点, 把workInProgress指向下一个兄弟节点fiber(div), 退出completeUnitOfWork.

performUnitOfWork第 5 次调用(执行beginWork):

  • 执行前: workInProgress指向fiber(div)节点, 且current = workInProgress.alternate有值
  • 执行过程:
    • 在updateHostComponent()函数中, 调用reconcileChildren()生成下级子节点.
    • 需要注意的是, 下级子节点是一个可迭代数组, 会把fiber.child.sibling一起构造出来, 同时根据需要设置fiber.flags. 在本例中, 下级节点有被删除的情况, 被删除的节点会被添加到父节点的副作用队列中(具体实现方式请参考React 算法之调和算法).
  • 执行后: 返回下级节点fiber(p), 移动workInProgress指向子节点fiber(p)

performUnitOfWork第 6 次调用(执行beginWork和completeUnitOfWork):

  • beginWork执行过程: 与第 4 次调用中构建fiber(button)的逻辑完全一致, 因为都是直接文本节点, reconcileChildren()返回的下级子节点为 null.

  • beginWork执行后: 由于下级节点为null, 所以进入completeUnitOfWork(unitOfWork)函数

  • completeUnitOfWork执行过程: 以fiber(p)为起点, 向上回溯

completeUnitOfWork第 1 次循环:

  1. 执行completeWork函数
    • 因为fiber(p).stateNode != null, 所以无需再次创建 DOM 对象. 在updateHostComponent()函数中, 又因为节点属性没有变动, 所以无需打标记
  2. 上移副作用队列: 本节点fiber(p)没有副作用(fiber.flags = 0).
  3. 向上回溯: 由于还有兄弟节点, 把workInProgress指向下一个兄弟节点fiber(p), 退出completeUnitOfWork.

performUnitOfWork第 7 次调用(执行beginWork和completeUnitOfWork):

  • beginWork执行过程: 与第 4 次调用中构建fiber(button)的逻辑完全一致, 因为都是直接文本节点, reconcileChildren()返回的下级子节点为 null.

  • beginWork执行后: 由于下级节点为null, 所以进入completeUnitOfWork(unitOfWork)函数

  • completeUnitOfWork执行过程: 以fiber(p)为起点, 向上回溯

completeUnitOfWork第 1 次循环:

  1. 执行completeWork函数:

    • 因为fiber(p).stateNode != null, 所以无需再次创建 DOM 对象. 在updateHostComponent()函数中, 又因为节点属性没有变动, 所以无需打标记
  2. 上移副作用队列: 本节点fiber(p)有副作用(fiber.flags = Placement), 需要将其添加到父节点的副作用队列之后.

  3. 向上回溯: 由于还有兄弟节点, 把workInProgress指向下一个兄弟节点fiber(p), 退出completeUnitOfWork.

performUnitOfWork第 8 次调用(执行beginWork和completeUnitOfWork):

  • beginWork执行过程: 本节点fiber(p)是一个新增节点, 其current === null, 会进入updateHostComponent()函数. 因为是直接文本节点, reconcileChildren()返回的下级子节点为 null.

  • beginWork执行后: 由于下级节点为null, 所以进入completeUnitOfWork(unitOfWork)函数

  • completeUnitOfWork执行过程: 以fiber(p)为起点, 向上回溯

completeUnitOfWork第 1 次循环:

  1. 执行completeWork函数: 由于本节点是一个新增节点,且fiber(p).stateNode === null, 所以创建fiber(p)节点对应的DOM实例, 挂载到fiber.stateNode之上.
  2. 上移副作用队列: 本节点fiber(p)有副作用(fiber.flags = Placement), 需要将其添加到父节点的副作用队列之后.
  3. 向上回溯: 由于没有兄弟节点, 把workInProgress指针指向父节点fiber(div).

completeUnitOfWork第 2 次循环:

  1. 执行completeWork函数: 由于div组件没有属性变动, 故updateHostComponent()没有设置副作用标记
  2. 上移副作用队列: 本节点fiber(div)的副作用队列添加到父节点的副作用队列之后.
  3. 向上回溯: 由于没有兄弟节点, 把workInProgress指针指向父节点fiber(<App/>)

completeUnitOfWork第 3 次循环:

  1. 执行completeWork函数: class 类型的节点无需处理
  2. 上移副作用队列: 本节点fiber(<App/>)的副作用队列添加到父节点的副作用队列之后.
  3. 向上回溯: 由于没有兄弟节点, 把workInProgress指针指向父节点fiber(HostRootFiber)

completeUnitOfWork第 4 次循环:

  1. 执行completeWork函数: HostRoot类型的节点无需处理
  2. 向上回溯: 由于父节点为空, 无需进入处理副作用队列的逻辑. 最后设置workInProgress=null, 并退出completeUnitOfWork
  3. 重置fiber.childLanes

到此整个fiber树构造循环(对比更新)已经执行完毕, 拥有一棵新的fiber树, 并且在fiber树的根节点上挂载了副作用队列. renderRootSync函数退出之前, 会重置workInProgressRoot = null, 表明没有正在进行中的render. 且把最新的fiber树挂载到fiberRoot.finishedWork上. 这时整个 fiber 树的内存结构如下(注意fiberRoot.finishedWork和fiberRoot.current指针,在commitRoot阶段会进行处理):

无论是初次构造或者是对比更新, 当fiber树构造完成之后, 余下的逻辑几乎一致, 在fiber 树渲染中继续讨论.

总结

本节演示了更新阶段fiber树构造(对比更新)的全部过程, 跟踪了创建过程中内存引用的变化情况. 与初次构造最大的不同在于fiber节点是否可以复用, 其中bailout逻辑是fiber子树能否复用的判断依据.