React 的源码与原理解读(七):commit 阶段
写在专栏开头(叠甲)
-
作者并不是前端技术专家,也只是一名喜欢学习新东西的前端技术小白,想要学习源码只是为了应付急转直下的前端行情和找工作的需要,这篇专栏是作者学习的过程中自己的思考和体会,也有很多参考其他教程的部分,如果存在错误或者问题,欢迎向作者指出,作者保证内容 100% 正确,请不要将本专栏作为参考答案。
-
本专栏的阅读需要你具有一定的 React 基础、 JavaScript 基础和前端工程化的基础,作者并不会讲解很多基础的知识点,例如:babel 是什么,jsx 的语法是什么,需要时请自行查阅相关资料。
-
本专栏很多部分参考了大量其他教程,若有雷同,那是作者抄袭他们的,所以本教程完全开源,你可以当成作者对各类教程进行了整合、总结并加入了自己的理解。
本一节的内容
本节的我们将谈谈 commit 中 React 的操作,因为在之前的教程中,我们已经讲到了 React 怎么样把 element 节点变成 Fiber 树,怎么样通过 DIFF 算法更新我们的 Fiber 树,那么现在我们需要将我们的 Fiber 树的更新同步到我们的真实 DOM 上展示给用户,这个阶段就是 Commit 阶段
从上个阶段说起
现在我们把我们的视线拉回到我们的第四篇教程 —— updateContainer
相关的部分,我们可以看到我们之前一笔带过的两个函数:performConcurrentWorkOnRoot
、 performSyncWorkOnRoot
,我们现在来看看他们的结构:
- 他们的逻辑大致相同,只不过是
ConcurrentWork
需要额外处理一下并发任务相关的进程调度和优先级的问题 - 通过教程 5 和 6 的学习我们知道了,在
renderRootSync
和renderRootConcurrent
中,我们生成了我们的 Fiber 树,所以在这两个函数之后,我们可以拿到我们的 Fiber 树了 - 在经过一些列的错误处理函数后(生成我们的 Fiber 很有可能出现问题)我们最终到达了
commitRoot
这个函数,这个函数传入我们刚刚生成的 Fiber 树的架构 。其中 sync 的任务直接调用,而 Concurrent 的任务因为需要通过finishConcurrentRender
这个函数,根据状态判定是不是完成了我们的渲染,然后调用commitRootWhenReady
(其中调用了 commitRoot)进入我们的commit
阶段(关于这两种任务的区别我们会后续单独来讲)
export function performSyncWorkOnRoot(root: FiberRoot): null {// 省略....flushPassiveEffects();// 优先级相关let lanes = getNextLanes(root, NoLanes);if (!includesSyncLane(lanes)) {ensureRootIsScheduled(root);return null;}// 生成 Fiber 的函数let exitStatus = renderRootSync(root, lanes);、//下面都是处理生成失败的处理if (root.tag !== LegacyRoot && exitStatus === RootErrored) {//...}if (exitStatus === RootFatalErrored) {//...}if (exitStatus === RootDidNotComplete) {//...}// 生成完毕,返回我们的 WorkInProgress 树的根节点const finishedWork: Fiber = (root.current.alternate: any);root.finishedWork = finishedWork;root.finishedLanes = lanes;// 进入 commit 阶段commitRoot(root,workInProgressRootRecoverableErrors,workInProgressTransitions,);ensureRootIsScheduled(root);return null;
}export function performConcurrentWorkOnRoot(root: FiberRoot,didTimeout: boolean,
): RenderTaskFn | null {// 省略处理优先级相关、调度相关的 ....const shouldTimeSlice =!includesBlockingLane(root, lanes) &&!includesExpiredLane(root, lanes) &&(disableSchedulerTimeoutInWorkLoop || !didTimeout);// 生成 Fiber 的函数let exitStatus = shouldTimeSlice? renderRootConcurrent(root, lanes): renderRootSync(root, lanes);//下面都是处理生成失败的处理....省略// 生成完毕,返回我们的 WorkInProgress 树的根节点root.finishedWork = finishedWork;root.finishedLanes = lanes;// 进入 commit 阶段finishConcurrentRender(root, exitStatus, finishedWork, lanes);}ensureRootIsScheduled(root);return getContinuationForRoot(root, originalCallbackNode);
}// finishConcurrentRender 判定是不是可以进入 commit 阶段,具体的分析之后我们会单独开一篇
function finishConcurrentRender(root: FiberRoot,exitStatus: RootExitStatus,finishedWork: Fiber,lanes: Lanes,
) {// 根据状态判定,switch (exitStatus) {case RootInProgress:case RootFatalErrored: {throw new Error('Root did not complete. This is a bug in React.');}case RootErrored: {commitRootWhenReady(root,finishedWork,workInProgressRootRecoverableErrors,workInProgressTransitions,lanes,);break;}case RootSuspended: {// 省略....commitRootWhenReady(root,finishedWork,workInProgressRootRecoverableErrors,workInProgressTransitions,lanes,);break;}case RootSuspendedWithDelay: {// 省略....commitRootWhenReady(root,finishedWork,workInProgressRootRecoverableErrors,workInProgressTransitions,lanes,);break;}case RootCompleted: {commitRootWhenReady(root,finishedWork,workInProgressRootRecoverableErrors,workInProgressTransitions,lanes,);break;}default: {throw new Error('Unknown root exit status.');}}
}
commitRoot
那么可见 commitRoot
这个函数就是我们在生成了我们的 Fiber 之后函数的统一入口,它又调用了 commitRootImpl
这个函数,我们现在来看看这个函数做了什么。首先需要明确,在 rootFiber.firstEffect 上保存了一条需要执行副作用的Fiber节点的单向链表 effectList ,这些 Fiber 节点的updateQueue 中保存了变化的props:
- 它首先执行 useEffect 回调和其他同步任务,因为他们可能出发新的渲染
- 之后为我们的变量赋值,并且进行 FiberRoot 状态重置的工作
- 之后它进行了三次遍历 effectList ,分别处理 BeforeMutationEffects,MutationEffects 和 LayoutEffects 三个阶段的处理:
BeforeMutationEffects
主要是更新class组件实例上的state、props 等,以及执行 getSnapshotBeforeUpdate 生命周期函数MutationEffects
主要是完成副作用的执行,主要包括重置文本节点以及真实 dom 节点的插入、删除和更新等操作。LayoutEffects
主要是去触发 componentDidMount、componentDidUpdate 以及各种回调函数等
- 最后,它还要处理 useEffect 相关的内容,因为在 commit 阶段会触发一些生命周期钩子,所以还要执行相关的同步任务
function commitRoot(root: FiberRoot,recoverableErrors: null | Array<CapturedValue<mixed>>,transitions: Array<Transition> | null,
) {const previousUpdateLanePriority = getCurrentUpdatePriority();const prevTransition = ReactCurrentBatchConfig.transition;try {ReactCurrentBatchConfig.transition = null;setCurrentUpdatePriority(DiscreteEventPriority);commitRootImpl(root,recoverableErrors,transitions,previousUpdateLanePriority,);} finally {ReactCurrentBatchConfig.transition = prevTransition;setCurrentUpdatePriority(previousUpdateLanePriority);}return null;
}function commitRootImpl(root: FiberRoot,recoverableErrors: null | Array<mixed>,transitions: Array<Transition> | null,renderPriorityLevel: EventPriority,
) {// 调用flushPassiveEffects执行完所有effect的任务do {flushPassiveEffects();} while (rootWithPendingPassiveEffects !== null);// 优先级调度 const finishedWork = root.finishedWork;const lanes = root.finishedLanes;if (enableSchedulingProfiler) {markCommitStarted(lanes);}if (finishedWork === null) {// 异常处理}//重置 FiberRoot root.finishedWork = null;root.finishedLanes = NoLanes;root.callbackNode = null;root.callbackPriority = NoLane;//省略优先级相关的....// 重置全局变量 if (root === workInProgressRoot) {workInProgressRoot = null;workInProgress = null;workInProgressRootRenderLanes = NoLanes;} else {}// 处理 useEffect相关内容if ((finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||(finishedWork.flags & PassiveMask) !== NoFlags) {if (!rootDoesHavePassiveEffects) {rootDoesHavePassiveEffects = true;pendingPassiveEffectsRemainingLanes = remainingLanes;pendingPassiveTransitions = transitions;scheduleCallback(NormalSchedulerPriority, () => {flushPassiveEffects();return null;});}}// 第一次遍历,执行 commitBeforeMutationEffectsconst subtreeHasEffects =(finishedWork.subtreeFlags &(BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==NoFlags;const rootHasEffect =(finishedWork.flags &(BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==NoFlags;if (subtreeHasEffects || rootHasEffect) {const prevTransition = ReactCurrentBatchConfig.transition;ReactCurrentBatchConfig.transition = null;const previousPriority = getCurrentUpdatePriority();setCurrentUpdatePriority(DiscreteEventPriority);const prevExecutionContext = executionContext;executionContext |= CommitContext;ReactCurrentOwner.current = null;const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(root,finishedWork,);if (enableProfilerTimer) {recordCommitTime();}if (enableProfilerTimer && enableProfilerNestedUpdateScheduledHook) {rootCommittingMutationOrLayoutEffects = root;}// 第二次遍历,执行 commitMutationEffectscommitMutationEffects(root, finishedWork, lanes);if (enableCreateEventHandleAPI) {if (shouldFireAfterActiveInstanceBlur) {afterActiveInstanceBlur();}}resetAfterCommit(root.containerInfo);root.current = finishedWork;if (enableSchedulingProfiler) {markLayoutEffectsStarted(lanes);}// 第三次遍历,执行 commitLayoutEffectscommitLayoutEffects(finishedWork, root, lanes);if (enableSchedulingProfiler) {markLayoutEffectsStopped();}if (enableProfilerTimer && enableProfilerNestedUpdateScheduledHook) {rootCommittingMutationOrLayoutEffects = null;}requestPaint();executionContext = prevExecutionContext;setCurrentUpdatePriority(previousPriority);ReactCurrentBatchConfig.transition = prevTransition;} else {// 没有任何副作用root.current = finishedWork;if (enableProfilerTimer) {recordCommitTime();}}const rootDidHavePassiveEffects = rootDoesHavePassiveEffects;// 处理 useEffect相关内容if (rootDoesHavePassiveEffects) {rootDoesHavePassiveEffects = false;rootWithPendingPassiveEffects = root;pendingPassiveEffectsLanes = lanes;} else {releaseRootPooledCache(root, remainingLanes);}// 省略优先级相关和DEV模式相关....// 触发一次新的调度,确保任何附加的任务被调度ensureRootIsScheduled(root, now());// 处理componentDidMount等生命周期或者useLayoutEffect等同步任务flushSyncCallbacks();return null;
}
BeforeMutationEffects
我们依次来看每个阶段,在BeforeMutationEffects
这个阶段,我们的来看看它做了什么:
-
这阶段会调用
commitBeforeMutationEffects
函数处理 -
commitBeforeMutationEffects
函数会依次调用commitBeforeMutationEffects_begin
,commitBeforeMutationEffects_complete
,commitBeforeMutationEffectsOnFiber
,getSnapShotBeforeUpdate
函数,也就是触发了getSnapShotBeforeUpdate
这个生命周期 -
commitBeforeMutationEffects_begin
函数里,获取了当前阶段阶段需要删除的孩子(在上一个阶段的 diff 中生成),然后对他们执行相关的操作,这里主要是处理DOM节点渲染/删除后的focus
和blur
逻辑。之后深度遍历直到没有孩子为止 -
commitBeforeMutationEffects_complete
中当我们遍历到的节点没有孩子,我们就遍历它的兄弟,也就是说commitBeforeMutationEffects_begin
函数和commitBeforeMutationEffects_complete
函数一起对我们的Fiber 进行了遍历(遍历的方式和之前的遍历 element 的时候大体一致) -
commitBeforeMutationEffectsOnFiber
则是用于处理每一个 Fiber,主要是对 ClassComponent 组件进行处理,更新实例上的state、props 等,以及执行 getSnapshotBeforeUpdate 生命周期函数:
export function commitBeforeMutationEffects(root: FiberRoot,firstChild: Fiber,
) {// 准备提交focusedInstanceHandle = prepareForCommit(root.containerInfo);nextEffect = firstChild;// 开始提交commitBeforeMutationEffects_begin();const shouldFire = shouldFireAfterActiveInstanceBlur;shouldFireAfterActiveInstanceBlur = false;focusedInstanceHandle = null;return shouldFire;
}function commitBeforeMutationEffects_begin() {while (nextEffect !== null) {const fiber = nextEffect;if (enableCreateEventHandleAPI) {// 获取需要删除的内容const deletions = fiber.deletions;if (deletions !== null) {for (let i = 0; i < deletions.length; i++) {const deletion = deletions[i];commitBeforeMutationEffectsDeletion(deletion);}}}const child = fiber.child;if ((fiber.subtreeFlags & BeforeMutationMask) !== NoFlags &&child !== null) {child.return = fiber;nextEffect = child;} else {// 提交结束commitBeforeMutationEffects_complete();}}
}
// 提交完毕
function commitBeforeMutationEffects_complete() {while (nextEffect !== null) {const fiber = nextEffect;setCurrentDebugFiberInDEV(fiber);try {commitBeforeMutationEffectsOnFiber(fiber);} catch (error) {captureCommitPhaseError(fiber, fiber.return, error);}resetCurrentDebugFiberInDEV();const sibling = fiber.sibling;if (sibling !== null) {sibling.return = fiber.return;nextEffect = sibling;return;}nextEffect = fiber.return;}
}function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {const current = finishedWork.alternate;const flags = finishedWork.flags;//...省略if ((flags & Snapshot) !== NoFlags) {setCurrentDebugFiberInDEV(finishedWork);switch (finishedWork.tag) {case FunctionComponent:case ForwardRef:case SimpleMemoComponent: {break;}case ClassComponent: {if (current !== null) {const prevProps = current.memoizedProps;const prevState = current.memoizedState;const instance = finishedWork.stateNode;//触发 getSnapshotBeforeUpdateconst snapshot = instance.getSnapshotBeforeUpdate(finishedWork.elementType === finishedWork.type? prevProps: resolveDefaultProps(finishedWork.type, prevProps),prevState,);instance.__reactInternalSnapshotBeforeUpdate = snapshot;}break;}case HostRoot: {if (supportsMutation) {const root = finishedWork.stateNode;clearContainer(root.containerInfo);}break;}case HostComponent:case HostText:case HostPortal:case IncompleteClassComponent:break;default: {throw new Error('This unit of work tag should not have side-effects. This error is ' +'likely caused by a bug in React. Please file an issue.',);}}resetCurrentDebugFiberInDEV();}
}
MutationEffects
前一个阶段只是做了一些准备工作,而在MutationEffects
这个阶段,我们就需要将我们的修改同步到我们的 DOM 上了,我们来看看它做了什么:
- 对不同类型的 fiber 在
MutationEffects
这个阶段会进行不同的处理,但有一些公共逻辑会执行的,那就是删除操作和插入操作 - 首先是调用
recursivelyTraverseMutationEffects
方法,这个方法会执行删除逻辑,完成删除逻辑后, - 接着就是调用
commitReconciliationEffects
,这个方法负责往真实 DOM 树中插入 DOM 节点 - 最后我们根据不同的组件再来做不同的操作
export function commitMutationEffects(root: FiberRoot,finishedWork: Fiber,committedLanes: Lanes,
) {inProgressLanes = committedLanes;inProgressRoot = root;setCurrentDebugFiberInDEV(finishedWork);、//直接进入 commitMutationEffectsOnFibercommitMutationEffectsOnFiber(finishedWork, root, committedLanes);setCurrentDebugFiberInDEV(finishedWork);inProgressLanes = null;inProgressRoot = null;
}function commitMutationEffectsOnFiber(finishedWork: Fiber,root: FiberRoot,lanes: Lanes,
) {// 获取 current 树的内容const current = finishedWork.alternate;// 获取元素被打上的标记const flags = finishedWork.flags;// 根据不同类别的元素做不同的擦操作switch (finishedWork.tag) {case FunctionComponent:case ForwardRef:case MemoComponent:case SimpleMemoComponent: {// 删除操作recursivelyTraverseMutationEffects(root, finishedWork, lanes);// 插入操作commitReconciliationEffects(finishedWork);// ....}case ClassComponent: {recursivelyTraverseMutationEffects(root, finishedWork, lanes);commitReconciliationEffects(finishedWork);// ....return;}case HostComponent: {recursivelyTraverseMutationEffects(root, finishedWork, lanes);commitReconciliationEffects(finishedWork);// ....return;}case HostText: {recursivelyTraverseMutationEffects(root, finishedWork, lanes);commitReconciliationEffects(finishedWork);// ....return;}case HostRoot: {recursivelyTraverseMutationEffects(root, finishedWork, lanes);commitReconciliationEffects(finishedWork);// ....return;}// .... 省略default: {recursivelyTraverseMutationEffects(root, finishedWork, lanes);commitReconciliationEffects(finishedWork);return;}}
}
我们先来看 recursivelyTraverseMutationEffects
的操作,它获取了之前我们上一个阶段存放在 deletions
中的孩子,然后依次调用了 commitDeletionEffects
方法,这个方法又会调用 commitDeletionEffectsOnFiber
这个函数,这个函数需要分类讨论:
- 对于原生组件,我们这样做:
- 先将它 ref 指向的元素设置为 null
- 向下遍历子节点,执行删除逻辑
- 最后调用 removeChild 方法删除真实 DOM
- 对于类组件
- 先将它 ref 指向的元素设置为 null
- 之后调用它的
componentWillUnmount
生命周期函数 - 向下遍历子节点,执行删除逻辑,因为它不对应真实 DOM 所以不需要删除,同时,它的孩子节点都是其他组件或者真实原生标签,所以只需要递归删除孩子节点即可
- 对于函数组件
- 我们需要遍历它的 updateQueue 执行 useInsertionEffect / useLayoutEffect 的回调函数所返回的函数(处理副作用)
- 向下遍历子节点,执行删除逻辑
function recursivelyTraverseMutationEffects(root: FiberRoot,parentFiber: Fiber,lanes: Lanes,
) {// 取出要删除的孩子const deletions = parentFiber.deletions;if (deletions !== null) {for (let i = 0; i < deletions.length; i++) {const childToDelete = deletions[i];try {// 删除孩子commitDeletionEffects(root, parentFiber, childToDelete);} catch (error) {captureCommitPhaseError(childToDelete, parentFiber, error);}}}
}function commitDeletionEffectsOnFiber(finishedRoot: FiberRoot,nearestMountedAncestor: Fiber,deletedFiber: Fiber,
) {onCommitUnmount(deletedFiber);switch (deletedFiber.tag) {case HostComponent: {if (!offscreenSubtreeWasHidden) {// ref 设置回 nullsafelyDetachRef(deletedFiber, nearestMountedAncestor);}}case HostText: {if (supportsMutation) {const prevHostParent = hostParent;const prevHostParentIsContainer = hostParentIsContainer;hostParent = null;// 往下遍历子节点,执行删除recursivelyTraverseDeletionEffects(finishedRoot,nearestMountedAncestor,deletedFiber,);hostParent = prevHostParent;hostParentIsContainer = prevHostParentIsContainer;// 删除真正的 DOM,调用了原生的 removeChild 方法if (hostParent !== null) {if (hostParentIsContainer) {removeChildFromContainer(((hostParent: any): Container),(deletedFiber.stateNode: Instance | TextInstance),);} else {removeChild(((hostParent: any): Instance),(deletedFiber.stateNode: Instance | TextInstance),);}}} else {recursivelyTraverseDeletionEffects(finishedRoot,nearestMountedAncestor,deletedFiber,);}return;}// ....// 函数组件case FunctionComponent:case ForwardRef:case MemoComponent:case SimpleMemoComponent: {if (!offscreenSubtreeWasHidden) {// 读取 updateQueue 队列,队列用链表的方式保存const updateQueue: FunctionComponentUpdateQueue | null = (deletedFiber.updateQueue: any);if (updateQueue !== null) {const lastEffect = updateQueue.lastEffect;if (lastEffect !== null) {const firstEffect = lastEffect.next;let effect = firstEffect;do {const {destroy, tag} = effect;if (destroy !== undefined) {if ((tag & HookInsertion) !== NoHookEffect) {// 处理 useInsertionEffect 副作用safelyCallDestroy(deletedFiber,nearestMountedAncestor,destroy,);} else if ((tag & HookLayout) !== NoHookEffect) {// 处理 useLayoutEffect 副作用if (enableSchedulingProfiler) {markComponentLayoutEffectUnmountStarted(deletedFiber);}if (enableProfilerTimer &&enableProfilerCommitHooks &&deletedFiber.mode & ProfileMode) {startLayoutEffectTimer();safelyCallDestroy(deletedFiber,nearestMountedAncestor,destroy,);recordLayoutEffectDuration(deletedFiber);} else {safelyCallDestroy(deletedFiber,nearestMountedAncestor,destroy,);}if (enableSchedulingProfiler) {markComponentLayoutEffectUnmountStopped();}}}effect = effect.next;} while (effect !== firstEffect);}}}// 遍历子节点执行删除逻辑recursivelyTraverseDeletionEffects(finishedRoot,nearestMountedAncestor,deletedFiber,);return;}// 类组件case ClassComponent: {if (!offscreenSubtreeWasHidden) {// 移除 refsafelyDetachRef(deletedFiber, nearestMountedAncestor);const instance = deletedFiber.stateNode;if (typeof instance.componentWillUnmount === 'function') {// 调用类组件实例的 componentWillUnmount 方法safelyCallComponentWillUnmount(deletedFiber,nearestMountedAncestor,instance,);}}//遍历子节点执行删除逻辑recursivelyTraverseDeletionEffects(finishedRoot,nearestMountedAncestor,deletedFiber,);return;}// ....省略default: {recursivelyTraverseDeletionEffects(finishedRoot,nearestMountedAncestor,deletedFiber,);return;}}
}
之后我们来看看另一个插入的逻辑 commitReconciliationEffects
,它只会在原生组件和 fiber 根节点上操作,并不操作函数组件和类组件,它的路逻辑是:
- 判断节点的父 Fiber 是不是需要重置,如果是则调用
parent.textContent = ''
来重置它的内容 - 之后寻找它有没有兄弟,如果有则调用
insertBefore
方法将内容插入到兄弟节点之前,如果没有,则调用父节点的appendChild
进行插入
function commitReconciliationEffects(finishedWork: Fiber) {const flags = finishedWork.flags;if (flags & Placement) {try {//执行操作commitPlacement(finishedWork);} catch (error) {captureCommitPhaseError(finishedWork, finishedWork.return, error);}// 移除 Placement 标志finishedWork.flags &= ~Placement;}if (flags & Hydrating) {finishedWork.flags &= ~Hydrating;}
}function commitPlacement(finishedWork: Fiber): void {if (!supportsMutation) {return;}//获取父 fiberconst parentFiber = getHostParentFiber(finishedWork);switch (parentFiber.tag) {case HostComponent: {const parent: Instance = parentFiber.stateNode;// 判断父 fiber 是否有 ContentReset(内容重置)标记if (parentFiber.flags & ContentReset) {// 通过 parent.textContent = '' 的方式重置resetTextContent(parent);parentFiber.flags &= ~ContentReset;}// 找它的下一个兄弟 DOM 节点,const before = getHostSibling(finishedWork);// 如果存在,用 insertBefore 方法;如果没有,就调用原生的 appendChild 方法insertOrAppendPlacementNode(finishedWork, before, parent);break;}case HostRoot:case HostPortal: {const parent: Container = parentFiber.stateNode.containerInfo;const before = getHostSibling(finishedWork);insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);break;}default:throw new Error('Invalid host parent fiber. This error is likely caused by a bug ' +'in React. Please file an issue.',);}
}
现在我们回到我们的 commitMutationEffectsOnFiber
,我们来看看在完成了删除和 插入操作后三中不同的节点又做了什么操作:
- 对于我们的类组件,我们不做处理
- 对于我们的函数组件,我们处理 useInsertionEffect ,useLayoutEffect 的副作用
- 对于原生组件,如果可以复用,我们调用
commitUpdate
执行更新逻辑,否则直接重置这个原生节点 - 如果是原生文本,我们调用
commitTextUpdate
函数,更新其文本内容
switch (finishedWork.tag) {case FunctionComponent:case ForwardRef:case MemoComponent:case SimpleMemoComponent: {// ....if (flags & Update) {try {// 找出 useInsertionEffect 的 destroy 方法去调用commitHookEffectListUnmount(HookInsertion | HookHasEffect,finishedWork,finishedWork.return,);// 执行 useInsertionEffect 的回调函数,并将返回值保存到 effect.destory 里。commitHookEffectListMount(HookInsertion | HookHasEffect,finishedWork,);} catch (error) {captureCommitPhaseError(finishedWork, finishedWork.return, error);}if (enableProfilerTimer &&enableProfilerCommitHooks &&finishedWork.mode & ProfileMode) {try {startLayoutEffectTimer();// 执行 useLayoutEffect 对应的 destroy 方法commitHookEffectListUnmount(HookLayout | HookHasEffect,finishedWork,finishedWork.return,);} catch (error) {captureCommitPhaseError(finishedWork, finishedWork.return, error);}recordLayoutEffectDuration(finishedWork);} else {try {commitHookEffectListUnmount(HookLayout | HookHasEffect,finishedWork,finishedWork.return,);} catch (error) {captureCommitPhaseError(finishedWork, finishedWork.return, error);}}}return;}// 类组件不会进行操作case ClassComponent: {// ...if (flags & Ref) {if (current !== null) {safelyDetachRef(current, current.return);}}return;}//原生组件case HostComponent: {//...if (flags & Ref) {if (current !== null) {safelyDetachRef(current, current.return);}}if (supportsMutation) {// 判断是不是需要重置if (finishedWork.flags & ContentReset) {const instance: Instance = finishedWork.stateNode;try {resetTextContent(instance);} catch (error) {captureCommitPhaseError(finishedWork, finishedWork.return, error);}}// 判断是不是需要更新if (flags & Update) {const instance: Instance = finishedWork.stateNode;if (instance != null) {const newProps = finishedWork.memoizedProps;const oldProps =current !== null ? current.memoizedProps : newProps;const type = finishedWork.type;const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);finishedWork.updateQueue = null;if (updatePayload !== null) {try {// 更新操作commitUpdate(instance,updatePayload,type,oldProps,newProps,finishedWork,);} catch (error) {captureCommitPhaseError(finishedWork,finishedWork.return,error,);}}}}}return;}//原生文本case HostText: {//....if (flags & Update) {if (supportsMutation) {if (finishedWork.stateNode === null) {throw new Error('This should have a text node initialized. This error is likely ' +'caused by a bug in React. Please file an issue.',);}const textInstance: TextInstance = finishedWork.stateNode;const newText: string = finishedWork.memoizedProps;const oldText: string =current !== null ? current.memoizedProps : newText;try {// 更新操作commitTextUpdate(textInstance, oldText, newText);} catch (error) {captureCommitPhaseError(finishedWork, finishedWork.return, error);}}}return;}//...
}
最后我们来看看这个更新函数,它的逻辑很简单:它获取了真实 dom 节点实例、props 以及 updateQueue ,将 props 的属性一一对应应用到真实 DOM 上
export function commitUpdate(domElement: Instance,updatePayload: Array<mixed>,type: string,oldProps: Props,newProps: Props,internalInstanceHandle: Object,
): void {// 对 props 的进行对比更新updateProperties(domElement, updatePayload, type, oldProps, newProps);// 应用更新updateFiberProps(domElement, newProps);
}
// 对比更新
export function updateProperties(domElement: Element,updatePayload: Array<any>,tag: string,lastRawProps: Object,nextRawProps: Object,
): void {// 针对表单组件进行特殊处理if (tag === 'input' &&nextRawProps.type === 'radio' &&nextRawProps.name != null) {ReactDOMInputUpdateChecked(domElement, nextRawProps);}// 判断是否为用户自定义的组件,即是否包含 "-"const wasCustomComponentTag = isCustomComponent(tag, lastRawProps);const isCustomComponentTag = isCustomComponent(tag, nextRawProps);// 将 diff 结果应用于真实 domupdateDOMProperties(domElement,updatePayload,wasCustomComponentTag,isCustomComponentTag,);// 针对表单的特殊处理switch (tag) {case 'input':ReactDOMInputUpdateWrapper(domElement, nextRawProps);break;case 'textarea':ReactDOMTextareaUpdateWrapper(domElement, nextRawProps);break;case 'select':ReactDOMSelectPostUpdateWrapper(domElement, nextRawProps);break;}
}function updateDOMProperties(domElement: Element,updatePayload: Array<any>,wasCustomComponentTag: boolean,isCustomComponentTag: boolean,
): void {// 对 updatePayload 遍历for (let i = 0; i < updatePayload.length; i += 2) {const propKey = updatePayload[i];const propValue = updatePayload[i + 1];if (propKey === STYLE) {// 处理 style 样式更新setValueForStyles(domElement, propValue);} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {// 处理 innerHTML 改变setInnerHTML(domElement, propValue);} else if (propKey === CHILDREN) {// 处理 textContentsetTextContent(domElement, propValue);} else {// 处理其他节点属性setValueForProperty(domElement, propKey, propValue, isCustomComponentTag);}}
}
LayoutEffects
最后我们来看看 LayoutEffects
这个阶段,这个阶段我们已经可以调用到我们的真实 DOM 了,它主要是执行 componentDidMount
和 componentDidUpdate
生命周期,我们来看看逻辑:
- 它的遍历过程和
BeforeMutationEffects
基本一致。都是使用了深度优先遍历的方式,这里我们不再具体阐述了,我们直接来看commitLayoutEffectOnFiber
这个函数 - 它还是分组件进行处理:
- 如果是函数组件,我们执行它的
useLayoutEffect
hooks 即可 - 如果是类组件,我们需要执行它的
componentDidMount
或者componentDidUpdate
生命周期,之后还要处理commitUpdateQueue
中的回调函数 - 如果根节点,我们需要处理其回调,也就是是
ReactDOM.render
的调回函数 - 如果是原生组件,我们处理其自动更新的情况
- 如果是函数组件,我们执行它的
commitUpdateQueue
函数会对 finishedQueue 上面的 effects 进行遍历,若有 callback,则执行 callback。同时会重置 finishedQueue 上面的 effects 为 null
function commitLayoutEffectOnFiber(finishedRoot: FiberRoot,current: Fiber | null,finishedWork: Fiber,committedLanes: Lanes,
): void {if ((finishedWork.flags & LayoutMask) !== NoFlags) {switch (finishedWork.tag) {case FunctionComponent:case ForwardRef:case SimpleMemoComponent: {if (!enableSuspenseLayoutEffectSemantics ||!offscreenSubtreeWasHidden) {if (enableProfilerTimer &&enableProfilerCommitHooks &&finishedWork.mode & ProfileMode) {try {startLayoutEffectTimer();// 提交 useLayoutEffect commitHookEffectListMount(HookLayout | HookHasEffect,finishedWork,);} finally {recordLayoutEffectDuration(finishedWork);}} else {commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);}}break;}case ClassComponent: {const instance = finishedWork.stateNode;if (finishedWork.flags & Update) {if (!offscreenSubtreeWasHidden) {if (current === null) {if (enableProfilerTimer &&enableProfilerCommitHooks &&finishedWork.mode & ProfileMode) {try {startLayoutEffectTimer();// 首次渲染,触发 componentDidMount 生命周期instance.componentDidMount();} finally {recordLayoutEffectDuration(finishedWork);}} else {instance.componentDidMount();}} else {const prevProps =finishedWork.elementType === finishedWork.type? current.memoizedProps: resolveDefaultProps(finishedWork.type,current.memoizedProps,);const prevState = current.memoizedState;if (enableProfilerTimer &&enableProfilerCommitHooks &&finishedWork.mode & ProfileMode) {try {startLayoutEffectTimer();// 非首次渲染,触发 componentDidUpdate 生命周期instance.componentDidUpdate(prevProps,prevState,instance.__reactInternalSnapshotBeforeUpdate,);} finally {recordLayoutEffectDuration(finishedWork);}} else {instance.componentDidUpdate(prevProps,prevState,instance.__reactInternalSnapshotBeforeUpdate,);}}}}const updateQueue: UpdateQueue<*,> | null = (finishedWork.updateQueue: any);if (updateQueue !== null) {// 执行 commitUpdateQueue 处理回调commitUpdateQueue(finishedWork, updateQueue, instance);}break;}case HostRoot: {const updateQueue: UpdateQueue<*,> | null = (finishedWork.updateQueue: any);if (updateQueue !== null) {let instance = null;if (finishedWork.child !== null) {switch (finishedWork.child.tag) {case HostComponent:instance = getPublicInstance(finishedWork.child.stateNode);break;case ClassComponent:instance = finishedWork.child.stateNode;break;}}// 调用 commitUpdateQueue 处理 ReactDOM.render 的回调commitUpdateQueue(finishedWork, updateQueue, instance);}break;}case HostComponent: {const instance: Instance = finishedWork.stateNode;// commitMount 处理 input 标签有 auto-focus 的情况if (current === null && finishedWork.flags & Update) {const type = finishedWork.type;const props = finishedWork.memoizedProps;commitMount(instance, type, props, finishedWork);}break;}case HostText: break;}case HostPortal: {break;}default:throw new Error('This unit of work tag should not have side-effects. This error is ' +'likely caused by a bug in React. Please file an issue.',);}}
}
总结
上述就是我们的 commit 阶段的内容,我们来总结一下它运行流程,大致可以分为五个阶段来说
- 在进入
BeforeMutationEffects
函数之前,我们先处理useEffect
相关的任务并且进行一些初始化的工作 - 之后进入
BeforeMutationEffects
阶段,我们深度优先遍历我们的 Fiber 树,处理DOM节点渲染/删除后的focus
和blur
逻辑,然后处理我们 clas 组件的getSnapshotBeforeUpdate
生命周期 - 之后我们进入
MutationEffects
阶段,这个阶段里,我们执行recursivelyTraverseMutationEffects
方法对需要删除的节点进行处理,再执行commitReconciliationEffects
逻辑插入DOM,之后分别进行处理- 对于我们的函数组件,我们处理 useInsertionEffect ,useLayoutEffect 的副作用
- 对于原生组件和文本,如果调用
commitUpdate
执行更新逻辑(复用)
- 最后我们进入
LayoutEffects
阶段,主要是执行类组件的componentDidMount
和componentDidUpdate
生命周期或者函数组件的useLayoutEffect
hooks ,然后对 finishedQueue 上面的 effects 的回调进行处理 - 三阶段结束后,再次处理
useEffect
相关的内容,再执行 commit 阶段相关的同步任务即可
经过上述的操作,React 的节点从我们构建的 Fiber 同步到了真实的 DOM 上展示给了用户,相关的生命周期和 Hooks 也得到了触发,至此,一个从 jsx 开始的 React 的渲染过程已经讲完了,我们之后会在讲完调度后再完整的来梳理一遍。
现在我们来看看还有什么问题没有解决:
- 我们在之前的讲解过默认跳过了优先级和调度相关的内容,这其实是一个很重要的阶段,
scheduler
阶段,之后我们会结合我们之前讲解渲染的过程来讲解我们的scheduler
是怎么安排任务和调度进程的 - 我们已经提到了部分的 hooks 的内容,但是并没有系统的讲到 hooks 是怎么起作用的,我们之后会单独开几篇来讲我们的 hooks 是怎么挂载和生效的
那么这两个问题我们会在后续的教程里给出解答