ReactDOM.render的一个流程如下:
1. 创建fiberRootNode、rootFiber、updateQueue(`legacyCreateRootFromDOMContainer`)
|
|
v
2. 创建Update对象(`updateContainer`)
|
|
v
3. 从fiber到root(`markUpdateLaneFromFiberToRoot`)
|
|
v
4. 调度更新(`ensureRootIsScheduled`)
|
|
v
5. render阶段(`performSyncWorkOnRoot` 或 `performConcurrentWorkOnRoot`)
|
|
v
6. commit阶段(`commitRoot`)
第一个阶段
执行ReactDOM.render会创建fiberRootNode和rootFiber。其中fiberRootNode是整个应用的根节点,rootFiber是要渲染组件所在组件树的根节点
这一步发生在调用ReactDOM.render后进入的legacyRenderSubtreeIntoContainer方法中。
// container指ReactDOM.render的第二个参数(即应用挂载的DOM节点)
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
container,
forceHydrate,
);
fiberRoot = root._internalRoot;
legacyCreateRootFromDOMContainer方法内部会调用createFiberRoot方法完成fiberRootNode和rootFiber的创建以及关联。并初始化updateQueue
export function createFiberRoot(
containerInfo: any,
tag: RootTag,
hydrate: boolean,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
): FiberRoot {
// 创建fiberRootNode
const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);
// 创建rootFiber
const uninitializedFiber = createHostRootFiber(tag);
// 连接rootFiber与fiberRootNode
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;
// 初始化updateQueue
initializeUpdateQueue(uninitializedFiber);
return root;
}
根据以上代码,可以在双缓存机制的基础上补充上rootFiber到fiberRootNode的引用
ClassComponent与HostRoot使用的UpdateQueue结构如下
const queue: UpdateQueue<State> = {
baseState: fiber.memoizedState,
firstBaseUpdate: null,
lastBaseUpdate: null,
shared: {
pending: null,
},
effects: null,
};
-
baseState:本次更新前该Fiber节点的state,Update基于该state计算更新后的state。
-
firstBaseUpdate与lastBaseUpdate:本次更新前该Fiber节点已保存的Update。以链表形式存在,链表头为firstBaseUpdate,链表尾为lastBaseUpdate。之所以在更新产生前该Fiber节点内就存在Update,是由于某些Update优先级较低所以在上次render阶段由Update计算state时被跳过。
-
shared.pending:触发更新时,产生的Update会保存在shared.pending中形成单向环状链表。当由Update计算state时这个环会被剪开并连接在lastBaseUpdate后面。
-
effects:数组。保存update.callback !== null的Update
在render阶段的completework中,处理update时,在updateHostComponent内部,被处理完的props会被赋值给workInProgress.updateQueue,并最终会在commit阶段被渲染在页面上
第二个阶段
每次状态更新都会创建一个保存更新状态相关内容的对象,我们叫他Update。在render阶段的beginWork中会根据Update计算新的state
做好了组件的初始化工作,接下来就等待创建Update来开启一次更新
这一步发生在updateContainer方法中
export function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function,
): Lane {
// ...省略与逻辑不相关代码
// 创建update
const update = createUpdate(eventTime, lane, suspenseConfig);
// update.payload为需要挂载在根节点的组件,
//对于HostRoot,payload为ReactDOM.render的第一个传参
update.payload = {element};
// callback为ReactDOM.render的第三个参数 —— 回调函数
callback = callback === undefined ? null : callback;
if (callback !== null) {
update.callback = callback;
}
// 将生成的update加入updateQueue
enqueueUpdate(current, update);
// 调度更新
scheduleUpdateOnFiber(current, lane, eventTime);
// ...省略与逻辑不相关代码
}
Update的分类:
首先,我们将可以触发更新的方法所隶属的组件分类:
ReactDOM.render —— HostRoot
this.setState —— ClassComponent
this.forceUpdate —— ClassComponent
useState —— FunctionComponent
useReducer —— FunctionComponent
可以看到,一共三种组件(HostRoot | ClassComponent | FunctionComponent)可以触发更新。
由于不同类型组件工作方式不同,所以存在两种不同结构的Update,其中ClassComponent与HostRoot共用一套Update结构,FunctionComponent单独使用一种Update结构
Update的结构:
ClassComponent与HostRoot(即rootFiber.tag对应类型)共用同一种Update结构。
对应的结构如下
Update由createUpdate方法返回
const update: Update<*> = {
eventTime,
lane,
suspenseConfig,
tag: UpdateState,
payload: null,
callback: null,
next: null,
};
-
eventTime:任务时间,通过performance.now()获取的毫秒数。由于该字段在未来会重构
-
lane:优先级相关字段。
-
suspenseConfig:Suspense相关,暂不关注。
-
tag:更新的类型,包括UpdateState | ReplaceState | ForceUpdate | CaptureUpdate。
-
payload:更新挂载的数据,不同类型组件挂载的数据不同。对于ClassComponent,payload为this.setState的第一个传参。对于HostRoot,payload为ReactDOM.render的第一个传参。
-
callback:更新的回调函数。即在commit 阶段的 layout 子阶段中的回调函数。
-
next:与其他Update连接形成链表
callback
在commit 阶段的 layout 子阶段中,commitLayoutEffectOnFiber方法会根据fiber.tag对不同类型的节点分别处理
对于ClassComponent,他会通过current === null?区分是mount还是update,调用componentDidMount (opens new window)或componentDidUpdate
触发状态更新的this.setState如果赋值了第二个参数回调函数,也会在此时调用
对于FunctionComponent及相关类型,他会调用useLayoutEffect hook的回调函数,调度useEffect的销毁与回调函数
useLayoutEffect hook从上一次更新的销毁函数调用到本次更新的回调函数调用是同步执行的。而useEffect则需要先调度,在Layout阶段完成后再异步执行。这就是useLayoutEffect与useEffect的区别
对于HostRoot,即rootFiber,如果赋值了第三个参数回调函数,也会在此时调用
ReactDOM.render(<App />, document.querySelector("#root"), function() { console.log("i am mount~"); });
Update与Fiber的联系:
Update存在一个连接其他Update形成链表的字段next。联系React中另一种以链表形式组成的结构Fiber,他们之间有什么关联么?
Fiber节点组成Fiber树,页面中最多同时存在两棵Fiber树:
-
代表当前页面状态的current Fiber树
-
代表正在render阶段的workInProgress Fiber树
类似Fiber节点组成Fiber树,Fiber节点上的多个Update会组成链表并被包含在fiber.updateQueue中
Fiber节点最多同时存在两个updateQueue:
-
current fiber保存的updateQueue即current updateQueue
-
workInProgress fiber保存的updateQueue即workInProgress updateQueue
在commit阶段完成页面渲染后,workInProgress Fiber树变为current Fiber树,workInProgress Fiber树内Fiber节点的updateQueue就变成current updateQueue
第三个阶段
现在触发状态更新的fiber上已经包含Update对象。
我们知道,render阶段是从rootFiber开始向下遍历。那么如何从触发状态更新的fiber得到rootFiber呢?
答案是:调用markUpdateLaneFromFiberToRoot方法。
该方法做的工作可以概括为:从触发状态更新的fiber一直向上遍历到rootFiber,并返回rootFiber。
由于不同更新优先级不尽相同,所以过程中还会更新遍历到的fiber的优先级。
第四个阶段
现在我们拥有一个rootFiber,该rootFiber对应的Fiber树中某个Fiber节点包含一个Update。
接下来通知Scheduler根据更新的优先级,决定以同步还是异步的方式调度本次更新。
这里调用的方法是ensureRootIsScheduled。
以下是ensureRootIsScheduled最核心的一段代码
if (newCallbackPriority === SyncLanePriority) {
// 任务已经过期,需要同步执行render阶段
newCallbackNode = scheduleSyncCallback(
performSyncWorkOnRoot.bind(null, root)
);
} else {
// 根据任务优先级异步执行render阶段
var schedulerPriorityLevel = lanePriorityToSchedulerPriority(
newCallbackPriority
);
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root)
);
}
其中,scheduleCallback和scheduleSyncCallback会调用Scheduler提供的调度方法根据优先级调度回调函数执行。
可以看到,这里调度的回调函数为:
- performSyncWorkOnRoot.bind(null, root);
- performConcurrentWorkOnRoot.bind(null, root);
即render阶段的入口函数
render阶段
render阶段开始于performSyncWorkOnRoot或performConcurrentWorkOnRoot方法的调用。这取决于本次更新是同步更新还是异步更新
在这两个方法中会调用如下两个方法:
// performSyncWorkOnRoot会调用该方法
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
// performConcurrentWorkOnRoot会调用该方法
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
他们唯一的区别是是否调用shouldYield。如果当前浏览器帧没有剩余时间,shouldYield会中止循环,直到浏览器有空闲时间后再继续遍历。
workInProgress代表当前已创建的workInProgress fiber。
performUnitOfWork方法会创建下一个Fiber节点并赋值给workInProgress,并将workInProgress与已创建的Fiber节点连接起来构成Fiber树
Fiber Reconciler是从Stack Reconciler重构而来,通过遍历的方式实现可中断的递归,所以performUnitOfWork的工作可以分为两部分:“递”和“归”
递
首先从rootFiber开始向下深度优先遍历。为遍历到的每个Fiber节点调用beginWork方法 (opens new window)。
该方法会根据传入的Fiber节点创建子Fiber节点,并将这两个Fiber节点连接起来。
当遍历到叶子节点(即没有子组件的组件)时就会进入“归”阶段
归
在“归”阶段会调用completeWork (opens new window)处理Fiber节点。
当某个Fiber节点执行完completeWork,如果其存在兄弟Fiber节点(即fiber.sibling !== null),会进入其兄弟Fiber的“递”阶段。
如果不存在兄弟Fiber,会进入父级Fiber的“归”阶段。
“递”和“归”阶段会交错执行直到“归”到rootFiber。至此,render阶段的工作就结束了
effectList
作为DOM操作的依据,commit阶段需要找到所有有effectTag的Fiber节点并依次执行effectTag对应操作。难道需要在commit阶段再遍历一次Fiber树寻找effectTag !== null的Fiber节点么?
这显然是很低效的。
为了解决这个问题,在completeWork的上层函数completeUnitOfWork中,每个执行完completeWork且存在effectTag的Fiber节点会被保存在一条被称为effectList的单向链表中
effectList中第一个Fiber节点保存在fiber.firstEffect,最后一个元素保存在fiber.lastEffect。
类似appendAllChildren,在“归”阶段,所有有effectTag的Fiber节点都会被追加在effectList中,最终形成一条以rootFiber.firstEffect为起点的单向链表
在commit阶段只需要遍历effectList就能执行所有effect了。
借用React团队成员Dan Abramov的话:effectList相较于Fiber树,就像圣诞树上挂的那一串彩灯
commit阶段
commitRoot方法是commit阶段工作的起点。fiberRootNode会作为传参
在rootFiber.firstEffect上保存了一条需要执行副作用的Fiber节点的单向链表effectList,这些Fiber节点的updateQueue中保存了变化的props
这些副作用对应的DOM操作在commit阶段执行。
commit阶段的主要工作(即Renderer的工作流程)分为三部分:
-
before mutation阶段(执行DOM操作前)
-
mutation阶段(执行DOM操作)
-
layout阶段(执行DOM操作后)
before mutation之前主要做一些变量赋值,状态重置的工作
layout阶段执行完后,主要包括三点内容:
-
useEffect相关的处理。
-
性能追踪相关。源码里有很多和interaction相关的变量。他们都和追踪React渲染时间、性能相关,在Profiler API (opens new window)和DevTools (opens new window)中使用。
-
在commit阶段会触发一些生命周期钩子(如 componentDidXXX)和hook(如useLayoutEffect、useEffect)。在这些回调方法中可能触发新的更新,新的更新会开启新的render-commit流程
before mutation阶段
在before mutation阶段,会遍历effectList,依次执行:
-
处理DOM节点渲染/删除后的 autoFocus、blur逻辑
-
调用getSnapshotBeforeUpdate生命周期钩子
-
调度useEffect
mutation阶段
mutation阶段会遍历effectList,依次执行commitMutationEffects。该方法的主要工作为“根据effectTag调用不同的处理函数处理Fiber
layout阶段
layout阶段之所以称为layout,因为该阶段的代码都是在DOM渲染完成(mutation阶段完成)后执行的。
该阶段触发的生命周期钩子和hook可以直接访问到已经改变后的DOM,即该阶段是可以参与DOM layout的阶段
layout阶段会遍历effectList,依次执行commitLayoutEffects。该方法的主要工作为“根据effectTag调用不同的处理函数处理Fiber并更新ref