1
0
Fork 0
Obsidian 管理的个人笔记仓库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

7.9 KiB

React 的 commit 阶段从 commitRoot 这个函数开始,从 performSyncWorkOnRoot 函数中被调用,接收一个名为 root 的参数,这个 root 就是从 Fiber 递归中完成递归流程的 WorkInProgress Fiber 树

commitRoot 在 React 18 的代码和 React 17 有着较大的不同,但是最终的目的都是类似的:调用 commitRootImpl 函数,在 React 17 中,commitRootImpl 和一个优先级一起作为参数交由 runWithPriority 函数,而 React 18 中则是直接执行 commitRootImpl 函数

commitRoot 的代码很少,其中最主要的是执行 commitRootImpl 函数,也就是说 commit 阶段最核心的任务就发生在 commitRootImpl 中,在 React 18 中,commitRootImpl 函数发生了比较大的变化,以往 17 中,有三个主要的循环,这三个循环主要代表了 commit 的阶段的三个时刻:分别是 before、mutation 和 layout,也对应了三个函数 commitBeforeMutationEffect、commitMutationEffects 和commitLayoutEffects,18 中这三个函数依旧存在,但已经不是在循环被执行了,以下的文章内容会先从 React 17 开始,然后再探讨在 React 18 中发生的变化如果我不懒的话

commitRootImpl 内部有许多名字中带有 Interactive 的函数,这些函数逻辑和性能追踪有关,这篇文章里面会直接跳过

React 17

进入 commitRootImpl 函数内部,首先会执行一个 do..while 循环

do {
	flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null);

这个循环的跳出条件是 rootWithPendingPassiveEffects 等于 null,不然就执行 flushPassiveEffects 函数,那么 rootWithPendingPassiveEffects 是什么?为什么要执行 flushPassiveEffects 函数,这个 rootWithPendingPassiveEffects 就是带有 PassiveEffects 标记的链表,而 flushPassiveEffects 函数内部会遍历这个链表,然后执行其各自内部的 useEffect 中的回调函数,也就是说开头的循环是为了检查是否存在还未执行的 useEffect 回调函数,而这些回调函数有可能触发新的渲染,所以需要遍历直到没有任务

接下来会执行 flushRenderPhaseStrictModeWarningsInDEV 函数,这个函数从名字上看出之和开发环境下有关,负责 React 中的 StrictMode

flushRenderPhaseStrictModeWarningsInDEV();

中间跳过一段逻辑无关和 performance 监控代码,进入对 Fiber Tree 和 effecrList 的一系列初始化

......

root.finishedWork = null;
root.finishedLanes = NoLanes;

root.callbackNode = null;
root.callbackPriority = NoLane;

// 内部位运算
var remainingLanes = mergeLanes(
	finishedWork.lanes,
	finishedWork.childLanes
);

// 内部也是位运算
markRootFinished(root, remainingLanes);

if (rootsWithPendingDiscreteUpdates !== null) {
	if (
		!hasDiscreteLanes(remainingLanes) &&
		rootsWithPendingDiscreteUpdates.has(root)
	) {
		rootsWithPendingDiscreteUpdates.delete(root);
	}
}

if (root === workInProgressRoot) {
	workInProgressRoot = null;
	workInProgress = null;
	workInProgressRootRenderLanes = NoLanes;
}

// 以下省略

然后会开始处理 effectList,因为之前 completeWork 生成 effectList 的时候并没有处理 FiberNode ,所以这里需要判断 FiberNode 是否存在 effectTag,并将其加入到 effectList 的末尾

而这个 firstEffect,就会作为接下来三个阶段中被遍历的 effectList

var firstEffect;  
if (finishedWork.effectTag > PerformedWork) {
	if (finishedWork.lastEffect !== null) {
		finishedWork.lastEffect.nextEffect = finishedWork;
		firstEffect = finishedWork.firstEffect;
	} else {
		firstEffect = finishedWork;
	}
} 
else {
	firstEffect = finishedWork.firstEffect;
}

最终走到 commitRootImpl 的第一个主要循环

do {
	{
		 invokeGuardedCallback(null, commitBeforeMutationEffects, null);
		 if (hasCaughtError()) {
			 if (!(nextEffect !== null)) {
				 {
					 throw Error( "Should be working on an effect." );
				 }
			 }
			 var error = clearCaughtError();
			 captureCommitPhaseError(nextEffect, error);
			 nextEffect = nextEffect.nextEffect;
		 }
	}
} while (nextEffect !== null);

