前言
本节我们通过 ReactDOM.render 渲染一个简单的函数组件来了解 React 底层的初渲染流程和运行机制。
- 代码示例:
import React from 'react';
import ReactDOM from 'react-dom';
function App() {
return 'Hello React!'
}
ReactDOM.render(<App />, window.root);
复制代码
2、代码调试:
打开浏览器控制台 – Sources – 找到 react-dom 文件,搜索 render 方法并在该方法内部添加断点,刷新页面即可进行调试。
3、JSX 编译转换
在进入 render 方法之前,babel 会解析和编译 JSX 语法元素,并处理成 React 可识别的语法和节点信息,本示例在 render 方法中的 element 数据格式为:
{
$$typeof: Symbol(react.element)
key: null
props: {}
ref: null
type: ƒ App()
}
复制代码
步骤一:创建应用 Fiber 树的根节点
function render(element, container, callback) {
return legacyRenderSubtreeIntoContainer(null, element, container, false, callback);
}
复制代码
在 legacRenderSubtreeIngoContainer 方法中首先会调用 legacCreateRootFromDOMContainer 创建整个应用的 Fiber 树(FiberRootNode),以及根容器对应的 Fiber 节点(HostRoot):
function legacyRenderSubtreeIntoContainer(parentComponent, children, container, forceHydrate, callback) {
let root = container._reactRootContainer; // 根据_reactRootContainer判断是否为初次渲染
let fiberRoot;
if (!root) {
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(container, forceHydrate);
fiberRoot = root._internalRoot;
// ...
unbatchedUpdates(() => {
updateContainer(children, fiberRoot, parentComponent, callback);
});
}
}
复制代码
在 legacCreateRootFromDOMContainer 中涉及到的相关函数调用栈层级较深,但核心是执行 createFiberRoot 这个方法来创建并返回 Fiber 树:
function createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks) {
const root = new FiberRootNode(containerInfo, tag, hydrate); // 创建 Fiber 应用树
const uninitializedFiber = createHostRootFiber(tag); // 创建根容器对应的 Fiber 节点
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;
initializeUpdateQueue(uninitializedFiber); // 为 fiber 创建 updateQueue
return root;
}
复制代码
步骤二:scheduler 调度阶段
React 整体工作流程分为三个阶段:
- schedule(调度):对产生的不同优先级任务进行排序。
- render(协调):根据优先级任务决定要更新哪些视图。
- commit(渲染):将需要改变的视图更新到视图中。
React 现在存在三种工作模式:
- Legacy 模式:通过
ReactDOM.render(<App>, rootNode)
创建,目前 React 默认使用的模式, - Blocking 模式:通过
ReactDOM.createBlockingRoot(rootNode).render(<App />)
创建,目前正在试验中,作为迁至 Concurrent 模式的第一个步骤; - Concurrent 模式:通过
ReactDOM.createRoot(rootNode).render(<App />)
创建,目前正在试验中,未来稳定之后,将作为 React 的默认使用模式,这个模式开启了所有的新功能。
而 schedule 调度阶段对任务的调度主要体现在 Concurrent 模式,而我们现在使用的 Legacy 模式在该阶段下没有做太多逻辑处理。
在上一步 legacRenderSubtreeIngoContainer 中创建 Fiber 应用后,会执行 updateContainer 准备进入 React 流程中的第一个阶段:scheduler(调度):
unbatchedUpdates(() => {
updateContainer(children, fiberRoot, parentComponent, callback);
});
export function unbatchedUpdates(fn, a) {
const prevExecutionContext = executionContext; // 获取上次的执行上下文
executionContext &= ~BatchedContext;
executionContext |= LegacyUnbatchedContext;
try {
return fn(a); // updateContainer
} finally {
executionContext = prevExecutionContext; // 恢复执行栈
}
}
复制代码
可以看到上面代码会先执行 unbatchedUpdates 修改当前执行栈为:LegacyUnbatchedContext(传统的同步模式),因为是初渲染,需要尽可能以最快的速度渲染到页面上,采用同步模式。
而在 updateContainer 中主要做的一件事情就是创建一个 update(每一个更新都对应一个update,这里的更新就是 element),然后调用 scheduleUpdateOnFiber 进入调度阶段:
export function updateContainer(element, container, parentComponent, callback) {
// ...
const update = createUpdate(eventTime, lane, suspenseConfig); // 创建一个update
update.payload = {element};
enqueueUpdate(current, update); // 将该update加入到fiber的updateQueue更新队列中,一个fiber对应多个update
scheduleUpdateOnFiber(current, lane, eventTime); // 开始进入schedule调度阶段
}
function enqueueUpdate(fiber, update) {
const updateQueue = fiber.updateQueue;
const sharedQueue = updateQueue.shared; // { pending: null }
const pending = sharedQueue.pending;
if (pending === null) {
update.next = update; // 环状单向链表
} else {
update.next = pending.next;
pending.next = update;
}
sharedQueue.pending = update;
}
复制代码
在 scheduleUpdateOnFiber 方法中对初渲染的任务调度很简单(不需要进行调度),因为是初次同步渲染,并且执行上下文为 LegacyUnbatchedContext 模式,所以会进入 performSyncWorkOnRoot 开始循环让每个任务进入 Work 阶段(Work 阶段可以分为:render和commit两个阶段)。
function scheduleUpdateOnFiber(fiber, lane, eventTime) {
// ...
if (lane === SyncLane) {
if (
(executionContext & LegacyUnbatchedContext) !== NoContext && // 初次渲染时执行unbatchedUpdates方法修改了executionContext
(executionContext & (RenderContext | CommitContext)) === NoContext // 并且当前还没有进入render或commit阶段
) {
// 从root根fiber开始进入reconciler阶段(render阶段)同步执行每一个任务
performSyncWorkOnRoot(root);
}
// ...
}
// ...
}
export function performSyncWorkOnRoot(root) {
// ...
// render 阶段
renderRootSync(root, lanes);
// commit 阶段
commitRoot(root);
}
复制代码
步骤三:render 渲染阶段
function renderRootSync(root, lanes) {
const prevExecutionContext = executionContext;
executionContext |= RenderContext; // 将当前执行栈改为Render阶段
if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
prepareFreshStack(root, lanes);
}
do {
try {
workLoopSync();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
executionContext = prevExecutionContext; // 恢复执行栈
// 在reconciler阶段workLoopSync中任务全部执行完毕后清空工作信息,后续进行commitRoot时是从fiberRootNode开始执行,而不是workInProgressRoot
workInProgressRoot = null;
workInProgressRootRenderLanes = NoLanes;
}
复制代码
在 renderRootSync 中首先会调用 prepareFreshStack 方法来初始化工作信息(React 内部工作流程上使用的全局变量):
function prepareFreshStack(root, lanes) {
root.finishedWork = null;
root.finishedLanes = NoLanes;
workInProgressRoot = root;
// 基于 HostRoot Fiber 创建一个 alternate(双工协议)作为当前工作的任务
workInProgress = createWorkInProgress(root.current, null);
// ...
}
function createWorkInProgress(current, pendingProps) {
let workInProgress = current.alternate;
if (workInProgress === null) {
// 基于 current 创建 Fiber Node
workInProgress = createFiber(current.tag, pendingProps, current.key, current.mode);
workInProgress.elementType = current.elementType;
workInProgress.type = current.type;
workInProgress.stateNode = current.stateNode;
workInProgress.alternate = current; // 双工协议
current.alternate = workInProgress;
} else {
workInProgress.pendingProps = pendingProps;
workInProgress.type = current.type;
workInProgress.effectTag = NoEffect;
workInProgress.nextEffect = null;
workInProgress.firstEffect = null;
workInProgress.lastEffect = null;
}
// ...
return workInProgress;
}
复制代码
接着在 workLoopSync 中循环调用 performUnitOfWork 处理每一个任务(一个 Fiber 节点对应一个任务):
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
function performUnitOfWork(unitOfWork) { // unit of work --> 工作单元
const current = unitOfWork.alternate;
// render - 递阶段
next = beginWork(current, unitOfWork, subtreeRenderLanes);
unitOfWork.memoizedProps = unitOfWork.pendingProps; // 更新props
// render - 归阶段(当该节点没有child节点,或者child已经处理完成后,进入归阶段)
if (next === null) {
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
}
复制代码
performUnitOfWork 的工作可以分为两部分:递阶段(beginWork)和 归阶段(completeUnitOfWork)
。
首先会调用 beginWork 将当前树上的节点和当前工作节点进行比较处理,并返回自己的子节点(如果有 child 的话)。
如果有子节点(next 值存在),则会重新进入 performUnitOfWork 让子节点执行 beginWork;如果没有则会调用 completeUnitOfWork 让当前节点进入归阶段。
beginWork 递阶段
beginWork 阶段的工作主要是传入当前 Fiber 节点,和渲染树上的节点(老节点)进行比较(Diff),并创建子 Fiber 节点。由于节点类型较多,将其分为多个 Case 交由不同的方法去处理。
export function beginWork(current, workInProgress, renderLanes) {
if (current !== null) { // update 阶段
// beginWork 首先判断是否可以复用节点,如果可以复用,执行 bailoutOnAlreadyFinishedWork 方法
}
switch (workInProgress.tag) {
case HostRoot: // 根节点
return updateHostRoot(current, workInProgress, renderLanes);
case HostComponent: // 原生节点
return updateHostComponent(current, workInProgress, renderLanes);
case HostText: // 文本节点
return updateHostText(current, workInProgress);
case IndeterminateComponent: { // 函数组件在mount时tag都是这个,在执行完beginWork后会将tag改为FunctionComponent
// 为什么函数组件在mount时会在这个函数中处理,而不是在FunctionComponent中处理?
return mountIndeterminateComponent(current, workInProgress, workInProgress.type, renderLanes);
}
case FunctionComponent: { // 函数在update阶段会执行这里的逻辑s
// ...
}
}
}
复制代码
beginWork – HostRoot 节点的处理
在初渲染时,第一个进入 begin 阶段的是 HostRoot 根节点,在 HostRoot 的更新队列 updateQueue 上拿到子节点(传入给 ReactDOM.render 方法的第一个参数),调用 reconCilechildren 创建子节点,并将子节点返回。
function updateHostRoot(current, workInProgress, renderLanes) {
const nextProps = workInProgress.pendingProps;
const prevState = workInProgress.memoizedState;
const prevChildren = prevState !== null ? prevState.element : null;
// clone current.updateQueue 给 workInProgress
cloneUpdateQueue(current, workInProgress);
// 从workInProgress.updateQueue中拿到update.payload(子节点),赋值给 workInProgress.memoizedState
processUpdateQueue(workInProgress, nextProps, null, renderLanes);
const nextState = workInProgress.memoizedState;
const nextChildren = nextState.element;
// 如果两个字节点相同,复用该节点,不需要创建新的fiber
if (nextChildren === prevChildren) {
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
// 否则开始创建子 fiber
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
复制代码
beginWork – reconcileChildren
reconcileChildren 会根据 current 是否有值(挂载/更新)来做不同处理(区别在于:第二参数 current.child 节点是否有值),最终会为当前 fiber 创建 child 节点。
mountChildFibers 和 reconcileChildFibers 指向同一个方法,内部通过 shouldTrackSideEffects 变量
来区分 mount 和 update。
function reconcileChildren(current, workInProgress, nextChildren, renderLanes) {
if (current === null) { // 尚未渲染的新组件,初次渲染
workInProgress.child =
mountChildFibers(workInProgress, null, nextChildren, renderLanes);
} else { // 更新渲染(HostRoot在初渲染会进入这里,其他节点只能在更新渲染时才会进入到这里)
workInProgress.child =
reconcileChildFibers(workInProgress, current.child, nextChildren, renderLanes);
}
}
复制代码
在 reconcileChildFibers 中会根据 nextChildren 的节点类型,交由不同的方法做处理,如:Fragment、单节点、多节点、纯文本节点等。
function reconcileChildFibers(returnFiber, currentFirstChild, newChild, lanes) {
// 如果 child fiber 类型是 fragment,则跳过它,处理它的子节点
const isUnkeyedTopLevelFragment = typeof newChild === 'object' && newChild !== null &&
newChild.type === REACT_FRAGMENT_TYPE && newChild.key === null;
if (isUnkeyedTopLevelFragment) {
newChild = newChild.props.children;
}
const isObject = typeof newChild === 'object' && newChild !== null;
if (isObject) { // 如果是一个对象,表示只有一个child元素,通过Single独生子女方式处理(不考虑sibling)
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement(returnFiber, currentFirstChild, newChild, lanes),
);
// ...
}
}
if (typeof newChild === 'string' || typeof newChild === 'number') {
return placeSingleChild(
reconcileSingleTextNode(returnFiber, currentFirstChild, '' + newChild, lanes),
);
}
if (isArray(newChild)) {
return reconcileChildrenArray(returnFiber, currentFirstChild, newChild, lanes);
}
// 其他情况都视为空,将父节点中所有的子节点都删除掉,让其成为一个空的元素
return deleteRemainingChildren(returnFiber, currentFirstChild);
}
复制代码
如果 child 子节点是单个元素节点的处理(非文本节点),会进入 reconcileSingleElement 进行处理(单节点 Diff)。通过老的 child 是否有值决定进行 Diff 还是新建节点。
function reconcileSingleElement(returnFiber, currentFirstChild, element, lanes) {
const key = element.key;
let child = currentFirstChild;
while (child !== null) {
// 通过key来判断,如果值不一样,删除节点
if (child.key === key) {
// key相同,接下来比较type是否相同
switch (child.tag) {
// ...
default: {
// type相同则表示可以复用
if (child.elementType === element.type) {
// type相同表示找到了相同的节点,因为这里是单节点处理,所以要标记删除视图上其他的兄弟节点
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props);
existing.ref = coerceRef(returnFiber, child, element);
existing.return = returnFiber;
return existing;
}
// type不同,则跳出循环
break;
}
}
// 代码执行到这里代表:key相同但是type不同
// 将该fiber及其兄弟fiber标记为删除,为什么要处理sibling呢?
// 因为视图上的子节点可能存在多个,而本次更新只有一个单节点,需要将多余的删除掉
deleteRemainingChildren(returnFiber, child);
break;
} else {
// 将该fiber标记为删除
deleteChild(returnFiber, child);
}
// 当前新节点只有一个,考虑到视图上的同级老节点可能是个多节点,
// 这里依次比较每个老sibling节点,看是否可以复用,所以也需要将sibling进行处理
child = child.sibling;
}
// 如果新增子节点,或者是经过 Diff 后发现老节点不可复用,在这里创建新节点
const created = createFiberFromElement(element, returnFiber.mode, lanes);
created.ref = coerceRef(returnFiber, currentFirstChild, element);
created.return = returnFiber;
return created;
}
复制代码
reconcileSingleElement 执行完成后返回 创建/复用 的 child 节点,并作为参数进入 placeSingleChild 方法,如果是 update 阶段,会标记 fiber 操作节点类型为 Placement(添加),对于本例来说,HostRoot 会为它的 newFiber(App 函数组件)增加 Placement 标识:
function placeSingleChild(newFiber) {
// shouldTrackSideEffects 存在表示update阶段
if (shouldTrackSideEffects && newFiber.alternate === null) {
newFiber.effectTag = Placement;
}
return newFiber;
}
复制代码
如果 child 子节点是文本节点,如我们这里的例子:函数组件返回一个普通的文本字符串:Hello React!
,所以在 reconcileChildFibers 中会使用 reconcileSingleTextNode 方法处理:
function reconcileSingleTextNode(returnFiber, currentFirstChild, textContent, lanes) {
// 如果说子节点对应的current fiber节点存在,并且也是文本节点,进行复用
if (currentFirstChild !== null && currentFirstChild.tag === HostText) {
deleteRemainingChildren(returnFiber, currentFirstChild.sibling);
const existing = useFiber(currentFirstChild, textContent);
existing.return = returnFiber;
return existing;
}
// 在 update 阶段会删除父节点中现有的内容,mount 阶段该方法不会进行删除处理
deleteRemainingChildren(returnFiber, currentFirstChild);
const created = createFiberFromText(textContent, returnFiber.mode, lanes); // 创建文本节点
created.return = returnFiber; // 建立关系
return created;
}
复制代码
内部实现比较简单:如果可以复用节点,则返回复用后的节点,否则创建一个新的文本 fiber 节点。
beginWork – 函数组件处理
对于函数组件,在创建对应的 fiber 阶段时,tag 类型赋值为 IndeterminateComponent(不确定组件),所以在 beginWork 方法中会命中 mountIndeterminateComponent 方法逻辑。
mountIndeterminateComponent 会对类组件和函数组件最不同处理,这里我们略过类组件,重点关注函数组件的处理方式。
function mountIndeterminateComponent(_current, workInProgress, Component, renderLanes) {
let value = renderWithHooks(null, workInProgress, Component, props, context, renderLanes); // 返回函数执行后的结果
// 对于本例,函数组件是HostRoot的child,在placeSingleChild中将 effectTag 设为 Update (2)
// 经过下面运算符合并后(函数组件完成工作后会标记 effectTag 或等于 PerformedWork 1),得到的 effectTag 为 3
workInProgress.effectTag |= PerformedWork; // PerformedWork 值是 1
// 省略 class 组件,只看函数组件的处理逻辑
workInProgress.tag = FunctionComponent;
reconcileChildren(null, workInProgress, value, renderLanes);
return workInProgress.child;
}
复制代码
可以看到 renderWithHooks 方法接收函数组件作为参数,并将返回值 value 作为 nextChild 传入 reconcileChildren,这里的 value 就是函数组件执行后 return 的结果,下面我们看看 renderWithHooks 内部是如何执行函数组件的。
function renderWithHooks(current, workInProgress, Component, props, secondArg, nextRenderLanes) {
currentlyRenderingFiber = workInProgress; // 保存当前要渲染的函数组件对应的fiber,后面执行函数内部的hooksAPI时会用到
// 决定hooks使用mount方式还是update方式
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
let children = Component(props, secondArg);
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
currentlyRenderingFiber = null;
// 重置函数组件中执行工作的hooks,腾出位置,方便给下一个函数组件使用。
currentHook = null;
workInProgressHook = null;
return children;
}
复制代码
首先根据 mount 或 update 来拿到不同的 Hooks 执行方法,即 ReactCurrentDispatcher,Hooks 的逻辑都是由这个对象提供而来;
接着执行 Component 来调用函数组件,函数执行完成,也会将内部的 Hooks 依次执行,最后恢复 ReactCurrentDispatcher 为初始值。
beginWork – HostText 文本节点的处理
本例中,函数组件会返回一个普通的文本字符串:Hello React!
会被创建 Text Fiber,然后再次进入 beginWork 函数中,因为文本节点不存在 child,所以在对文本节点的处理上会直接返回 null。
function updateHostText(current, workInProgress) {
// 因为它没有子节点,所以不需要进行下去,并返回null,让这个文本节点进入 completeUnitOfWork 归阶段
return null;
}
复制代码
completeUnitOfWork 归阶段
接着我们的例子看,Hello React!
文本节点没有子节点,所以会进入到归阶段,我们先来看一下这个方法的核心功能:
- 调用 completeWork 为当前 fiber 节点创建真实 DOM 节点;
- 将当前 fiber 节点上保存的 effect 列表添加到父节点之上;
- 如果有兄弟节点,将 sibling 节点作为 workInProgress 进入 begin 递阶段;
- 如果没有兄弟节点,则返回父节点,让父节点进入 complete 归阶段,之后再查找父节点的 sibling 即叔叔节点进入 beigin 递阶段。
export function completeUnitOfWork(unitOfWork) {
// 完成当前工作单元,然后转到下一个单元 sibling,如果没有 sibling,返回父节点查找叔叔
let completedWork = unitOfWork;
do {
const current = completedWork.alternate;
const returnFiber = completedWork.return;
// 该节点需要进行修改(effectTag === placement、delete、update)
if ((completedWork.effectTag & Incomplete) === NoEffect) {
let next = completeWork(current, completedWork, subtreeRenderLanes);
if (next !== null) { // 又有新的工作任务,执行新任务
workInProgress = next;
return;
}
if (returnFiber !== null && (returnFiber.effectTag & Incomplete) === NoEffect) {
// 1、将当前节点上存储的子节点 effect 添加到父节点上
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;
}
// 2、当前节点本身也具有effect,也添加到父节点上
const effectTag = completedWork.effectTag;
if (effectTag > PerformedWork) { // 当前fiber有DOM更新等操作
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork;
} else {
returnFiber.firstEffect = completedWork;
}
returnFiber.lastEffect = completedWork;
}
}
} else {
// TODO...
}
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) { // 存在sibling,让sibling进入begin递阶段
workInProgress = siblingFiber;
return;
}
completedWork = returnFiber;
workInProgress = completedWork; // 这里可以理解为更新 workInProgress 为 null
} while (completedWork !== null);
}
复制代码
在 completeWork 中主要处理:根节点、class类组件、原生节点、文本节点这几种类型元素,函数组件等类型都不做处理:
function completeWork(current, workInProgress, renderLanes) {
const newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
case IndeterminateComponent:
case LazyComponent:
case SimpleMemoComponent:
case FunctionComponent:
case ForwardRef:
case Fragment:
case Mode:
case Profiler:
case ContextConsumer:
case MemoComponent:
return null;
case ClassComponent: {
// ...
}
case HostRoot: {
// ...
}
case HostComponent: {
// ...
}
case HostText: {
// ...
}
}
}
复制代码
completeUnitOfWork – HostText 文本节点的处理
如果存在老节点并且比较后需要进行更新,则调用 updateHostText 标记节点 effect 为 Update;否则创建新节点。
case HostText: {
const newText = newProps;
if (current && workInProgress.stateNode != null) { // 更新文本操作
const oldText = current.memoizedProps;
updateHostText(current, workInProgress, oldText, newText); // 新旧文本不同时更新 workInProgress.effectTag = Update
} else {
const rootContainerInstance = getRootHostContainer();
const currentHostContext = getHostContext();
workInProgress.stateNode = createTextInstance(newText, rootContainerInstance, currentHostContext, workInProgress);
}
return null;
}
function updateHostText(current, workInProgress, oldText, newText) {
if (oldText !== newText) markUpdate(workInProgress);
}
function markUpdate(workInProgress) {
workInProgress.effectTag |= Update;
}
function createTextInstance(text, rootContainerInstance, hostContext, internalInstanceHandle) {
const textNode = createTextNode(text, rootContainerInstance);
textNode[internalInstanceKey] = internalInstanceHandle; // 将textFiber添加到text文本节点上
return textNode;
}
复制代码
completeUnitOfWork – 函数组件的处理
尽管说函数组件不需要创建节点,在 completeWork 直接返回出去了,但由于本例函数组件是 HostRoot 的根元素,它的 effectTag 为 3,所以在 completeUnitOfWork 方法中会将它的 effect 加入到父节点(HostRoot)的 effectList 上,即:
const effectTag = completedWork.effectTag;
if (effectTag > PerformedWork) { // effectTag > 1 说明有 effect 更新操作
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork;
} else {
returnFiber.firstEffect = completedWork;
}
returnFiber.lastEffect = completedWork;
}
// 由于当前 HostRoot 上没有任何 effect,所以上面代码执行后相当于:
// returnFiber.firstEffect = returnFiber.lastEffect = completedWork; // 将函数组件fiber节点加入 effectList
复制代码
completeUnitOfWork – HostRoot 根节点的处理
由于 HostRoot 对应的真实 DOM 就是我们的容器节点 id=root,所以不需要做 DOM 创建,这里主要做了调用栈的清空,并标记根节点的 effect 为:Snapshot(256)
case HostRoot: {
// 将 workInProgress 从执行栈中移除
popHostContainer(workInProgress);
popTopLevelLegacyContextObject(workInProgress);
const fiberRoot = workInProgress.stateNode;
// 当hostRoot进入completeUnitOfWork阶段时,表示render阶段将要结束,更新context对象信息
if (fiberRoot.pendingContext) {
fiberRoot.context = fiberRoot.pendingContext;
fiberRoot.pendingContext = null;
}
// 计划在下一次提交开始时清除此容器的效果。
// 因为是初次渲染,所以根节点的effectTag设为Snapshot,表示根节点也要进行更新,
// 而且在commitRoot的commitBeforeMutationEffects阶段,会进入处理Snapshot的方法,用于清空根容器下所有的子节点内容,
// 确保容器节点没有任何内容,方便后续将所以字节点挂载到容器节点上
workInProgress.effectTag |= Snapshot;
updateHostContainer(workInProgress); // 这是一个空方法
return null;
}
复制代码
步骤四:commit 提交阶段
在上面我们知道,在 performSyncWorkOnRoot 中会同步执行 RenderRootSync 方法来处理每个 fiber,render 阶段完成后,便会进入 commit 阶段将 DOM 节点渲染到视图上。
export function performSyncWorkOnRoot(root) {
renderRootSync(root, lanes); // render 阶段同步执行
const finishedWork = root.current.alternate; // 刚刚执行完的 workInProcess HostRoot Fiber
// 在commitRoot中会根据它来执行EffectList链表,遍历每一个effect并作出更新
// (effectList顺序是从最小的子节点到最顶层需要更新的元素形成)
root.finishedWork = finishedWork;
root.finishedLanes = lanes;
commitRoot(root);
return null;
}
export function commitRoot(root) {
const renderPriorityLevel = getCurrentPriorityLevel();
// 这里使用ImmediateSchedulerPriority最高优先级来执行commitRoot
runWithPriority(
ImmediateSchedulerPriority,
commitRootImpl.bind(null, root, renderPriorityLevel),
);
return null;
}
复制代码
在 commitRoot 中会根据优先级来执行 commit 提交处理,但核心逻辑在 commitRootImpl 这个方法中。
commitRootImpl 中的逻辑处理就很复杂了,它将 commit 阶段又划分为了三个阶段:
- 在进入三个阶段之前会做一些初始处理。。。
- Before Mutation 阶段(执行 DOM 操作前)
- Mutation 阶段(执行 DOM 操作)
- Layout 阶段(执行 DOM 操作后)
- 三个阶段完成之后做一些处理。。。
commit – 进入三个子阶段之前的处理
在 commit 阶段主要是根据 effectList 来处理要进行更新操作的节点。所以在进入 Before Mutation 阶段之前,会先处理 effectList 并拿到第一个要更新的节点:firstEffect,如果根节点上也存在 effectTag,将根节点追加到 effectList 尾部(初渲染会追加根节点)。
function commitRootImpl(root, renderPriorityLevel) {
// 取出Render阶段完成的工作树:finishedWork,上面保存了本次渲染要提交的 effectList
const finishedWork = root.finishedWork;
// 重置根节点上的变量信息
root.finishedWork = null;
root.finishedLanes = NoLanes;
root.callbackNode = null;
root.callbackId = NoLanes;
// 如果根节点上存在 effect,将其添加到 effectList 末尾(如初渲染时根节点的effectTag为:Snapshot=256)
// 接着取出 effectList 上的第一个节点作为开始更新节点
let firstEffect;
if (finishedWork.effectTag > PerformedWork) { // rootFiber存在更新操作
if (finishedWork.lastEffect !== null) {
finishedWork.lastEffect.nextEffect = finishedWork; // 初渲染时将根节点加入至 effectList 尾部,用于将视图挂载于容器节点
firstEffect = finishedWork.firstEffect;
} else {
firstEffect = finishedWork;
}
} else {
// 根节点没有effectTag,取出第一个effect执行commit阶段
firstEffect = finishedWork.firstEffect;
}
// ... 三个阶段的代码处理。
// ... 三个阶段之后的处理
}
复制代码
拿到 firstEffect 后,开始让每个 effect 进入 Before Mutation 阶段、Mutation 阶段、Layout 阶段处理任务。
function commitRootImpl(root, renderPriorityLevel) {
// ... 三个阶段进入之前的处理
if (firstEffect !== null) {
// 设置当前执行栈为 commit 阶段
executionContext |= CommitContext;
// 阶段一:Before Mutation 阶段
nextEffect = firstEffect; // nextEffect 是一个全部变量
do {
commitBeforeMutationEffects();
} while (nextEffect !== null);
// 阶段二:Mutation 阶段
nextEffect = firstEffect;
do {
commitMutationEffects(root, renderPriorityLevel);
} while (nextEffect !== null);
root.current = finishedWork; // Mutation阶段完成后更新到 current 上(因为节点依旧渲染到视图上了)
// 阶段三:Layout 阶段
nextEffect = firstEffect;
do {
commitLayoutEffects(root, lanes);
} while (nextEffect !== null);
nextEffect = null; // 重置全局变
executionContext = prevExecutionContext; // 恢复执行栈
} else {
// No effects.
root.current = finishedWork;
}
// ... 三个阶段执行完成之后的处理
}
复制代码
commit – Before Mutation 阶段
从上面的代码可以看到,从 firstEffect 开始,遍历 effect 依次进入 commitBeforeMutationEffects 中去处理。方法内部整体处理分为三部分:
- 处理 DOM 节点上的 autoFocus、blur 逻辑;
- 调用 getSnapshotBeforeUpdate 生命周期钩子函数(该函数用于代替以前的 ComponentWillXXX 钩子,它是在 commit 阶段下的 Before Mutation 阶段执行,因为是同步的,不会像那些钩子在可中断的 Render 阶段多次调用)。
- 调度 useEffect(调用 flushPassiveEffects 来异步调度 useEffect )。
function commitBeforeMutationEffects() {
while (nextEffect !== null) {
const current = nextEffect.alternate;
if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) {
// ...focus blur相关 处理DOM节点渲染/删除后的 autoFocus、blur 逻辑
}
const effectTag = nextEffect.effectTag;
// 调用getSnapshotBeforeUpdate生命周期钩子 class组件和初次挂载的HostRoot会进入到这里
if ((effectTag & Snapshot) !== NoEffect) {
commitBeforeMutationEffectOnFiber(current, nextEffect); // HostRoot做的事情是清除容器内的节点
}
// 调度useEffect
if ((effectTag & Passive) !== NoEffect) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
scheduleCallback(NormalSchedulerPriority, () => {
// 触发useEffect
flushPassiveEffects();
return null;
});
}
}
nextEffect = nextEffect.nextEffect;
}
}
复制代码
commit – Mutation 阶段
在 Mutation 阶段会进入 commitMutationEffects 方法中处理,这个阶段就会将更新渲染到视图上。内部会根据 effectTag 类型来为节点做不同处理:
- Placement effect:获取父级节点,获取兄弟节点(用于insertBefore、appendChild),将 Fiber 对应的 DOM 节点插入到页面父节点上。(期间可能会不断的查找父节点或子节点,因为函数组件没有真实节点)。
- Update effect:更新原生节点属性。
- Deletion effect:删除节点。
function commitMutationEffects(root, renderPriorityLevel) {
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
// ... 省略其他Tag的处理情况,如:refTag
// 利用运算符更快的判断出 effectTag 归属于哪一类,比如本例:函数组件的effectTag为3,3 & Placement(2) 后得到2,为函数组件执行插入操作
const primaryEffectTag = effectTag & (Placement | Update | Deletion | Hydrating);
switch (primaryEffectTag) {
// 插入DOM
case Placement: { // 2
commitPlacement(nextEffect);
// ~表示非,作用就是将effectTag置为0,此时后面的layout阶段中拿到的effectTag都变为0了,更新和删除并没有这样做
nextEffect.effectTag &= ~Placement;
break;
}
// 更新DOM
case Update: { // 4
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
// 删除DOM
case Deletion: { // 8
commitDeletion(root, nextEffect, renderPriorityLevel);
break;
}
}
nextEffect = nextEffect.nextEffect;
}
}
复制代码
对于本例的函数组件来说,它的 effectTag 为 3,会被作为 Placement 插入方式去处理,并调用 commitPlacement。
在这个方法内部会先找到函数组件的父节点,然后将函数组件的子节点添加到父节点上(函数组件不是一个真实DOM节点),此时本例中的 Hello React!
就挂载到了视图容器之上,这时候页面就看到了效果。
function commitPlacement(finishedWork) {
// 1、获取父级DOM节点。其中finishedWork为传入的Fiber节点
const parentFiber = getHostParentFiber(finishedWork);
let parent;
let isContainer; // 是否为容器
// 父级DOM节点
const parentStateNode = parentFiber.stateNode;
switch (parentFiber.tag) {
case HostComponent:
parent = parentStateNode; // 原生 DOM 节点
isContainer = false;
break;
case HostRoot:
parent = parentStateNode.containerInfo; // 容器节点
isContainer = true;
break;
// ...
default:
break;
}
// 2、获取Fiber节点的DOM兄弟节点
const before = getHostSibling(finishedWork);
// 3、根据DOM兄弟节点是否存在决定调用parentNode.insertBefore或parentNode.appendChild执行DOM插入操作。
if (isContainer) {
insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
} else {
insertOrAppendPlacementNode(finishedWork, before, parent);
}
}
复制代码
commit – Layout 阶段
在 Layout 阶段中会进入 commitLayoutEffects 方法,它一共做了两件事:
- 满足条件调用 class 组件和 Hooks 生命周期函数(componentDidMount、componentDidUpdate、uselayoutEffect),此时可以在钩子函数中拿到更新后的 DOM。
- 满足条件,赋值 ref 节点属性。
function commitLayoutEffects(root, committedLanes) {
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
// 1、调用生命周期钩子和hook
if (effectTag & (Update | Callback)) {
const current = nextEffect.alternate;
commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes);
}
// 2、赋值ref
if (effectTag & Ref) {
commitAttachRef(nextEffect);
}
nextEffect = nextEffect.nextEffect;
}
}
复制代码
commit – 三个子阶段完成之后的处理
最后如果函数组件中没有用到 Effect Hook,则主要做的工作是初始化处理,从 firstEffect 开始,遍历每个 effect 节点,并将 effect 标记从 fiber 节点上移除,以便下次更新时重新计算 effect 副作用。
还有一点就是处理生命周期回调中产生的更新,新的更新会开启新的 render-commit 流程。(如:componentDidMount 钩子函数中调用 this.setState)。
function commitRootImpl(root, renderPriorityLevel) {
// ... 三个阶段之前的处理
// ... 三个阶段的代码处理。
// 1、将 effectList 列表初始化为 null
const rootDidHavePassiveEffects = rootDoesHavePassiveEffects; // 根节点挂载后还有effect
if (rootDoesHavePassiveEffects) { // 处理 Effect Hook
rootDoesHavePassiveEffects = false;
rootWithPendingPassiveEffects = root; // 存在useEffect要处理,保存root
pendingPassiveEffectsLanes = lanes;
pendingPassiveEffectsRenderPriority = renderPriorityLevel;
} else { // 将effectList列表初始化为null
nextEffect = firstEffect;
while (nextEffect !== null) {
const nextNextEffect = nextEffect.nextEffect;
nextEffect.nextEffect = null;
if (nextEffect.effectTag & Deletion) {
detachFiberAfterEffects(nextEffect);
}
nextEffect = nextNextEffect;
}
}
// 2、开启新的更新流程
ensureRootIsScheduled(root, now());
flushSyncCallbackQueue();
}
function detachFiberAfterEffects(fiber) {
fiber.sibling = null;
}
复制代码
结尾
至此,一个简单的函数组件初渲染分析完成了。整体阅读下来代码还是很长的,文中如有需要纠正的地方,欢迎读者提出宝贵意见。