引言
作为一个天天用 React 开发的人,某天突发奇想,凭我的技术能力,居然只停留在用的方向,而不去研究它的源码?这怎么能说的过去(手动狗头🐶),于是,就踏上了研究 React 源码的道路。
大纲
为了尽可能讲的清晰易懂,打算采用总分总的形式来讲解,同时辅助截图来说明问题,下面先贴一张流程图。这是 React 从初始化到页面渲染出来的整个过程,有不对的欢迎在评论区指出。另外本人用的教材是卡老师的 React 设计原理, 如果文章中出现了雷同的,纯属我觉得卡老师写的比我好~
React的文件结构
GitHub - facebook/react: The library for web and native user interfaces.
分析的源码是 React18,React18 主要有三大部分构成,分别是reconciler,scheduler,react。
本次源码用的是卡老师分析编译出来的 js 版本,调试起来方便且读起来也好读一些。下面就按照流程图上的大纲串讲一遍,有些深度的计划分为不同的文章讲解。
createRoot
这是 React调用的第一个函数,下面一起来看 createRoot 做了什么
function createRoot(){
// ...
var root = createContainer(
container,
ConcurrentRoot,
hydrate,
hydrationCallbacks,
isStrictMode
);
var rootContainerElement =
container.nodeType === COMMENT_NODE ? container.parentNode : container;
listenToAllSupportedEvents
return new ReactDOMRoot(root);
}
源码我精简了一部分,主要就是三大部分,调用了 createContainer,创建了一个rootContainerElement,处理合成事件,以及返回了一个new ReactDOMRoot(root);
createContainer
createContainer内部调用了createFiberRoot,所以直接看这个方法,这个方法也很简单
function createFiberRoot(){
var root = new FiberRootNode(containerInfo, tag, hydrate);
// stateNode is any.
var uninitializedFiber = createHostRootFiber(tag, isStrictMode);
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;
{
var _initialState = {
element: null,
};
uninitializedFiber.memoizedState = _initialState;
}
initializeUpdateQueue(uninitializedFiber);
return root;
}
- 创建了一个 FiberRootNode 类型的 root,这个变量很重要!!!快记到小本本上。
- 创建了一个 HostRootFiber
- 将 root.current 指向uninitializedFiber
- 将uninitializedFiber的stateNode指向 root
- 初始化一个initializeUpdateQueue
-
- 这个方法内部生成了一个 queue 的对象
- 返回 root
FiberRootNode 长这样:
贴一段源码,感兴趣的打开看看
function FiberRootNode(containerInfo, tag, hydrate) {
this.tag = tag;
this.containerInfo = containerInfo;
this.pendingChildren = null;
this.current = null;
this.pingCache = null;
this.finishedWork = null;
this.timeoutHandle = noTimeout;
this.context = null;
this.pendingContext = null;
this.isDehydrated = hydrate;
this.callbackNode = null;
this.callbackPriority = NoLane;
this.eventTimes = createLaneMap(NoLanes);
this.expirationTimes = createLaneMap(NoTimestamp);
this.pendingLanes = NoLanes;
this.suspendedLanes = NoLanes;
this.pingedLanes = NoLanes;
this.expiredLanes = NoLanes;
this.mutableReadLanes = NoLanes;
this.finishedLanes = NoLanes;
this.entangledLanes = NoLanes;
this.entanglements = createLaneMap(NoLanes);
{
this.mutableSourceEagerHydrationData = null;
}
{
this.effectDuration = 0;
this.passiveEffectDuration = 0;
}
{
this.memoizedUpdaters = new Set();
var pendingUpdatersLaneMap = (this.pendingUpdatersLaneMap = []);
for (var i = 0; i < TotalLanes; i++) {
pendingUpdatersLaneMap.push(new Set());
}
}
{
switch (tag) {
case ConcurrentRoot:
this._debugRootType = hydrate ? "hydrateRoot()" : "createRoot()";
break;
case LegacyRoot:
this._debugRootType = hydrate ? "hydrate()" : "render()";
break;
}
}
}
HostFiberNode
互相指向
function initializeUpdateQueue(fiber) {
var queue = {
baseState: fiber.memoizedState,
firstBaseUpdate: null,
lastBaseUpdate: null,
shared: {
pending: null,
interleaved: null,
lanes: NoLanes,
},
effects: null,
};
fiber.updateQueue = queue;
}
生成一个 queue 对象,然后 fiber 的 updateQueue 指向queue。updateQueue是一个更新队列,后续会根据 updateQueue 进行更新。
listenToAllSupportedEvents
这个大概讲一下就是 React 用事件委托的方式将所有事件处理都委托到了 root 的 dom 节点上,自己为了抹平浏览器的差异,实现了一套事件的派发机制,由于比较复杂,计划单独出一篇文章讲一下。
ReactDOMRoot
function ReactDOMRoot(internalRoot) {
this._internalRoot = internalRoot;
}
这个方法也很简单,刚才不是生成一个FiberRootNode 吗,这个操作就是将_internalRoot赋值为 FiberRootNode,然后生成一个 ReactDOMRoot 实例。
总结
createRoot 方法生成了一个 FiberRootNode 以及 HostRootFiber,同时通过 current 和 stateNode 互相连接,然后实现了一套自己的事件合成机制,最后返回了一个ReactDOMRoot 对象实例。
Render
render 的代码比较简单,重点关注一下 updateContainer 就 OK
function render(){
updateContainer(children, root, null, null);
}
updateContainer
这个方法是比较重点的一个方法,分别获取了事件优先级,lane 优先级,标记了 render 以及获取了上下文对象,同时启动了一个 scheduler 的调度。
function updateContainer(){
var eventTime = requestEventTime();
var lane = requestUpdateLane(current$1);
var context = getContextForSubtree(parentComponent);
var root = scheduleUpdateOnFiber(current$1, lane, eventTime);
}
requestEventTime
function requestEventTime() {
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
// We're inside React, so it's fine to read the actual time.
return now();
} // We're not inside React, so we may be in the middle of a browser event.
if (currentEventTime !== NoTimestamp) {
// Use the same start time for all updates until we enter React again.
return currentEventTime;
} // This is the first update since React yielded. Compute a new start time.
currentEventTime = now();
return currentEventTime;
}
- 通过上下文判断,如果处于 render 上下文或 commit 上下文,就返回当前的时间。
- 默认兜底返回当前时间。
requestUpdateLane
function requestUpdateLane(fiber) {
// Special cases
var mode = fiber.mode;
if ((mode & ConcurrentMode) === NoMode) {
return SyncLane;
} else if (
(executionContext & RenderContext) !== NoContext &&
workInProgressRootRenderLanes !== NoLanes
) {
// This is a render phase update. These are not officially supported. The
// old behavior is to give this the same "thread" (lanes) as
// whatever is currently rendering. So if you call `setState` on a component
// that happens later in the same render, it will flush. Ideally, we want to
// remove the special case and treat them as if they came from an
// interleaved event. Regardless, this pattern is not officially supported.
// This behavior is only a fallback. The flag only exists until we can roll
// out the setState warning, since existing code might accidentally rely on
// the current behavior.
return pickArbitraryLane(workInProgressRootRenderLanes);
}
- 通过 mode 进行判断,如果是不是并发更新(React18新特性),返回异步优先级
- 否则返回最高优先级。
这块的思想有点复杂,后续降到 lane 的时候会着重讲这一块,现在知道这个函数返回的是最高优先级即可。
总结
updateContainer 函数生成了当前的任务时间以及任务优先级,然后还启动了一个调度任务。
scheduleUpdateOnFiber
这个函数主要的一个任务是通过ensureRootIsScheduled函数实际的去调度任务,准备进入reconcile阶段。
ensureRootIsScheduled
这个函数也是 React 中非常有灵魂的函数之一,下面直接看代码。主要有几个重要的函数,分别是markStarvedLanesAsExpired(用于标记饥饿任务) , scheduleMicrotask(调度异步任务) ,这种任务是不可以被打断的,一条路走到底。scheduleCallback$1(以并发模式调度任务,任务期间可打断,可恢复) 。
function ensureRootIsScheduled(root, currentTime) {
markStarvedLanesAsExpired(root, currentTime); // Determine the next lanes to work on, and their priority.
var nextLanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes
);
if (newCallbackPriority === SyncLane) {
// Special case: Sync React callbacks are scheduled on a special
// internal queue
if (root.tag === LegacyRoot) {
if (ReactCurrentActQueue$1.isBatchingLegacy !== null) {
ReactCurrentActQueue$1.didScheduleLegacyUpdate = true;
}
/*KaSong*/ logHook(
"scheduleCallback",
"legacySync",
performSyncWorkOnRoot.name
);
scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
} else {
/*KaSong*/ logHook(
"scheduleCallback",
"sync",
performSyncWorkOnRoot.name
);
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
}
{
// Flush the queue in a microtask.
if (ReactCurrentActQueue$1.current !== null) {
// Inside `act`, use our internal `act` queue so that these get flushed
// at the end of the current scope even when using the sync version
// of `act`.
ReactCurrentActQueue$1.current.push(flushSyncCallbacks);
} else {
/*KaSong*/ logHook(
"scheduleCallback",
"microtask",
flushSyncCallbacks.name
);
scheduleMicrotask(flushSyncCallbacks);
}
}
newCallbackNode = null;
} else {
var schedulerPriorityLevel;
switch (lanesToEventPriority(nextLanes)) {
case DiscreteEventPriority:
schedulerPriorityLevel = ImmediatePriority;
break;
case ContinuousEventPriority:
schedulerPriorityLevel = UserBlockingPriority;
break;
case DefaultEventPriority:
schedulerPriorityLevel = NormalPriority;
break;
case IdleEventPriority:
schedulerPriorityLevel = IdlePriority;
break;
default:
schedulerPriorityLevel = NormalPriority;
break;
}
/*KaSong*/ logHook(
"scheduleCallback",
"concurrent",
performConcurrentWorkOnRoot.name,
schedulerPriorityLevel
);
newCallbackNode = scheduleCallback$1(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root)
);
}
}
markStarvedLanesAsExpired
Scheduler会存在饥饿问题,这个函数是用来标记哪些任务处于饥饿,处于饥饿的就不可打断了。这块也放到 Scheduler 章节单独说把。
function markStarvedLanesAsExpired(root, currentTime) {
// TODO: This gets called every time we yield. We can optimize by storing
// the earliest expiration time on the root. Then use that to quickly bail out
// of this function.
var pendingLanes = root.pendingLanes;
var suspendedLanes = root.suspendedLanes;
var pingedLanes = root.pingedLanes;
var expirationTimes = root.expirationTimes; // Iterate through the pending lanes and check if we've reached their
// expiration time. If so, we'll assume the update is being starved and mark
// it as expired to force it to finish.
var lanes = pendingLanes;
while (lanes > 0) {
var index = pickArbitraryLaneIndex(lanes);
var lane = 1 << index;
var expirationTime = expirationTimes[index];
if (expirationTime === NoTimestamp) {
// Found a pending lane with no expiration time. If it's not suspended, or
// if it's pinged, assume it's CPU-bound. Compute a new expiration time
// using the current time.
if (
(lane & suspendedLanes) === NoLanes ||
(lane & pingedLanes) !== NoLanes
) {
// Assumes timestamps are monotonically increasing.
expirationTimes[index] = computeExpirationTime(lane, currentTime);
}
} else if (expirationTime <= currentTime) {
// This lane expired
root.expiredLanes |= lane;
/*KaSong*/ logHook("expiredLanes", lane);
}
lanes &= ~lane;
}
}
scheduleMicrotask
首先会根据newCallbackPriority(赛道模型)判断当前更新属于什么更新,如果属于异步更新,会进入scheduleMicrotask调度。下面来看一下它的源码:
var scheduleMicrotask =
typeof queueMicrotask === "function"
? queueMicrotask
: typeof localPromise !== "undefined"
? function (callback) {
return localPromise
.resolve(null)
.then(callback)
.catch(handleErrorInNextTick);
}
: scheduleTimeout; // TODO: Determine the best fallback here.
可以看到这个函数其实就是一个queueMicrotask,但是queueMicrotask可能有些环境没有,所以为了兼容各种环境,写了兼容代码。
function flushSyncCallbacks() {
if (!isFlushingSyncQueue && syncQueue !== null) {
// Prevent re-entrance.
isFlushingSyncQueue = true;
var i = 0;
var previousUpdatePriority = getCurrentUpdatePriority();
try {
var isSync = true;
var queue = syncQueue; // TODO: Is this necessary anymore? The only user code that runs in this
// queue is in the render or commit phases.
setCurrentUpdatePriority(DiscreteEventPriority);
for (; i < queue.length; i++) {
var callback = queue[i];
do {
callback = callback(isSync);
} while (callback !== null);
}
syncQueue = null;
includesLegacySyncCallbacks = false;
} catch (error) {
// If something throws, leave the remaining callbacks on the queue.
if (syncQueue !== null) {
syncQueue = syncQueue.slice(i + 1);
} // Resume flushing in the next tick
scheduleCallback(ImmediatePriority, flushSyncCallbacks);
throw error;
} finally {
setCurrentUpdatePriority(previousUpdatePriority);
isFlushingSyncQueue = false;
}
}
return null;
}
scheduleMicrotask(flushSyncCallbacks);
将一个调度任务变成微任务 push 进微任务队列。flushSyncCallbacks主要的作用是取出syncQueue里面的调度任务挨个进行调度,可以看到是一个循环走到底,所以是不可打断的。
总结
这个阶段属于 Schedule 调度阶段,在 React18 存在并发更新且有着可中断,可恢复的特性,会存在着饥饿任务的问题,为了解决这个问题,React 会在每次更新之前进行调度任务的标记,然后通过 lane 进行判断,如果是异步更新,走异步更新的任务调度流程,如果是并发更新走并发更新的调度流程。
performSyncWorkOnRoot
这个阶段是reconcile调和阶段,主要的作用就是生成 Fiber 树,然后渲染到页面上。
function performSyncWorkOnRoot(root){
// ...
// 构建离屏的 Fiber 树
var exitStatus = renderRootSync(root, lanes);
// 进入 commit 提交阶段
commitRoot(root);
}
主要有两个函数,第一个是将虚拟 dom 变成 React 中的 Fiber 结构,然后生成 Fiber 树。第二个函数是将构建出来的Fiber 树呈现在页面上。
renderRootSync
这个函数处理了一些边界条件,主要生效的函数是performUnitOfWork,下面来直接分析performUnitOfWork函数。
function performUnitOfWork(){
// ...
next = beginWork$1(current, unitOfWork, subtreeRenderLanes);
// ...
completeUnitOfWork(unitOfWork);
}
主要就是两部分,第一部分是 beginWork,第二部分是completeUnitOfWork,下面分别看下这两个函数做了什么。
beginWork
function beginWork(){
// ...
switch (workInProgress.tag) {
case IndeterminateComponent: {
return mountIndeterminateComponent(
current,
workInProgress,
workInProgress.type,
renderLanes
);
}
case LazyComponent: {
var elementType = workInProgress.elementType;
return mountLazyComponent(
current,
workInProgress,
elementType,
renderLanes
);
}
case FunctionComponent: {
var Component = workInProgress.type;
var unresolvedProps = workInProgress.pendingProps;
var resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes
);
}
case ClassComponent: {
var _Component = workInProgress.type;
var _unresolvedProps = workInProgress.pendingProps;
var _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$1(current, workInProgress, renderLanes);
case HostText:
return updateHostText$1(current, workInProgress);
case SuspenseComponent:
return updateSuspenseComponent(current, workInProgress, renderLanes);
case HostPortal:
return updatePortalComponent(current, workInProgress, renderLanes);
case ForwardRef: {
var type = workInProgress.type;
var _unresolvedProps2 = workInProgress.pendingProps;
var _resolvedProps2 =
workInProgress.elementType === type
? _unresolvedProps2
: resolveDefaultProps(type, _unresolvedProps2);
return updateForwardRef(
current,
workInProgress,
type,
_resolvedProps2,
renderLanes
);
}
case Fragment:
return updateFragment(current, workInProgress, renderLanes);
case Mode:
return updateMode(current, workInProgress, renderLanes);
case Profiler:
return updateProfiler(current, workInProgress, renderLanes);
case ContextProvider:
return updateContextProvider(current, workInProgress, renderLanes);
case ContextConsumer:
return updateContextConsumer(current, workInProgress, renderLanes);
case MemoComponent: {
var _type2 = workInProgress.type;
var _unresolvedProps3 = workInProgress.pendingProps; // Resolve outer props first, then resolve inner props.
var _resolvedProps3 = resolveDefaultProps(_type2, _unresolvedProps3);
{
if (workInProgress.type !== workInProgress.elementType) {
var outerPropTypes = _type2.propTypes;
if (outerPropTypes) {
checkPropTypes(
outerPropTypes,
_resolvedProps3, // Resolved for outer only
"prop",
getComponentNameFromType(_type2)
);
}
}
}
_resolvedProps3 = resolveDefaultProps(_type2.type, _resolvedProps3);
return updateMemoComponent(
current,
workInProgress,
_type2,
_resolvedProps3,
renderLanes
);
}
case SimpleMemoComponent: {
return updateSimpleMemoComponent(
current,
workInProgress,
workInProgress.type,
workInProgress.pendingProps,
renderLanes
);
}
case IncompleteClassComponent: {
var _Component2 = workInProgress.type;
var _unresolvedProps4 = workInProgress.pendingProps;
var _resolvedProps4 =
workInProgress.elementType === _Component2
? _unresolvedProps4
: resolveDefaultProps(_Component2, _unresolvedProps4);
return mountIncompleteClassComponent(
current,
workInProgress,
_Component2,
_resolvedProps4,
renderLanes
);
}
case SuspenseListComponent: {
return updateSuspenseListComponent(
current,
workInProgress,
renderLanes
);
}
case ScopeComponent: {
break;
}
case OffscreenComponent: {
return updateOffscreenComponent(current, workInProgress, renderLanes);
}
case LegacyHiddenComponent: {
return updateLegacyHiddenComponent(
current,
workInProgress,
renderLanes
);
}
}
}
这里主要就是通过workInProgress的 tag 生成不同的 Fiber 节点。然后将生成的节点赋值给 next,用来判断是否遍历到叶子节点。具体怎么生成的后续单独展开讲一下吧~
completeUnitOfWork
function completeUnitOfWork(){
next = completeWork(current, completedWork, subtreeRenderLanes);
}
switch (workInProgress.tag) {
case IndeterminateComponent:
case LazyComponent:
case SimpleMemoComponent:
case FunctionComponent:
case ForwardRef:
case Fragment:
case Mode:
case Profiler:
case ContextConsumer:
case MemoComponent:
bubbleProperties(workInProgress);
return null;
case ClassComponent: {
var Component = workInProgress.type;
if (isContextProvider(Component)) {
popContext(workInProgress);
}
bubbleProperties(workInProgress);
return null;
}
case HostRoot: {
var fiberRoot = workInProgress.stateNode;
popHostContainer(workInProgress);
popTopLevelContextObject(workInProgress);
resetWorkInProgressVersions();
if (fiberRoot.pendingContext) {
fiberRoot.context = fiberRoot.pendingContext;
fiberRoot.pendingContext = null;
}
if (current === null || current.child === null) {
// If we hydrated, pop so that we can delete any remaining children
// that weren't hydrated.
var wasHydrated = popHydrationState(workInProgress);
if (wasHydrated) {
// If we hydrated, then we'll need to schedule an update for
// the commit side-effects on the root.
markUpdate(workInProgress);
} else if (!fiberRoot.isDehydrated) {
// Schedule an effect to clear this container at the start of the next commit.
// This handles the case of React rendering into a container with previous children.
// It's also safe to do for updates too, because current.child would only be null
// if the previous render was null (so the container would already be empty).
workInProgress.flags |= Snapshot;
}
}
updateHostContainer(current, workInProgress);
bubbleProperties(workInProgress);
return null;
}
case HostComponent: {
popHostContext(workInProgress);
var rootContainerInstance = getRootHostContainer();
var type = workInProgress.type;
// 如果是复用的节点,那么会走到这里,在 这里创建一个更新队列,进行数据的更新。 然后将副作用冒泡之后,直接 return
// 如果 current= null 说明是新创建的节点,走创建流程即可。
if (current !== null && workInProgress.stateNode != null) {
updateHostComponent(
current,
workInProgress,
type,
newProps,
rootContainerInstance
);
if (current.ref !== workInProgress.ref) {
markRef(workInProgress);
}
} else {
if (!newProps) {
if (workInProgress.stateNode === null) {
throw new Error(
"We must have new props for new mounts. This error is likely " +
"caused by a bug in React. Please file an issue."
);
} // This can happen when we abort work.
bubbleProperties(workInProgress);
return null;
}
var currentHostContext = getHostContext(); // TODO: Move createInstance to beginWork and keep it on a context
// "stack" as the parent. Then append children as we go in beginWork
// or completeWork depending on whether we want to add them top->down or
// bottom->up. Top->down is faster in IE11.
var _wasHydrated = popHydrationState(workInProgress);
if (_wasHydrated) {
// TODO: Move this and createInstance step into the beginPhase
// to consolidate.
if (
prepareToHydrateHostInstance(
workInProgress,
rootContainerInstance,
currentHostContext
)
) {
// If changes to the hydrated node need to be applied at the
// commit-phase we mark this as such.
markUpdate(workInProgress);
}
} else {
var instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress
);
appendAllChildren(instance, workInProgress, false, false);
workInProgress.stateNode = instance; // Certain renderers require commit-time effects for initial mount.
// (eg DOM renderer supports auto-focus for certain elements).
// Make sure such renderers get scheduled for later work.
if (
finalizeInitialChildren(
instance,
type,
newProps,
rootContainerInstance
)
) {
markUpdate(workInProgress);
}
}
if (workInProgress.ref !== null) {
// If there is a ref on a host node we need to schedule a callback
markRef(workInProgress);
}
}
bubbleProperties(workInProgress);
return null;
}
case HostText: {
var newText = newProps;
if (current && workInProgress.stateNode != null) {
var oldText = current.memoizedProps; // If we have an alternate, that means this is an update and we need
// to schedule a side-effect to do the updates.
updateHostText(current, workInProgress, oldText, newText);
} else {
if (typeof newText !== "string") {
if (workInProgress.stateNode === null) {
throw new Error(
"We must have new props for new mounts. This error is likely " +
"caused by a bug in React. Please file an issue."
);
} // This can happen when we abort work.
}
var _rootContainerInstance = getRootHostContainer();
var _currentHostContext = getHostContext();
var _wasHydrated2 = popHydrationState(workInProgress);
if (_wasHydrated2) {
if (prepareToHydrateHostTextInstance(workInProgress)) {
markUpdate(workInProgress);
}
} else {
workInProgress.stateNode = createTextInstance(
newText,
_rootContainerInstance,
_currentHostContext,
workInProgress
);
}
}
bubbleProperties(workInProgress);
return null;
}
case SuspenseComponent: {
popSuspenseContext(workInProgress);
var nextState = workInProgress.memoizedState;
{
if (nextState !== null && nextState.dehydrated !== null) {
// We might be inside a hydration state the first time we're picking up this
// Suspense boundary, and also after we've reentered it for further hydration.
var _wasHydrated3 = popHydrationState(workInProgress);
if (current === null) {
if (!_wasHydrated3) {
throw new Error(
"A dehydrated suspense component was completed without a hydrated node. " +
"This is probably a bug in React."
);
}
prepareToHydrateHostSuspenseInstance(workInProgress);
bubbleProperties(workInProgress);
{
if ((workInProgress.mode & ProfileMode) !== NoMode) {
var isTimedOutSuspense = nextState !== null;
if (isTimedOutSuspense) {
// Don't count time spent in a timed out Suspense subtree as part of the base duration.
var primaryChildFragment = workInProgress.child;
if (primaryChildFragment !== null) {
// $FlowFixMe Flow doesn't support type casting in combination with the -= operator
workInProgress.treeBaseDuration -=
primaryChildFragment.treeBaseDuration;
}
}
}
}
return null;
} else {
// We might have reentered this boundary to hydrate it. If so, we need to reset the hydration
// state since we're now exiting out of it. popHydrationState doesn't do that for us.
resetHydrationState();
if ((workInProgress.flags & DidCapture) === NoFlags) {
// This boundary did not suspend so it's now hydrated and unsuspended.
workInProgress.memoizedState = null;
} // If nothing suspended, we need to schedule an effect to mark this boundary
// as having hydrated so events know that they're free to be invoked.
// It's also a signal to replay events and the suspense callback.
// If something suspended, schedule an effect to attach retry listeners.
// So we might as well always mark this.
workInProgress.flags |= Update;
bubbleProperties(workInProgress);
{
if ((workInProgress.mode & ProfileMode) !== NoMode) {
var _isTimedOutSuspense = nextState !== null;
if (_isTimedOutSuspense) {
// Don't count time spent in a timed out Suspense subtree as part of the base duration.
var _primaryChildFragment = workInProgress.child;
if (_primaryChildFragment !== null) {
// $FlowFixMe Flow doesn't support type casting in combination with the -= operator
workInProgress.treeBaseDuration -=
_primaryChildFragment.treeBaseDuration;
}
}
}
}
return null;
}
}
}
if ((workInProgress.flags & DidCapture) !== NoFlags) {
// Something suspended. Re-render with the fallback children.
workInProgress.lanes = renderLanes; // Do not reset the effect list.
if ((workInProgress.mode & ProfileMode) !== NoMode) {
transferActualDuration(workInProgress);
} // Don't bubble properties in this case.
return workInProgress;
}
var nextDidTimeout = nextState !== null;
var prevDidTimeout = false;
if (current === null) {
popHydrationState(workInProgress);
} else {
var prevState = current.memoizedState;
prevDidTimeout = prevState !== null;
}
// an effect to toggle the subtree's visibility. When we switch from
// fallback -> primary, the inner Offscreen fiber schedules this effect
// as part of its normal complete phase. But when we switch from
// primary -> fallback, the inner Offscreen fiber does not have a complete
// phase. So we need to schedule its effect here.
//
// We also use this flag to connect/disconnect the effects, but the same
// logic applies: when re-connecting, the Offscreen fiber's complete
// phase will handle scheduling the effect. It's only when the fallback
// is active that we have to do anything special.
if (nextDidTimeout && !prevDidTimeout) {
var _offscreenFiber = workInProgress.child;
_offscreenFiber.flags |= Visibility; // TODO: This will still suspend a synchronous tree if anything
// in the concurrent tree already suspended during this render.
// This is a known bug.
if ((workInProgress.mode & ConcurrentMode) !== NoMode) {
// TODO: Move this back to throwException because this is too late
// if this is a large tree which is common for initial loads. We
// don't know if we should restart a render or not until we get
// this marker, and this is too late.
// If this render already had a ping or lower pri updates,
// and this is the first time we know we're going to suspend we
// should be able to immediately restart from within throwException.
var hasInvisibleChildContext =
current === null &&
workInProgress.memoizedProps.unstable_avoidThisFallback !==
true;
if (
hasInvisibleChildContext ||
hasSuspenseContext(
suspenseStackCursor.current,
InvisibleParentSuspenseContext
)
) {
// If this was in an invisible tree or a new render, then showing
// this boundary is ok.
renderDidSuspend();
} else {
// Otherwise, we're going to have to hide content so we should
// suspend for longer if possible.
renderDidSuspendDelayIfPossible();
}
}
}
var wakeables = workInProgress.updateQueue;
if (wakeables !== null) {
// Schedule an effect to attach a retry listener to the promise.
// TODO: Move to passive phase
workInProgress.flags |= Update;
}
bubbleProperties(workInProgress);
{
if ((workInProgress.mode & ProfileMode) !== NoMode) {
if (nextDidTimeout) {
// Don't count time spent in a timed out Suspense subtree as part of the base duration.
var _primaryChildFragment2 = workInProgress.child;
if (_primaryChildFragment2 !== null) {
// $FlowFixMe Flow doesn't support type casting in combination with the -= operator
workInProgress.treeBaseDuration -=
_primaryChildFragment2.treeBaseDuration;
}
}
}
}
return null;
}
case HostPortal:
popHostContainer(workInProgress);
updateHostContainer(current, workInProgress);
if (current === null) {
preparePortalMount(workInProgress.stateNode.containerInfo);
}
bubbleProperties(workInProgress);
return null;
case ContextProvider:
// Pop provider fiber
var context = workInProgress.type._context;
popProvider(context, workInProgress);
bubbleProperties(workInProgress);
return null;
case IncompleteClassComponent: {
// Same as class component case. I put it down here so that the tags are
// sequential to ensure this switch is compiled to a jump table.
var _Component = workInProgress.type;
if (isContextProvider(_Component)) {
popContext(workInProgress);
}
bubbleProperties(workInProgress);
return null;
}
case SuspenseListComponent: {
popSuspenseContext(workInProgress);
var renderState = workInProgress.memoizedState;
if (renderState === null) {
// We're running in the default, "independent" mode.
// We don't do anything in this mode.
bubbleProperties(workInProgress);
return null;
}
var didSuspendAlready =
(workInProgress.flags & DidCapture) !== NoFlags;
var renderedTail = renderState.rendering;
if (renderedTail === null) {
// We just rendered the head.
if (!didSuspendAlready) {
// This is the first pass. We need to figure out if anything is still
// suspended in the rendered set.
// If new content unsuspended, but there's still some content that
// didn't. Then we need to do a second pass that forces everything
// to keep showing their fallbacks.
// We might be suspended if something in this render pass suspended, or
// something in the previous committed pass suspended. Otherwise,
// there's no chance so we can skip the expensive call to
// findFirstSuspended.
var cannotBeSuspended =
renderHasNotSuspendedYet() &&
(current === null || (current.flags & DidCapture) === NoFlags);
if (!cannotBeSuspended) {
var row = workInProgress.child;
while (row !== null) {
var suspended = findFirstSuspended(row);
if (suspended !== null) {
didSuspendAlready = true;
workInProgress.flags |= DidCapture;
cutOffTailIfNeeded(renderState, false); // If this is a newly suspended tree, it might not get committed as
// part of the second pass. In that case nothing will subscribe to
// its thenables. Instead, we'll transfer its thenables to the
// SuspenseList so that it can retry if they resolve.
// There might be multiple of these in the list but since we're
// going to wait for all of them anyway, it doesn't really matter
// which ones gets to ping. In theory we could get clever and keep
// track of how many dependencies remain but it gets tricky because
// in the meantime, we can add/remove/change items and dependencies.
// We might bail out of the loop before finding any but that
// doesn't matter since that means that the other boundaries that
// we did find already has their listeners attached.
var newThenables = suspended.updateQueue;
if (newThenables !== null) {
workInProgress.updateQueue = newThenables;
workInProgress.flags |= Update;
} // Rerender the whole list, but this time, we'll force fallbacks
// to stay in place.
// Reset the effect flags before doing the second pass since that's now invalid.
// Reset the child fibers to their original state.
workInProgress.subtreeFlags = NoFlags;
resetChildFibers(workInProgress, renderLanes); // Set up the Suspense Context to force suspense and immediately
// rerender the children.
pushSuspenseContext(
workInProgress,
setShallowSuspenseContext(
suspenseStackCursor.current,
ForceSuspenseFallback
)
); // Don't bubble properties in this case.
return workInProgress.child;
}
row = row.sibling;
}
}
if (renderState.tail !== null && now() > getRenderTargetTime()) {
// We have already passed our CPU deadline but we still have rows
// left in the tail. We'll just give up further attempts to render
// the main content and only render fallbacks.
workInProgress.flags |= DidCapture;
didSuspendAlready = true;
cutOffTailIfNeeded(renderState, false); // Since nothing actually suspended, there will nothing to ping this
// to get it started back up to attempt the next item. While in terms
// of priority this work has the same priority as this current render,
// it's not part of the same transition once the transition has
// committed. If it's sync, we still want to yield so that it can be
// painted. Conceptually, this is really the same as pinging.
// We can use any RetryLane even if it's the one currently rendering
// since we're leaving it behind on this node.
workInProgress.lanes = SomeRetryLane;
}
} else {
cutOffTailIfNeeded(renderState, false);
} // Next we're going to render the tail.
} else {
// Append the rendered row to the child list.
if (!didSuspendAlready) {
var _suspended = findFirstSuspended(renderedTail);
if (_suspended !== null) {
workInProgress.flags |= DidCapture;
didSuspendAlready = true; // Ensure we transfer the update queue to the parent so that it doesn't
// get lost if this row ends up dropped during a second pass.
var _newThenables = _suspended.updateQueue;
if (_newThenables !== null) {
workInProgress.updateQueue = _newThenables;
workInProgress.flags |= Update;
}
cutOffTailIfNeeded(renderState, true); // This might have been modified.
if (
renderState.tail === null &&
renderState.tailMode === "hidden" &&
!renderedTail.alternate &&
!getIsHydrating() // We don't cut it if we're hydrating.
) {
// We're done.
bubbleProperties(workInProgress);
return null;
}
} else if (
// The time it took to render last row is greater than the remaining
// time we have to render. So rendering one more row would likely
// exceed it.
now() * 2 - renderState.renderingStartTime >
getRenderTargetTime() &&
renderLanes !== OffscreenLane
) {
// We have now passed our CPU deadline and we'll just give up further
// attempts to render the main content and only render fallbacks.
// The assumption is that this is usually faster.
workInProgress.flags |= DidCapture;
didSuspendAlready = true;
cutOffTailIfNeeded(renderState, false); // Since nothing actually suspended, there will nothing to ping this
// to get it started back up to attempt the next item. While in terms
// of priority this work has the same priority as this current render,
// it's not part of the same transition once the transition has
// committed. If it's sync, we still want to yield so that it can be
// painted. Conceptually, this is really the same as pinging.
// We can use any RetryLane even if it's the one currently rendering
// since we're leaving it behind on this node.
workInProgress.lanes = SomeRetryLane;
}
}
if (renderState.isBackwards) {
// The effect list of the backwards tail will have been added
// to the end. This breaks the guarantee that life-cycles fire in
// sibling order but that isn't a strong guarantee promised by React.
// Especially since these might also just pop in during future commits.
// Append to the beginning of the list.
renderedTail.sibling = workInProgress.child;
workInProgress.child = renderedTail;
} else {
var previousSibling = renderState.last;
if (previousSibling !== null) {
previousSibling.sibling = renderedTail;
} else {
workInProgress.child = renderedTail;
}
renderState.last = renderedTail;
}
}
if (renderState.tail !== null) {
// We still have tail rows to render.
// Pop a row.
var next = renderState.tail;
renderState.rendering = next;
renderState.tail = next.sibling;
renderState.renderingStartTime = now();
next.sibling = null; // Restore the context.
// TODO: We can probably just avoid popping it instead and only
// setting it the first time we go from not suspended to suspended.
var suspenseContext = suspenseStackCursor.current;
if (didSuspendAlready) {
suspenseContext = setShallowSuspenseContext(
suspenseContext,
ForceSuspenseFallback
);
} else {
suspenseContext =
setDefaultShallowSuspenseContext(suspenseContext);
}
pushSuspenseContext(workInProgress, suspenseContext); // Do a pass over the next row.
// Don't bubble properties in this case.
return next;
}
bubbleProperties(workInProgress);
return null;
}
case ScopeComponent: {
break;
}
case OffscreenComponent:
case LegacyHiddenComponent: {
popRenderLanes(workInProgress);
var _nextState = workInProgress.memoizedState;
var nextIsHidden = _nextState !== null;
if (current !== null) {
var _prevState = current.memoizedState;
var prevIsHidden = _prevState !== null;
if (
prevIsHidden !== nextIsHidden &&
newProps.mode !== "unstable-defer-without-hiding" && // LegacyHidden doesn't do any hiding — it only pre-renders.
workInProgress.tag !== LegacyHiddenComponent
) {
workInProgress.flags |= Visibility;
}
}
if (
!nextIsHidden ||
(workInProgress.mode & ConcurrentMode) === NoMode
) {
bubbleProperties(workInProgress);
} else {
// Don't bubble properties for hidden children unless we're rendering
// at offscreen priority.
if (includesSomeLane(subtreeRenderLanes, OffscreenLane)) {
bubbleProperties(workInProgress);
{
// Check if there was an insertion or update in the hidden subtree.
// If so, we need to hide those nodes in the commit phase, so
// schedule a visibility effect.
if (
workInProgress.tag !== LegacyHiddenComponent &&
workInProgress.subtreeFlags & (Placement | Update) &&
newProps.mode !== "unstable-defer-without-hiding"
) {
workInProgress.flags |= Visibility;
}
}
}
}
return null;
}
}
主要是通过completeWork进行生成,源码里可以看到熟悉的代码,也是一个 switch 循环,通过判断tag 进入不同的组件生成逻辑,然后根据不同组件的特性,生成对应的真实的dom节点、初始化 style 以及 className 等。
commitRoot
上述的阶段主要是根据虚拟 dom 生成真实的Fiber树,但是此时 Fiber 树还存在于内存中,需要把 Fiber 树渲染到屏幕上。同时React 提供了很多副作用 hook,比如 useEffect,useLayoutEffect,useState。有些 hook 也需要在 commit 阶段进行处理。
function commitRoot(){
// ...
commitRootImpl(root, previousUpdateLanePriority);
// ...
}
function commitRootImpl(){
// ...
var shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(
root,
finishedWork
);
commitMutationEffects(root, finishedWork, lanes);
commitLayoutEffects(finishedWork, root, lanes);
}
commit 阶段主要分为三个阶段,分别是beforeMutationEffects、commitMutationEffects、commitLayoutEffects。这三个阶段主要是处理了组件中的副作用(useEffect、useLayoutEffect、useState等) ,同时进行缓存树的切换,屏幕中就渲染出了真实的 dom。具体每个阶段分别做了什么,后续章节会讲到。
总结
该阶段主要有两个小阶段,分别是:
- 根据虚拟 dom 生成 Fiber 树,同时根据Fiber 去生成对应 Fiber 对应的真实 dom 节点,处理 style 和 className 属性等。
- 通过生成的 Fiber 树,去处理组件中的副作用,然后将真实的 dom 节点渲染到页面上。
最后总结
React 的大概工作流程就是上诉这些关键节点,总结下来总共分为以下几个部分:
- 初始化相关,包括初始化 FiberRootNode 和 HostFiber 以及对事件进行处理。
- Schedule 调度阶段,进行任务的调度。
- reconcile阶段,根据虚拟 dom 生成对应的Fiber 节点,并根据不同的组件类型生成对应的真实 dom 节点。
- commit 阶段,处理副作用和将内存中构建的 Fiber 树渲染到屏幕上。
后续一些具体的流程我会分为不同的文章分别进行讲解,敬请期待~