这个循环内部由 invokeGuardedCallback 执行 commitBeforeMutationEffects 函数,commitBeforeMutationEffects 就是开头说到的负责 before 阶段的函数,具体函数的深入可以看 React 的深入探索 - commitBeforeMutationEffects

然后根据 hasCaughtError 函数的返回值,执行 captureCommitPhaseError 函数,这个函数和 React 的 Error Boundaries (错误边界)有关,这里不展开谈,后边的两个阶段的逻辑里边也有着类似的逻辑,从这里可以看出来,Error Boundaries 会捕获 commit 阶段的错误

循环的跳出条件是 nextEffect 等于 null,也就是这个循环会遍历 effectList,后边的两个主要循环的跳出条件也是相同的

跳出 before 阶段的循环之后进入第二个主要循环:mutation

do {
	{
		invokeGuardedCallback(
			null,
			commitMutationEffects,
			null,
			root,
			renderPriorityLevel
		);
		if (hasCaughtError()) {
			if (!(nextEffect !== null)) {
				{
				 throw Error( "Should be working on an effect." );
				}
			}
			var _error = clearCaughtError();
			captureCommitPhaseError(nextEffect, _error);
			nextEffect = nextEffect.nextEffect;
		}
	}
} while (nextEffect !== null);

和 before 的循环非常类似,commitMutationEffects 也是有 invokeGuardedCallback 调用,也有着相同 Error Boundaries 的逻辑

关于 commitMutationEffects: React 的深入探索 - commitMutationEffects

最后一个阶段:layout 阶段

do {
	{
		invokeGuardedCallback(
			null,
			commitLayoutEffects,
			null,
			root,
			lanes
		);
		if (hasCaughtError()) {
			if (!(nextEffect !== null)) {
				{
					throw Error( "Should be working on an effect." );
				}
			}
			var _error2 = clearCaughtError();
			captureCommitPhaseError(nextEffect, _error2);
			nextEffect = nextEffect.nextEffect;
		}
	}
} while (nextEffect !== null);

关于 commitLayoutEffects: React 的深入探索 - commitLayoutEffects

结束三个循环之后 commit 阶段并没有结束,还会进入接下来的逻辑

var rootDidHavePassiveEffects = rootDoesHavePassiveEffects;

if (rootDoesHavePassiveEffects) {
	// 本次更新存在 useEffect
	rootDoesHavePassiveEffects = false;
	// 将 root 赋值给 rootWithPendingPassiveEffects,没错就是开头的循环
	rootWithPendingPassiveEffects = root;
	pendingPassiveEffectsLanes = lanes;
	pendingPassiveEffectsRenderPriority = renderPriorityLevel;
} else {
	// 本次更新不存在 useEffect
	nextEffect = firstEffect;
	while (nextEffect !== null) {
		var nextNextEffect = nextEffect.nextEffect;
		// 循环设置为 null,目的是为了垃圾回收
		nextEffect.nextEffect = null;
		if (nextEffect.effectTag & Deletion) {
			detachFiberAfterEffects(nextEffect);
		}
		nextEffect = nextNextEffect;
	}
}

从官方留下的注释中可以明白,这一段代码主要是是否进入了无限循环的更新当中

if (remainingLanes === SyncLane) {
	// Count the number of times the root synchronously re-renders without
	// finishing. If there are too many, it indicates an infinite update loop.
	// 翻译:计算根节点未完成同步重新呈现的次数。如果有太多,则表示无限更新循环。
	if (root === rootWithNestedUpdates) {
		nestedUpdateCount++;
	} else {
		nestedUpdateCount = 0;
		rootWithNestedUpdates = root;
	}
} else {
	nestedUpdateCount = 0;
}

这段代码是为了将当前的 root 重新调度一次,是因为在 commit 阶段有可能会产生新的更新

ensureRootIsScheduled(root, now());

React 内会存在一些同步的更新(useLayoutEffect 中的触发更新),React 会将此放在 flushSyncCallbackQueue 函数中在 commit 阶段同步的执行

flushSyncCallbackQueue();

至此, React 17 中 commit 阶段发生的事情就结束了

React 18

TODO