github项目地址(配合源码阅读更加) react-research
1 jsx
jsx 语法由 babel 编译成为函数,因此在 react 中所有组件都是函数
// 编译前
function Main() {
return (
<div className="wrapper">
<h1>Hello World</h1>
<h1>ReactChild</h1>
</div>
);
}
// 编译后
function Main() {
return createElement(
"div",
{ className: "wrapper", ref: "main", key: "main" },
createElement("h1", null, "Hello World"),
createElement("h2", null, "ReactChild")
);
}
// react element数据结构
{
?typeof: Symbol(react.element),
key: "main",
props: {
children: [
{
?typeof: Symbol(react.element),
key: null,
props: { children: "Hello World" },
type: "h1",
_owner: null
},
{
?typeof: Symbol(react.element),
key: null,
props: { children: "ReactChild" },
type: "h1",
_owner: null
}
],
className: "wrapper"
},
ref: "main",
type: "div",
_owner: null
}
2 render
所有 react element 都是通过 ReactDOM.render 方法渲染成真实 DOM
//调用过程
render(element, document.querySelector("#body"));
function render(element, container, callback) {
return legacyRenderSubtreeIntoContainer(
null,
element,
container,
false,
callback
);
}
2.1 legacyRenderSubtreeIntoContainer
function legacyRenderSubtreeIntoContainer(
parentComponent, // null
children, // ReactEelement
container, // DOM
forceHydrate, // false
callback
) {
let root = container._reactRootContainer;
let fiberRoot;
if (!root) {
// 创建fiber root
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
container,
forceHydrate
);
fiberRoot = root._internalRoot;
// 非批量更新
unbatchedUpdates(() => {
updateContainer(children, fiberRoot, parentComponent, callback);
});
}
}
2.1.1 legacyCreateRootFromDOMContainer
legacyCreateRootFromDOMContainer 主要功能是返回一个 FiberRoot
hydrate 的含义是注水的意思,用途是在服务器渲染的过程中给(干扁扁的)DOM 注入一些交互事件
本文不研究服务器渲染所有 hydrate 均为 false
function legacyCreateRootFromDOMContainer(container, forceHydrate) {
const shouldHydrate = forceHydrate;
if (!shouldHydrate) {
let rootSibling;
// 移除根容器下的所有DOM
while ((rootSibling = container.lastChild)) {
container.removeChild(rootSibling);
}
}
return createLegacyRoot(
container,
shouldHydrate
? {
hydrate: true,
}
: undefined
);
}
2.1.2 createLegacyRoot
内部函数 createLegacyRoot 展开
function createLegacyRoot(container, options) {
function ReactDOMBlockingRoot(container, tag, options) {
function FiberRootNode(containerInfo, tag, hydrate) {
this.tag = tag; // 0
this.containerInfo = containerInfo; // #body
this.pendingChildren = null;
this.current = null; // FiberNode
this.pingCache = null;
this.finishedWork = null;
this.timeoutHandle = noTimeout; // -1
this.context = null;
this.pendingContext = null;
this.hydrate = hydrate; // false
this.callbackNode = null;
this.callbackId = NoLanes; // 0
this.callbackIsSync = false;
this.expiresAt = -1;
this.pendingLanes = NoLanes; //0
this.suspendedLanes = NoLanes;
this.pingedLanes = NoLanes;
this.expiredLanes = NoLanes;
this.mutableReadLanes = NoLanes;
this.finishedLanes = NoLanes;
}
function FiberNode(tag, pendingProps, key, mode) {
// Instance
this.tag = tag; // 3
this.key = key; // null
this.elementType = null;
this.type = null;
this.stateNode = null;
// Fiber
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;
this.ref = null;
this.pendingProps = pendingProps; // null
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies_new = null;
this.mode = mode; // 0
// Effects
this.effectTag = NoEffect; // 0
this.nextEffect = null;
this.firstEffect = null;
this.lastEffect = null;
this.lanes = NoLanes; // 0
this.childLanes = NoLanes;
this.alternate = null;
}
function initializeUpdateQueue(fiber) {
const queue = {
baseState: fiber.memoizedState,
firstBaseUpdate: null,
lastBaseUpdate: null,
shared: {
pending: null,
},
effects: null,
};
fiber.updateQueue = queue;
}
const hydrate = options != null && options.hydrate === true;
const hydrationCallbacks =
(options != null && options.hydrationOptions) || null;
const root = new FiberRootNode(container, tag, hydrate);
// react的各种模式
// NoMode = 0b00000;
// StrictMode = 0b00001;
// BlockingMode = 0b00010;
// ConcurrentMode = 0b00100;
// ProfileMode = 0b01000;
let mode;
// tag = LegacyRoot = 0 = NoMode
if (tag === ConcurrentRoot) {
// ConcurrentMode并行模式是最新的模式
// 关于并行模式: https://reactjs.org/docs/concurrent-mode-intro.html
// 如何开启并行模式: https://reactjs.org/docs/concurrent-mode-reference.html#createroot
mode = ConcurrentMode | BlockingMode | StrictMode;
} else if (tag === BlockingRoot) {
mode = BlockingMode | StrictMode;
} else {
mode = NoMode;
}
// HostRoot = 3
const uninitializedFiber = new FiberNode(HostRoot, null, null, mode);
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;
initializeUpdateQueue(uninitializedFiber);
// 随机数转为36位生成随机字符串
const randomKey = Math.random().toString(36).slice(2);
container["__reactContainer$" + randomKey] = root.current;
this._internalRoot = root;
}
// LegacyRoot = 0
return new ReactDOMBlockingRoot(container, LegacyRoot, options);
}
小结(Fiber 数据结构):
尽早了解每个属性的作用,对后面帮助很大
2.2 unbatchedUpdates
修改执行上下文变量executionContext 进入非批量更新模式
执行完后恢复成上一个模式
// BatchedContext = 0b000001
// LegacyUnbatchedContext = 0b001000
// executionContext = 0b001000 = 8
function unbatchedUpdates(fn, a) {
const preExecutionContext = executionContext;
// 且操作符和取反操作符代表除去BatchedContext这个类型
executionContext &= ~BatchedContext; // 0b000000 & 0b111110
// 或操作符代表包含LegacyUnbatchedContext这个类型
executionContext |= LegacyUnbatchedContext; // 0b000000 | 0b001000
try {
return fn(a);
} finally {
executionContext = preExecutionContext;
if (executionContext === NoContext) {
// 首次执行不会进入flushSyncCallbackQueue先不展开
// 刷新次批调度任务中的立即毁掉函数
flushSyncCallbackQueue();
}
}
}
2.3 updateContainer
function updateContainer(element, container, parentComponent, callback) {
const current = container.current;
const eventTime = requestEventTime();
const suspenseConfig = null;
const lane = requestUpdateLane(current, suspenseConfig); // SyncLane 1
// 获取当前节点和子节点的上下文
const context = getContextForSubtree(parentComponent); // {}
if (container.context === null) {
container.context = context; // fiberRoot.context = {}
} else {
container.pendingContext = context;
}
// 创建一个更新对线
const update = createUpdate(eventTime, lane, suspenseConfig);
update.payload = { element };
callback = callback === undefined ? null : callback;
if (callback !== null) {
update.callback = callback;
}
enqueueUpdate(container.current, update);
scheduleUpdateOnFiber(current, lane);
return lane;
}
2.3.1 requestEventTime
获取程序运行到目前为止的时间,用于进行优先级排序
let currentEventTime = -1;
let now;
const initialTime = Date.now();
if (typeof performance === "object" && typeof performance.now === "function") {
now = () => performance.now();
} else {
now = () => Date.now() - initialTime;
}
// executionContext = 0b001000
// RenderContext = 0b010000;
// CommitContext = 0b100000;
function requestEventTime() {
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
// 我们在React内部,因此可以读取实际时间
return now();
}
// 我们不在React内部,因此我们可能处于浏览器事件的中间。
if (currentEventTime !== -1) {
// 对所有更新使用相同的开始时间,直到我们再次进入React
return currentEventTime;
}
// 这是自React产生以来的第一次更新。计算新的开始时间。
currentEventTime = now();
return currentEventTime;
}
2.3.2 requestUpdateLane
function requestUpdateLane(fiber, suspenseConfig) {
const mode = fiber.mode;
if ((mode & BlockingMode) === NoMode) {
// 旧模式中lane只为SyncLane = 1
return SyncLane;
} else if ((mode & ConcurrentMode) === NoMode) {
return SyncBatchedLane;
} else if (
!deferRenderPhaseUpdateToNextBatch &&
(executionContext & RenderContext) !== NoContext &&
workInProgressRootRenderLanes !== NoLane
) {
return pickArbitraryLane(workInProgressRootRenderLanes);
}
if (currentEventWipLanes === NoLanes) {
currentEventWipLanes = workInProgressRootIncludedLanes;
}
let lane;
if (suspenseConfig !== null) {
} else {
const schedulerPriority = getCurrentPriorityLevel();
if (
(executionContext & DiscreteEventContext) !== NoContext &&
schedulerPriority === UserBlockingPriority
) {
lane = findUpdateLane(InputDiscreteLanePriority, currentEventWipLanes);
} else {
const lanePriority = schedulerPriorityToLanePriority(schedulerPriority);
lane = findUpdateLane(lanePriority, currentEventWipLanes);
}
}
return lane;
}
2.3.3 getContextForSubtree
const emptyContextObject = {};
function getContextForSubtree(parentComponent) {
if (!parentComponent) {
return emptyContextObject;
}
const fiber = parentComponent._reactInternals;
const disableLegacyContext = false;
function isContextProvider(type) {
if (disableLegacyContext) {
return false;
} else {
const childContextTypes = type.childContextTypes;
return childContextTypes !== null && childContextTypes !== undefined;
}
}
function findCurrentUnmaskedContext(fiber) {
if (disableLegacyContext) {
return emptyContextObject;
} else {
let node = fiber;
do {
switch (node.tag) {
case HostRoot:
return node.stateNode.context;
case ClassComponent: {
const Component = node.type;
if (isContextProvider(Component)) {
return node.stateNode.__reactInternalMemoizedMergedChildContext;
}
break;
}
}
node = node.return;
} while (node !== null);
}
}
const parentContext = findCurrentUnmaskedContext(fiber);
function processChildContext(fiber, type, parentContext) {
if (disableLegacyContext) {
return parentContext;
} else {
const instance = fiber.stateNode;
const childContextTypes = type.childContextTypes;
if (typeof instance.getChildContext !== "function") {
return parentContext;
}
const childContext = instance.getChildContext();
return { ...parentContext, ...childContext };
}
}
const isLegacyContextProvider = isContextProvider;
if (fiber.tag === ClassComponent) {
const Component = fiber.type;
if (isLegacyContextProvider(Component)) {
return processChildContext(fiber, Component, parentContext);
}
}
return parentContext;
}
2.3.4 createUpdate
创建一个更新,会把 children 以 element 属性置入 payload 中
后面调节时会根据这个 update 对象去展开子结点
function createUpdate(eventTime, lane, suspenseConfig) {
const update = {
eventTime, // 20.0000
lane, // 1
suspenseConfig, // null
tag: UpdateState, // 0
payload: null,
callback: null,
next: null,
};
return update;
}
2.3.5 enqueueUpdate
function enqueueUpdate(fiber, update) {
const updateQueue = fiber.updateQueue;
if (updateQueue === null) {
// 只会发生在fiber节点注销的时候
return;
}
const sharedQueue = updateQueue.shared;
const pending = sharedQueue.pending;
if (pending === null) {
// 这是第一次更新,创建一个循环单链表
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
sharedQueue.pending = update;
}
2.3.6 scheduleUpdateOnFiber
const RootIncomplete = 0;
const RootSuspendedWithDelay = 4;
let workInProgressRootExitStatus = RootIncomplete;
function scheduleUpdateOnFiber(fiber, lane) {
function mergeLanes(a, b) {
return a | b;
}
function markRootUpdated(root, updateLane) {
// root.pendingLanes = NoLanes = 0
// updateLane = 1
root.pendingLanes |= updateLane; // 1
// 现在,我们使用与旧的ExpirationTimes模型相同的启发式方法:
// 重试一些lane在相同或者更低的优先级,但不尝试更新那些更高优先级的
// 但不包括更低优先级的更新。这很好
// 在考虑不同优先级的更新时,但不是足以在相同优先级内进行更新
// 我们希望并行的处理这些更新
// 非suspend具有相同或更低优先级的任何更新。
const higherPriorityLanes = updateLane - 1; // 将 0b1000 变成 0b0111
root.suspendedLanes &= higherPriorityLanes; // 0
root.pingedLanes &= higherPriorityLanes; // 0
}
function markUpdateLaneFromFiberToRoot(fiber, lane) {
// 更新源fiber的lanes
fiber.lanes = mergeLanes(fiber.lanes, lane); // 1
let alternate = fiber.alternate; // null
if (alternate !== null) {
// false
alternate.lanes = mergeLanes(alternate.lanes, lane);
}
let node = fiber.return; // null
let root = null;
if (node === null && fiber.tag === HostRoot) {
// true
root = fiber.stateNode; // RootFiber
} else {
while (node !== null) {
alternate = node.alternate;
node.childLanes = mergeLanes(node.childLanes, lane);
if (alternate !== null) {
alternate.childLanes = mergeLanes(alternate.childLanes, lane);
}
if (node.return === null && node.tag === HostRoot) {
root = node.stateNode;
break;
}
node = node.return;
}
}
if (root !== null) {
// true
// 标记root有待处理的更新。
markRootUpdated(root, lane);
if (workInProgressRoot === root) {
// false 不进入
if (
deferRenderPhaseUpdateToNextBatch ||
(executionContext & RenderContext) === NoContext
) {
workInProgressRootUpdatedLanes = mergeLanes(
workInProgressRootUpdatedLanes,
lane
);
}
if (workInProgressRootExitStatus === RootSuspendedWithDelay) {
markRootSuspended(root, workInProgressRootRenderLanes);
}
}
}
return root;
}
const root = markUpdateLaneFromFiberToRoot(fiber, lane);
// 获取当前优先级
const priorityLevel = getCurrentPriorityLevel();
if (lane === SyncLane) {
if (
// 检查我们是否在非批量更新模式
(executionContext & LegacyUnbatchedContext) !== NoContext &&
// 检查我们是否没有正在进行渲染
(executionContext & (RenderContext | CommitContext)) === NoContext
) {
// 在根上注册待处理的交互,以避免丢失跟踪的交互数据。
// 未执行不展开
schedulePendingInteractions(root, lane);
// 这是一个遗留的方法,用来初始化安装ReactDOM.render-ed
// batchedUpdates 内部的root应该是同步的,但是布局更新应该延迟直到batch结束
performSyncWorkOnRoot(root);
} else {
// 初次渲染不进入
ensureRootIsScheduled(root);
schedulePendingInteractions(root, lane);
if (executionContext === NoContext) {
// 马上清空同步任务,除非我们已经进入batch。
// 故意将其放在scheduleUpdateOnFiber而不是scheduleCallbackForFiber中,以保留安排回调的功能无需立即清空。
// 我们仅针对用户发起的操作更新,以保留旧版模式的历史行为。
flushSyncCallbackQueue();
}
}
} else {
// ...其他模式
}
}
小结:
3 performSyncWorkOnRoot
真正的渲染入口
function performSyncWorkOnRoot(root) {
// 初次渲染不进入
flushPassiveEffects();
let lanes;
let exitStatus;
if (
root === workInProgressRoot &&
includesSomeLane(root.expiredLanes, workInProgressRootRenderLanes)
) {
// 这是一颗不完整的树,并且最少其中一条lane已经过期完成渲染
// 在渲染其余过期工作之前,请完成渲染。
lanes = workInProgressRootRenderLanes;
exitStatus = renderRootSync(root, lanes);
if (
includesSomeLane(
workInProgressRootIncludedLanes,
workInProgressRootUpdatedLanes
)
) {
// 渲染包括在渲染阶段更新的lanes。
// 例如,当取消隐藏隐藏的树时,我们包括所有lanes
// 隐藏树时先前已跳过的内容。该lanes是我们开始渲染时使用的lanes的超集。
// 请注意,这仅在部分树被渲染时发生
// 同时。如果整个树是同步呈现的,则没有交错事件。
lanes = getNextLanes(root, lanes);
exitStatus = renderRootSync(root, lanes);
}
} else {
// 首次渲染进入这里
lanes = getNextLanes(root, NoLanes); // 1
exitStatus = renderRootSync(root, lanes);
}
if (root.tag !== LegacyRoot && exitStatus === RootErrored) {
// If something threw an error, try rendering one more time. We'll render
// synchronously to block concurrent data mutations, and we'll includes
// all pending updates are included. If it still fails after the second
// attempt, we'll give up and commit the resulting tree.
lanes = getLanesToRetrySynchronouslyOnError(root);
if (lanes !== NoLanes) {
exitStatus = renderRootSync(root, lanes);
}
}
if (exitStatus === RootFatalErrored) {
const fatalError = workInProgressRootFatalError;
prepareFreshStack(root, NoLanes);
markRootSuspended(root, lanes);
ensureRootIsScheduled(root);
throw fatalError;
}
// We now have a consistent tree. Because this is a sync render, we
// will commit it even if something suspended.
const finishedWork = root.current.alternate;
root.finishedWork = finishedWork;
root.finishedLanes = lanes;
commitRoot(root);
// Before exiting, make sure there's a callback scheduled for the next
// pending level.
ensureRootIsScheduled(root);
return null;
}
3.1 getNextLanes
function getNextLanes(root, wipLanes) {
const pendingLanes = root.pendingLanes; // 1
if (pendingLanes === NoLanes) {
// false
return_highestLanePriority = NoLanePriority;
return NoLanes;
}
let nextLanes = NoLanes; // 0
let nextLanePriority = NoLanePriority; // 0
let equalOrHigherPriorityLanes = NoLanes; // 0
const expiredLanes = root.expiredLanes; // 0
const suspendedLanes = root.suspendedLanes; // 0
const pingedLanes = root.pingedLanes; // 0
// 检查任务是否过期了
if (expiredLanes !== NoLanes) {
// false
nextLanes = expiredLanes;
nextLanePriority = return_highestLanePriority = SyncLanePriority;
equalOrHigherPriorityLanes = (getLowestPriorityLane(nextLanes) << 1) - 1;
} else {
// 在完成所有非空闲工作之前,请勿进行任何闲置工作,即使工作是suspended。
// const NonIdleLanes = 0b0000111111111111111111111111111;
const nonIdlePendingLanes = pendingLanes & NonIdleLanes; // 1
if (nonIdlePendingLanes !== NoLanes) {
// true
const nonIdleUnblockedLanes = nonIdlePendingLanes & ~suspendedLanes; // 1
if (nonIdleUnblockedLanes !== NoLanes) {
// true
nextLanes = getHighestPriorityLanes(nonIdleUnblockedLanes); // 1
nextLanePriority = return_highestLanePriority; // SyncLanePriority 16
equalOrHigherPriorityLanes = (1 << return_updateRangeEnd) - 1; // 1
} else {
const nonIdlePingedLanes = nonIdlePendingLanes & pingedLanes;
if (nonIdlePingedLanes !== NoLanes) {
nextLanes = getHighestPriorityLanes(nonIdlePingedLanes);
nextLanePriority = return_highestLanePriority;
equalOrHigherPriorityLanes = (1 << return_updateRangeEnd) - 1;
}
}
} else {
// 只剩下空闲的工作
const unblockedLanes = pendingLanes & ~suspendedLanes;
if (unblockedLanes !== NoLanes) {
nextLanes = getHighestPriorityLanes(unblockedLanes);
nextLanePriority = return_highestLanePriority;
equalOrHigherPriorityLanes = (1 << return_updateRangeEnd) - 1;
} else {
if (pingedLanes !== NoLanes) {
nextLanes = getHighestPriorityLanes(pingedLanes);
nextLanePriority = return_highestLanePriority;
equalOrHigherPriorityLanes = (1 << return_updateRangeEnd) - 1;
}
}
}
}
if (nextLanes === NoLanes) {
// 只有我们是suspended才能到达这里
return NoLanes;
}
// 如果有更高优先级的lanes,我们将包含他们即便他们是suspended
nextLanes = pendingLanes & equalOrHigherPriorityLanes; // 1
// 如果我们已经在渲染中间,切换lanes将会中断他并且我们会失去我们的进度
// 我们只能在新的更高优先级的lanes中做这些
if (
wipLanes !== NoLanes &&
wipLanes !== nextLanes &&
// 如果我们已经suspended了一段时间,那么中断就可以了。别等到根完成为止。
(wipLanes & suspendedLanes) === NoLanes
) {
getHighestPriorityLanes(wipLanes);
const wipLanePriority = return_highestLanePriority;
if (nextLanePriority <= wipLanePriority) {
return wipLanes;
} else {
return_highestLanePriority = nextLanePriority;
}
}
return nextLanes; // 1
}
3.1.1 getHighestPriorityLanes
const DefaultLanePriority = 9;
const SyncLanePriority = 16;
const SyncUpdateRangeEnd = 1;
let return_highestLanePriority = DefaultLanePriority;
let return_updateRangeEnd = -1;
function getHighestPriorityLanes(lanes) {
if ((SyncLane & lanes) !== NoLanes) {
return_highestLanePriority = SyncLanePriority;
return_updateRangeEnd = SyncUpdateRangeEnd;
return SyncLane; // 1
}
// ...
}
3.2 renderRootSync
const ReactCurrentDispatcher = {
current: null,
};
// react hooks相关
const ContextOnlyDispatcher = {
readContext,
useCallback: throwInvalidHookError,
useContext: throwInvalidHookError,
useEffect: throwInvalidHookError,
useImperativeHandle: throwInvalidHookError,
useLayoutEffect: throwInvalidHookError,
useMemo: throwInvalidHookError,
useReducer: throwInvalidHookError,
useRef: throwInvalidHookError,
useState: throwInvalidHookError,
useDebugValue: throwInvalidHookError,
useResponder: throwInvalidHookError,
useDeferredValue: throwInvalidHookError,
useTransition: throwInvalidHookError,
useMutableSource: throwInvalidHookError,
useOpaqueIdentifier: throwInvalidHookError,
};
function renderRootSync(root, lanes) {
function pushDispatcher(root) {
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
if (prevDispatcher === null) {
return ContextOnlyDispatcher;
} else {
return prevDispatcher;
}
}
const prevExecutionContext = executionContext;
// 切换到渲染执行上下文
executionContext |= RenderContext;
// hooks相关
const prevDispatcher = pushDispatcher(root);
// 如果root或者lanes改变,丢弃现有的栈
// 而且准备一个新的,否则我们会继续离开我们所在的地方
if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
// 准备一个当前fiber节点的克隆 放在全局变量workInProgress中
prepareFreshStack(root, lanes);
// 暂时不了解
startWorkOnPendingInteractions(root, lanes);
}
// 暂时不了解
const prevInteractions = pushInteractions(root);
do {
try {
// 循环处理workInProgress
workLoopSync();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
resetContextDependencies();
if (enableSchedulerTracing) {
popInteractions(((prevInteractions: any): Set<Interaction>));
}
executionContext = prevExecutionContext;
popDispatcher(prevDispatcher);
if (workInProgress !== null) {
// This is a sync render, so we should have finished the whole tree.
invariant(
false,
"Cannot commit an incomplete root. This error is likely caused by a " +
"bug in React. Please file an issue."
);
}
// Set this to null to indicate there's no in-progress render.
workInProgressRoot = null;
workInProgressRootRenderLanes = NoLanes;
return workInProgressRootExitStatus;
}
3.2.1 prepareFreshStack
创建 workInProgress,workInProgress 很重要相当于 virtual tree
后面要主键构建整棵树,用于渲染
// 全局变量
let workInProgress = null;
function prepareFreshStack(root, lanes) {
function createFiber(tag, pendingProps, key, mode) {
return new FiberNode(tag, pendingProps, key, mode);
}
function createWorkInProgress(current, pendingProps) {
let workInProgress = current.alternate;
if (workInProgress === null) {
// 我们使用双重缓冲池技术
// 因为我们知道我们最多需要一颗数的两个版本
// 我们将"其他"未使用的资源合并这样我们就可以自由的重用其他节点
// 这个懒创建是为了避免分配额外的一些永不更新的对象
// 如果需要,这也允许我们回收额外的内存
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;
// 必须要因为Blocks按照类型存储数据
workInProgress.type = current.type;
// 我们已经有了一个备份
// 重置effectTag
workInProgress.effectTag = NoEffect;
// effect链表不再生效
workInProgress.nextEffect = null;
workInProgress.firstEffect = null;
workInProgress.lastEffect = null;
if (enableProfilerTimer) {
workInProgress.actualDuration = 0;
workInProgress.actualStartTime = -1;
}
}
workInProgress.childLanes = current.childLanes;
workInProgress.lanes = current.lanes;
workInProgress.child = current.child;
workInProgress.memoizedProps = current.memoizedProps;
workInProgress.memoizedState = current.memoizedState;
workInProgress.updateQueue = current.updateQueue;
// 克隆依赖对象,在渲染阶段将其突变,这不能与现在的fiber共享
const currentDependencies = current.dependencies_new;
workInProgress.dependencies_new =
currentDependencies === null
? null
: {
lanes: currentDependencies.lanes,
firstContext: currentDependencies.firstContext,
responders: currentDependencies.responders,
};
// 这些将会在父节点的和解阶段被覆盖
workInProgress.sibling = current.sibling;
workInProgress.index = current.index;
workInProgress.ref = current.ref;
if (enableProfilerTimer) {
workInProgress.selfBaseDuration = current.selfBaseDuration;
workInProgress.treeBaseDuration = current.treeBaseDuration;
}
return workInProgress;
}
root.finishedWork = null;
root.finishedLanes = NoLanes;
const timeoutHandle = root.timeoutHandle;
if (timeoutHandle !== noTimeout) {
// The root previous suspended and scheduled a timeout to commit a fallback
// state. Now that we have additional work, cancel the timeout.
root.timeoutHandle = noTimeout;
// $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check above
cancelTimeout(timeoutHandle);
}
if (workInProgress !== null) {
let interruptedWork = workInProgress.return;
while (interruptedWork !== null) {
unwindInterruptedWork(interruptedWork);
interruptedWork = interruptedWork.return;
}
}
workInProgressRoot = root;
workInProgress = createWorkInProgress(root.current, null);
workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
workInProgressRootExitStatus = RootIncomplete;
workInProgressRootFatalError = null;
workInProgressRootLatestProcessedEventTime = -1;
workInProgressRootLatestSuspenseTimeout = -1;
workInProgressRootCanSuspendUsingConfig = null;
workInProgressRootSkippedLanes = NoLanes;
workInProgressRootUpdatedLanes = NoLanes;
workInProgressRootPingedLanes = NoLanes;
if (enableSchedulerTracing) {
spawnedWorkDuringRender = null;
}
}
3.2.2 workLoopSync
performUnitOfWork 方法循环调用 beginWork 找到处理对应组件的 hander 方法
采用深度优先(先序优先)的方式遍历创建子节点,遇到同级节点下有多个子节点时,会为每
个节点创建一个 sibling 属性指向下一个同级节点
当遍历到某个分支的最深节点(没有子节点)时调用 completeUnitOfWork 方法
completeUnitOfWork 方法判断如果有 sibling(下一个同级节点)则返回给 performUnitOfWork
没有 sibling 则寻找 return(父节点),如果父节点有 sibling 继续返回给 performUnitOfWork
从而实现深度优先遍历去创建整个 Fiber tree
function workLoopSync() {
function performUnitOfWork(unitOfWork) {
// 创建一个备份的fiber
// 最理想的情况是不依靠current fiber, 创建一个workInProgress
// 创建一个workInProgress的alternate属性指向current fiber
const current = unitOfWork.alternate;
let next;
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
startProfilerTimer(unitOfWork);
next = beginWork(current, unitOfWork, subtreeRenderLanes);
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
} else {
// true
// workInProgress.alternate, workInProgress, SyncLane
next = beginWork(current, unitOfWork, subtreeRenderLanes);
}
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// 如果没有分配新的工作,完成当前工作
completeUnitOfWork(unitOfWork);
} else {
// workInProgress设置为一下个工作
workInProgress = next;
}
ReactCurrentOwner.current = null;
}
function beginWork(current, workInProgress, renderLanes) {
const updateLanes = workInProgress.lanes;
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
// 如果renderLanes不包含当前workInProgress的lanes进入这里
if (!includesSomeLane(renderLanes, updateLanes)) {
// ...
} else {
// 一个更新被安排到这个fiber上面,但是他没有新的props也没有
// legacy context.设置didReceiveUpdate为false,如果一个更新队列
// 或context消费者产生的值发生了变化,将会把设置didReceiveUpdate为
// true.不然的话改组件假定children没有改变并退出
didReceiveUpdate = false;
}
} else {
didReceiveUpdate = false;
}
// 在进入 begin 阶段之前,清除等待更新优先级。
workInProgress.lanes = NoLanes;
// 根据各种类型的tag去处理
switch (workInProgress.tag) {
// ...
case IndeterminateComponent: {
return mountIndeterminateComponent(
current,
workInProgress,
workInProgress.type,
renderLanes
);
}
case ClassComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const 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(current, workInProgress, renderLanes);
case HostText:
return updateHostText(current, workInProgress);
// ...
}
}
// 已经超时了,所以执行工作,不需要检查我们是否需要产量。
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
3.2.2.1 processUpdateQueue
let workInProgressRootSkippedLanes = NoLanes; // 0
function processUpdateQueue(workInProgress, props, instance, renderLanes) {
// 是否子集 NoLane为0是任何数字的子集
function isSubsetOfLanes(set, subset) {
return (set & subset) === subset;
}
// 从update中获取state
function getStateFromUpdate(
workInProgress,
queue,
update,
prevState,
nextProps,
instance
) {
switch (update.tag) {
// 重置状态
case ReplaceState: {
const payload = update.payload;
if (typeof payload === "function") {
const nextState = payload.call(instance, prevState, nextProps);
return nextState;
}
return payload;
}
case CaptureUpdate: {
workInProgress.effectTag =
(workInProgress.effectTag & ~ShouldCapture) | DidCapture;
}
// 更新状态 首次进入这里
case UpdateState: {
const payload = update.payload;
let partialState;
// 如果update是一个方法 例如this.setState((state, props) => (newState)) 调用方法返回新的state
if (typeof payload === "function") {
partialState = payload.call(instance, prevState, nextProps);
} else {
// 部分状态对象
partialState = payload;
}
if (partialState === null || partialState === undefined) {
// 如果得到的状态为空 视为无操作 返回上一个state
return prevState;
}
// 合并上一个状态和新得到的部分状态 浅层合并
return Object.assign({}, prevState, partialState);
}
// 强制更新
case ForceUpdate: {
hasForceUpdate = true;
return prevState;
}
}
return prevState;
}
// 全局变量workInProgressRootSkippedLanes合并传入的lane
function markSkippedUpdateLanes(lane) {
workInProgressRootSkippedLanes = mergeLanes(
lane,
workInProgressRootSkippedLanes
);
}
// 这在ClassComponent或HostRoot上总是非空。
const queue = workInProgress.updateQueue;
hasForceUpdate = false;
let firstBaseUpdate = queue.firstBaseUpdate;
let lastBaseUpdate = queue.lastBaseUpdate;
// 检查是否有等待更新。如果有,请将其转移到基础队列中。
let pendingQueue = queue.shared.pending;
if (pendingQueue !== null) {
queue.shared.pending = null;
// 挂起的队列是循环的。断开第一个和最后一个之间的指针,使其非循环。
const lastPendingUpdate = pendingQueue;
const firstPendingUpdate = lastPendingUpdate.next;
lastPendingUpdate.next = null;
// 将待定更新添加到基本队列中
if (lastBaseUpdate === null) {
firstBaseUpdate = firstPendingUpdate;
} else {
lastBaseUpdate.next = firstPendingUpdate;
}
lastBaseUpdate = lastPendingUpdate;
// 如果当前有一个队列,而且它与基础队列不同,那么我们也需要将更新的内容转移到这个队列中。
// 因为基本队列是一个没有周期的单链路链表,所以我们可以附加到两个链表中,并利用结构共享的优势。
const current = workInProgress.alternate;
if (current !== null) {
// 这在ClassComponent或HostRoot上总是非空。
const currentQueue = current.updateQueue;
const currentLastBaseUpdate = currentQueue.lastBaseUpdate;
if (currentLastBaseUpdate !== lastBaseUpdate) {
if (currentLastBaseUpdate === null) {
currentQueue.firstBaseUpdate = firstPendingUpdate;
} else {
currentLastBaseUpdate.next = firstPendingUpdate;
}
currentQueue.lastBaseUpdate = lastPendingUpdate;
}
}
}
// 这些值可能会随着我们处理队列而改变。
if (firstBaseUpdate !== null) {
let newState = queue.baseState;
let newLanes = NoLanes;
let newBaseState = null;
let newFirstBaseUpdate = null;
let newLastBaseUpdate = null;
let update = firstBaseUpdate;
do {
const updateLane = update.lane;
const updateEventTime = update.eventTime;
if (!isSubsetOfLanes(renderLanes, updateLane)) {
// 优先权不足。跳过此更新。如果这是第一个
// 跳过更新,以前的更新/状态是新的基础更新/状态。
const clone = {
eventTime: updateEventTime,
lane: updateLane,
suspenseConfig: update.suspenseConfig,
tag: update.tag,
payload: update.payload,
callback: update.callback,
next: null,
};
if (newLastBaseUpdate === null) {
newFirstBaseUpdate = newLastBaseUpdate = clone;
newBaseState = newState;
} else {
newLastBaseUpdate = newLastBaseUpdate.next = clone;
}
// 更新队列中的剩余优先级。
newLanes = mergeLanes(newLanes, updateLane);
} else {
// 此更新确实具有足够的优先级。
if (newLastBaseUpdate !== null) {
const clone = {
eventTime: updateEventTime,
// 此更新将要提交,因此我们永远都不想取消提交
// 它。使用NoLane是可行的,因为0是所有位掩码的子集,因此
// 这将永远不会被上面的检查跳过。
lane: NoLane,
suspenseConfig: update.suspenseConfig,
tag: update.tag,
payload: update.payload,
callback: update.callback,
next: null,
};
newLastBaseUpdate = newLastBaseUpdate.next = clone;
}
// 将此更新的事件时间标记为与此渲染过程相关。
// TODO:理想情况下,应使用此更新的真实事件时间,而不是
// 它的优先级,这是一个衍生且不可逆的值。
// 待办事项:如果已提交但当前不执行此更新,则应跳过此更新
// 我们无法检测到已提交和已暂停之间的差异
// 在这里更新。
// 处理此更新。
newState = getStateFromUpdate(
workInProgress,
queue,
update,
newState,
props,
instance
);
const callback = update.callback;
if (callback !== null) {
workInProgress.effectTag |= Callback;
const effects = queue.effects;
if (effects === null) {
queue.effects = [update];
} else {
effects.push(update);
}
}
}
update = update.next;
if (update === null) {
pendingQueue = queue.shared.pending;
if (pendingQueue === null) {
// 退出while循环
break;
} else {
// 暂时不清楚什么时候进入这里
// 一个从reducer内部安排了一次更新
// 新增新的待处理的更新链表末尾并继续处理。
const lastPendingUpdate = pendingQueue;
// 故意不健全。待更新形成了一个循环链表,但我们
// 在转移到基本队列时,解开它们。
const firstPendingUpdate = lastPendingUpdate.next;
lastPendingUpdate.next = null;
update = firstPendingUpdate;
queue.lastBaseUpdate = lastPendingUpdate;
queue.shared.pending = null;
}
}
} while (true);
if (newLastBaseUpdate === null) {
// true
newBaseState = newState;
}
queue.baseState = newBaseState;
queue.firstBaseUpdate = newFirstBaseUpdate; // null
queue.lastBaseUpdate = newLastBaseUpdate; // null
markSkippedUpdateLanes(newLanes); // workInProgressRootSkippedLanes = 0
workInProgress.lanes = newLanes; // 0
workInProgress.memoizedState = newState;
}
}
3.2.2.1 updateHostRoot
function updateHostRoot(current, workInProgress, renderLanes) {
function cloneUpdateQueue(current, workInProgress) {
// 从当前节点克隆update quene,除非它已经是克隆的了
const queue = workInProgress.updateQueue;
const currentQueue = current.updateQueue;
// 浅拷贝
if (queue === currentQueue) {
const clone = {
baseState: currentQueue.baseState,
firstBaseUpdate: currentQueue.firstBaseUpdate,
lastBaseUpdate: currentQueue.lastBaseUpdate,
shared: currentQueue.shared,
effects: currentQueue.effects,
};
workInProgress.updateQueue = clone;
}
}
// context相关
pushHostRootContext(workInProgress);
const updateQueue = workInProgress.updateQueue;
const nextProps = workInProgress.pendingProps;
const prevState = workInProgress.memoizedState;
const prevChildren = prevState !== null ? prevState.element : null;
// 克隆一份updateQueue
cloneUpdateQueue(current, workInProgress);
processUpdateQueue(workInProgress, nextProps, null, renderLanes);
const nextState = workInProgress.memoizedState;
const nextChildren = nextState.element;
if (nextChildren === prevChildren) {
// false
resetHydrationState();
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
const root = workInProgress.stateNode;
if (root.hydrate && enterHydrationState(workInProgress)) {
// 无需了解
} else {
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
}
return workInProgress.child;
}
3.2.2.3 updateClassComponent
处理 ClassComponent 节点,包含实例化和调用组件的生命周期(注意在此处的生命周期父组件的生命周期方法会比子组件的生命周期方法先调用)
const emptyRefsObject = new Component().refs;
function updateClassComponent(
current,
workInProgress,
Component,
nextProps,
renderLanes
) {
function isLegacyContextProvider(type) {
if (disableLegacyContext) {
// false
return false;
} else {
// true
const childContextTypes = type.childContextTypes; // undefined
return childContextTypes !== null && childContextTypes !== undefined;
}
}
function prepareToReadContext(workInProgress, renderLanes) {
currentlyRenderingFiber = workInProgress;
lastContextDependency = null;
lastContextWithAllBitsObserved = null;
const dependencies = workInProgress.dependencies_new; // null
if (dependencies !== null) {
// false
const firstContext = dependencies.firstContext;
if (firstContext !== null) {
if (includesSomeLane(dependencies.lanes, renderLanes)) {
//上下文列表有一个待处理的更新。标记该fiber已完成工作。
markWorkInProgressReceivedUpdate();
}
// 重置work-in-progress列表
dependencies.firstContext = null;
}
}
}
function getUnmaskedContext(
workInProgress,
Component,
didPushOwnContextIfProvider
) {
if (disableLegacyContext) {
return emptyContextObject;
} else {
if (didPushOwnContextIfProvider && isLegacyContextProvider(Component)) {
// If the fiber is a context provider itself, when we read its context
// we may have already pushed its own child context on the stack. A context
// provider should not "see" its own child context. Therefore we read the
// previous (parent) context instead for a context provider.
return previousContext;
}
return contextStackCursor.current;
}
}
function setInstance(key, value) {
key._reactInternals = value;
}
function adoptClassInstance(workInProgress, instance) {
instance.updater = classComponentUpdater;
workInProgress.stateNode = instance;
// 实例需要访问fiber以便安排更新
setInstance(instance, workInProgress);
if (__DEV__) {
instance._reactInternalInstance = fakeInternalInstance;
}
}
function constructClassInstance(workInProgress, ctor, props) {
let isLegacyContextConsumer = false;
let unmaskedContext = emptyContextObject;
let context = emptyContextObject;
const contextType = ctor.contextType; // undefined
if (typeof contextType === "object" && contextType !== null) {
context = readContext(contextType);
} else if (!disableLegacyContext) {
// true
// 不去了解
unmaskedContext = getUnmaskedContext(workInProgress, ctor, true);
const contextTypes = ctor.contextTypes;
isLegacyContextConsumer =
contextTypes !== null && contextTypes !== undefined; // false
// 老的context consumer
context = isLegacyContextConsumer
? getMaskedContext(workInProgress, unmaskedContext)
: emptyContextObject;
}
// 实例化组件
const instance = new ctor(props, context);
// 获取实例化组件的初始state
const state = (workInProgress.memoizedState =
instance.state !== null && instance.state !== undefined
? instance.state
: null); // { text: "Hello World" }
// 将实例关联fiber
adoptClassInstance(workInProgress, instance);
// 不去了解
if (isLegacyContextConsumer) {
cacheContext(workInProgress, unmaskedContext, context);
}
return instance;
}
function mountClassInstance(workInProgress, ctor, newProps, renderLanes) {
function applyDerivedStateFromProps(
workInProgress,
ctor,
getDerivedStateFromProps,
nextProps
) {
const prevState = workInProgress.memoizedState;
const partialState = getDerivedStateFromProps(nextProps, prevState);
// 合并之前的state和通过getDerivedStateFromProps方法获取的state
const memoizedState =
partialState === null || partialState === undefined
? prevState
: Object.assign({}, prevState, partialState);
workInProgress.memoizedState = memoizedState;
// 更新队列为空后,将派生状态保持在基本状态上
if (workInProgress.lanes === NoLanes) {
// 队列对于类始终为非null
const updateQueue = workInProgress.updateQueue;
updateQueue.baseState = memoizedState;
}
}
const instance = workInProgress.stateNode;
instance.props = newProps; // {}
instance.state = workInProgress.memoizedState; // { text: "Hello World" }
instance.refs = emptyRefsObject; // {}
// 初始化更新队列
initializeUpdateQueue(workInProgress);
const contextType = ctor.contextType;
if (typeof contextType === "object" && contextType !== null) {
instance.context = readContext(contextType);
} else if (disableLegacyContext) {
instance.context = emptyContextObject; // {}
} else {
const unmaskedContext = getUnmaskedContext(workInProgress, ctor, true);
instance.context = getMaskedContext(workInProgress, unmaskedContext);
}
processUpdateQueue(workInProgress, newProps, instance, renderLanes);
instance.state = workInProgress.memoizedState;
// 装载生命周期一(getDerivedStateFromProps): 注意是获取构造方式的属性所以定义时必须是static
const getDerivedStateFromProps = ctor.getDerivedStateFromProps;
if (typeof getDerivedStateFromProps === "function") {
applyDerivedStateFromProps(
workInProgress,
ctor,
getDerivedStateFromProps,
newProps
);
instance.state = workInProgress.memoizedState;
}
// react生命周期polyfilled
// 不安全的生命周期不应该被调用,要在组建中使用新的API
// 装载生命周期二(UNSAFE_componentWillMount || componentWillMount): 注意如果使用了一起使用了getDerivedStateFromProps和getSnapshotBeforeUpdate生命周期的话将不会调用
if (
typeof ctor.getDerivedStateFromProps !== "function" &&
typeof instance.getSnapshotBeforeUpdate !== "function" &&
(typeof instance.UNSAFE_componentWillMount === "function" ||
typeof instance.componentWillMount === "function")
) {
callComponentWillMount(workInProgress, instance);
//如果在此生命周期内还有其他状态更新,让我们
//立即处理它们
processUpdateQueue(workInProgress, newProps, instance, renderLanes);
instance.state = workInProgress.memoizedState;
}
// 如果有componentDidMount生命周期 将workInProgress.effectTag设置为包含Update tag,暂时不知道为啥
if (typeof instance.componentDidMount === "function") {
workInProgress.effectTag |= Update;
}
}
function markRef(current, workInProgress) {
const ref = workInProgress.ref;
if (
(current === null && ref !== null) ||
(current !== null && current.ref !== ref)
) {
// Schedule a Ref effect
workInProgress.effectTag |= Ref;
}
}
function finishClassComponent(
current,
workInProgress,
Component,
shouldUpdate,
hasContext,
renderLanes
) {
// 引用应该更新,即使shouldComponentUpdate返回false
markRef(current, workInProgress);
// 是否捕获到了错误
const didCaptureError =
(workInProgress.effectTag & DidCapture) !== NoEffect;
if (!shouldUpdate && !didCaptureError) {
// Context providers should defer to sCU for rendering
if (hasContext) {
invalidateContextProvider(workInProgress, Component, false);
}
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
const instance = workInProgress.stateNode;
// Rerender
ReactCurrentOwner.current = workInProgress;
let nextChildren;
if (
didCaptureError &&
typeof Component.getDerivedStateFromError !== "function"
) {
// 如果我们捕获了错误,但是未定义getDerivedStateFromError方法,就卸载所有子组件。
// componentDidCatch将安排一个update去重新渲染一个回退
// 这是暂时的知道我们所有人使用新的api
nextChildren = null;
if (enableProfilerTimer) {
stopProfilerTimerIfRunning(workInProgress);
}
} else {
if (__DEV__) {
setIsRendering(true);
nextChildren = instance.render();
if (
debugRenderPhaseSideEffectsForStrictMode &&
workInProgress.mode & StrictMode
) {
disableLogs();
try {
instance.render();
} finally {
reenableLogs();
}
}
setIsRendering(false);
} else {
// 调用组件实例的render方法返回
nextChildren = instance.render();
}
}
// React DevTools reads this flag.
workInProgress.effectTag |= PerformedWork;
if (current !== null && didCaptureError) {
// 如果我们正在从错误中恢复
// 请在不服用任何调节的情况下进行调节
// 现有的子组件。概念上讲,正常子组件和错误显示
// 的子组件是两个不同的合集,所以我们不应该
// 重用正常的子组件,及时他们身份匹配
forceUnmountCurrentAndReconcile(
current,
workInProgress,
nextChildren,
renderLanes
);
} else {
// 调用reconcileChildren方法调节子组件
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
}
// 使用我们刚刚用于渲染的值来记忆状态。
workInProgress.memoizedState = instance.state;
// 上下文可能已更改,因此我们需要重新计算。
if (hasContext) {
invalidateContextProvider(workInProgress, Component, true);
}
return workInProgress.child;
}
// 尽早推送上下文提供者,以防止上下文堆栈不匹配。
// 在挂载期间,我们还不知道子上下文,因为该实例不存在。
// 渲染后,我们将在finishClassComponent()中使子上下文无效。
let hasContext;
if (isLegacyContextProvider(Component)) {
hasContext = true;
pushLegacyContextProvider(workInProgress);
} else {
// true
hasContext = false;
}
prepareToReadContext(workInProgress, renderLanes);
const instance = workInProgress.stateNode;
let shouldUpdate;
if (instance === null) {
// true
if (current !== null) {
// false
// A class component without an instance only mounts if it suspended
// inside a non-concurrent tree, in an inconsistent state. We want to
// treat it like a new mount, even though an empty version of it already
// committed. Disconnect the alternate pointers.
current.alternate = null;
workInProgress.alternate = null;
// Since this is conceptually a new fiber, schedule a Placement effect
workInProgress.effectTag |= Placement;
}
// 在最初的过程中,我们可能需要构造实例。
constructClassInstance(workInProgress, Component, nextProps);
mountClassInstance(workInProgress, Component, nextProps, renderLanes);
shouldUpdate = true;
} else if (current === null) {
// In a resume, we'll already have an instance we can reuse.
shouldUpdate = resumeMountClassInstance(
workInProgress,
Component,
nextProps,
renderLanes
);
} else {
shouldUpdate = updateClassInstance(
current,
workInProgress,
Component,
nextProps,
renderLanes
);
}
const nextUnitOfWork = finishClassComponent(
current,
workInProgress,
Component,
shouldUpdate,
hasContext,
renderLanes
);
if (__DEV__) {
const inst = workInProgress.stateNode;
if (shouldUpdate && inst.props !== nextProps) {
if (!didWarnAboutReassigningProps) {
console.error(
"It looks like %s is reassigning its own `this.props` while rendering. " +
"This is not supported and can lead to confusing bugs.",
getComponentName(workInProgress.type) || "a component"
);
}
didWarnAboutReassigningProps = true;
}
}
return nextUnitOfWork;
}
3.2.2.4 updateHostComponent
function updateHostComponent(current, workInProgress, renderLanes) {
pushHostContext(workInProgress);
if (current === null) {
// tryToClaimNextHydratableInstance(workInProgress);
}
const type = workInProgress.type;
const nextProps = workInProgress.pendingProps;
const prevProps = current !== null ? current.memoizedProps : null;
let nextChildren = nextProps.children;
const isDirectTextChild = shouldSetTextContent(type, nextProps);
if (isDirectTextChild) {
// We special case a direct text child of a host node. This is a common
// case. We won't handle it as a reified child. We will instead handle
// this in the host environment that also has access to this prop. That
// avoids allocating another HostText fiber and traversing it.
nextChildren = null;
} else if (prevProps !== null && shouldSetTextContent(type, prevProps)) {
// If we're switching from a direct text child to a normal child, or to
// empty, we need to schedule the text content to be reset.
workInProgress.effectTag |= ContentReset;
}
markRef(current, workInProgress);
if (
(workInProgress.mode & ConcurrentMode) !== NoMode &&
nextProps.hasOwnProperty("hidden")
) {
const wrappedChildren = {
?typeof: REACT_ELEMENT_TYPE,
type: REACT_LEGACY_HIDDEN_TYPE,
key: null,
ref: null,
props: {
children: nextChildren,
// Check the host config to see if the children are offscreen/hidden.
mode: shouldDeprioritizeSubtree(type, nextProps) ? "hidden" : "visible",
},
_owner: __DEV__ ? {} : null,
};
nextChildren = wrappedChildren;
}
// 调节当前fiber节点的子节点
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
4 reconcileChildren
function reconcileChildren(current, workInProgress, nextChildren, renderLanes) {
if (current === null) {
// 如果这是尚未渲染的全新组件
// 我们不会通过应用最小的副作用来更新其子集
// 代替它的是,我们会在渲染它们之前将他们全部添加到child中
// 这意味着我们可以通过不跟踪副作用来优化和解流程
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes
);
} else {
// 如果current child和workInProgress相同,
// 这意味着我们没有在这些children上开始任何工作
// 因此,我们使用克隆算法去创建一个current child的副本
// 如果我们已经有了一些progressed work,在这一点是无效的
// 让我们把它抛出来
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes
);
}
}
4.1 reconcileChildFibers & mountChildFibers
调节(创建、删除、更新)Fiber 子节点,设置 Fiber 的 ref、sibling、return 等关键属性
reconcileChildFibers 和 mountChildFibers 的区别是前者追踪副作用后者不追踪
const reconcileChildFibers = ChildReconciler(true);
const mountChildFibers = ChildReconciler(false);
// 调节组件的children
function ChildReconciler(shouldTrackSideEffects) {
function deleteChild(returnFiber, childToDelete) {
if (!shouldTrackSideEffects) {
// Noop.
return;
}
// Deletions are added in reversed order so we add it to the front.
// At this point, the return fiber's effect list is empty except for
// deletions, so we can just append the deletion to the list. The remaining
// effects aren't added until the complete phase. Once we implement
// resuming, this may not be true.
const last = returnFiber.lastEffect;
if (last !== null) {
last.nextEffect = childToDelete;
returnFiber.lastEffect = childToDelete;
} else {
returnFiber.firstEffect = returnFiber.lastEffect = childToDelete;
}
childToDelete.nextEffect = null;
childToDelete.effectTag = Deletion;
}
function deleteRemainingChildren(returnFiber, currentFirstChild) {
if (!shouldTrackSideEffects) {
// Noop.
return null;
}
// TODO: For the shouldClone case, this could be micro-optimized a bit by
// assuming that after the first child we've already added everything.
let childToDelete = currentFirstChild;
while (childToDelete !== null) {
deleteChild(returnFiber, childToDelete);
childToDelete = childToDelete.sibling;
}
return null;
}
function mapRemainingChildren(returnFiber, currentFirstChild) {
// Add the remaining children to a temporary map so that we can find them by
// keys quickly. Implicit (null) keys get added to this set with their index
// instead.
const existingChildren = new Map();
let existingChild = currentFirstChild;
while (existingChild !== null) {
if (existingChild.key !== null) {
existingChildren.set(existingChild.key, existingChild);
} else {
existingChildren.set(existingChild.index, existingChild);
}
existingChild = existingChild.sibling;
}
return existingChildren;
}
function useFiber(fiber, pendingProps) {
// We currently set sibling to null and index to 0 here because it is easy
// to forget to do before returning it. E.g. for the single child case.
const clone = createWorkInProgress(fiber, pendingProps);
clone.index = 0;
clone.sibling = null;
return clone;
}
function placeChild(newFiber, lastPlacedIndex, newIndex) {
newFiber.index = newIndex;
if (!shouldTrackSideEffects) {
// Noop.
return lastPlacedIndex;
}
const current = newFiber.alternate;
if (current !== null) {
const oldIndex = current.index;
if (oldIndex < lastPlacedIndex) {
// This is a move.
newFiber.effectTag = Placement;
return lastPlacedIndex;
} else {
// This item can stay in place.
return oldIndex;
}
} else {
// This is an insertion.
newFiber.effectTag = Placement;
return lastPlacedIndex;
}
}
function placeSingleChild(newFiber) {
// This is simpler for the single child case. We only need to do a
// placement for inserting new children.
if (shouldTrackSideEffects && newFiber.alternate === null) {
newFiber.effectTag = Placement;
}
return newFiber;
}
function updateTextNode(returnFiber, current, textContent, lanes) {
if (current === null || current.tag !== HostText) {
// Insert
const created = createFiberFromText(textContent, returnFiber.mode, lanes);
created.return = returnFiber;
return created;
} else {
// Update
const existing = useFiber(current, textContent);
existing.return = returnFiber;
return existing;
}
}
function updateElement(returnFiber, current, element, lanes) {
if (current !== null) {
if (
current.elementType === element.type ||
// Keep this check inline so it only runs on the false path:
(__DEV__ ? isCompatibleFamilyForHotReloading(current, element) : false)
) {
// Move based on index
const existing = useFiber(current, element.props);
existing.ref = coerceRef(returnFiber, current, element);
existing.return = returnFiber;
if (__DEV__) {
existing._debugSource = element._source;
existing._debugOwner = element._owner;
}
return existing;
} else if (enableBlocksAPI && current.tag === Block) {
// The new Block might not be initialized yet. We need to initialize
// it in case initializing it turns out it would match.
let type = element.type;
if (type.?typeof === REACT_LAZY_TYPE) {
type = resolveLazyType(type);
}
if (
type.?typeof === REACT_BLOCK_TYPE &&
type === current.type._render
) {
// Same as above but also update the .type field.
const existing = useFiber(current, element.props);
existing.return = returnFiber;
existing.type = type;
if (__DEV__) {
existing._debugSource = element._source;
existing._debugOwner = element._owner;
}
return existing;
}
}
}
// Insert
const created = createFiberFromElement(element, returnFiber.mode, lanes);
created.ref = coerceRef(returnFiber, current, element);
created.return = returnFiber;
return created;
}
function updatePortal(returnFiber, current, portal, lanes) {
if (
current === null ||
current.tag !== HostPortal ||
current.stateNode.containerInfo !== portal.containerInfo ||
current.stateNode.implementation !== portal.implementation
) {
// Insert
const created = createFiberFromPortal(portal, returnFiber.mode, lanes);
created.return = returnFiber;
return created;
} else {
// Update
const existing = useFiber(current, portal.children || []);
existing.return = returnFiber;
return existing;
}
}
function updateFragment(returnFiber, current, fragment, lanes, key) {
if (current === null || current.tag !== Fragment) {
// Insert
const created = createFiberFromFragment(
fragment,
returnFiber.mode,
lanes,
key
);
created.return = returnFiber;
return created;
} else {
// Update
const existing = useFiber(current, fragment);
existing.return = returnFiber;
return existing;
}
}
function createChild(returnFiber, newChild, lanes) {
if (typeof newChild === "string" || typeof newChild === "number") {
// Text nodes don't have keys. If the previous node is implicitly keyed
// we can continue to replace it without aborting even if it is not a text
// node.
const created = createFiberFromText(
"" + newChild,
returnFiber.mode,
lanes
);
created.return = returnFiber;
return created;
}
if (typeof newChild === "object" && newChild !== null) {
switch (newChild.?typeof) {
case REACT_ELEMENT_TYPE: {
const created = createFiberFromElement(
newChild,
returnFiber.mode,
lanes
);
created.ref = coerceRef(returnFiber, null, newChild);
created.return = returnFiber;
return created;
}
case REACT_PORTAL_TYPE: {
const created = createFiberFromPortal(
newChild,
returnFiber.mode,
lanes
);
created.return = returnFiber;
return created;
}
}
if (isArray(newChild) || getIteratorFn(newChild)) {
const created = createFiberFromFragment(
newChild,
returnFiber.mode,
lanes,
null
);
created.return = returnFiber;
return created;
}
throwOnInvalidObjectType(returnFiber, newChild);
}
if (__DEV__) {
if (typeof newChild === "function") {
warnOnFunctionType(returnFiber);
}
}
return null;
}
function updateSlot(returnFiber, oldFiber, newChild, lanes) {
// Update the fiber if the keys match, otherwise return null.
const key = oldFiber !== null ? oldFiber.key : null;
if (typeof newChild === "string" || typeof newChild === "number") {
// Text nodes don't have keys. If the previous node is implicitly keyed
// we can continue to replace it without aborting even if it is not a text
// node.
if (key !== null) {
return null;
}
return updateTextNode(returnFiber, oldFiber, "" + newChild, lanes);
}
if (typeof newChild === "object" && newChild !== null) {
switch (newChild.?typeof) {
case REACT_ELEMENT_TYPE: {
if (newChild.key === key) {
if (newChild.type === REACT_FRAGMENT_TYPE) {
return updateFragment(
returnFiber,
oldFiber,
newChild.props.children,
lanes,
key
);
}
return updateElement(returnFiber, oldFiber, newChild, lanes);
} else {
return null;
}
}
case REACT_PORTAL_TYPE: {
if (newChild.key === key) {
return updatePortal(returnFiber, oldFiber, newChild, lanes);
} else {
return null;
}
}
}
if (isArray(newChild) || getIteratorFn(newChild)) {
if (key !== null) {
return null;
}
return updateFragment(returnFiber, oldFiber, newChild, lanes, null);
}
throwOnInvalidObjectType(returnFiber, newChild);
}
if (__DEV__) {
if (typeof newChild === "function") {
warnOnFunctionType(returnFiber);
}
}
return null;
}
function updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChild,
lanes
) {
if (typeof newChild === "string" || typeof newChild === "number") {
// Text nodes don't have keys, so we neither have to check the old nor
// new node for the key. If both are text nodes, they match.
const matchedFiber = existingChildren.get(newIdx) || null;
return updateTextNode(returnFiber, matchedFiber, "" + newChild, lanes);
}
if (typeof newChild === "object" && newChild !== null) {
switch (newChild.?typeof) {
case REACT_ELEMENT_TYPE: {
const matchedFiber =
existingChildren.get(
newChild.key === null ? newIdx : newChild.key
) || null;
if (newChild.type === REACT_FRAGMENT_TYPE) {
return updateFragment(
returnFiber,
matchedFiber,
newChild.props.children,
lanes,
newChild.key
);
}
return updateElement(returnFiber, matchedFiber, newChild, lanes);
}
case REACT_PORTAL_TYPE: {
const matchedFiber =
existingChildren.get(
newChild.key === null ? newIdx : newChild.key
) || null;
return updatePortal(returnFiber, matchedFiber, newChild, lanes);
}
}
if (isArray(newChild) || getIteratorFn(newChild)) {
const matchedFiber = existingChildren.get(newIdx) || null;
return updateFragment(returnFiber, matchedFiber, newChild, lanes, null);
}
throwOnInvalidObjectType(returnFiber, newChild);
}
if (__DEV__) {
if (typeof newChild === "function") {
warnOnFunctionType(returnFiber);
}
}
return null;
}
/**
* Warns if there is a duplicate or missing key
*/
function warnOnInvalidKey(child, knownKeys, returnFiber) {
if (__DEV__) {
if (typeof child !== "object" || child === null) {
return knownKeys;
}
switch (child.?typeof) {
case REACT_ELEMENT_TYPE:
case REACT_PORTAL_TYPE:
warnForMissingKey(child, returnFiber);
const key = child.key;
if (typeof key !== "string") {
break;
}
if (knownKeys === null) {
knownKeys = new Set();
knownKeys.add(key);
break;
}
if (!knownKeys.has(key)) {
knownKeys.add(key);
break;
}
console.error(
"Encountered two children with the same key, `%s`. " +
"Keys should be unique so that components maintain their identity " +
"across updates. Non-unique keys may cause children to be " +
"duplicated and/or omitted — the behavior is unsupported and " +
"could change in a future version.",
key
);
break;
default:
break;
}
}
return knownKeys;
}
function reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChildren,
lanes
) {
// This algorithm can't optimize by searching from both ends since we
// don't have backpointers on fibers. I'm trying to see how far we can get
// with that model. If it ends up not being worth the tradeoffs, we can
// add it later.
// Even with a two ended optimization, we'd want to optimize for the case
// where there are few changes and brute force the comparison instead of
// going for the Map. It'd like to explore hitting that path first in
// forward-only mode and only go for the Map once we notice that we need
// lots of look ahead. This doesn't handle reversal as well as two ended
// search but that's unusual. Besides, for the two ended optimization to
// work on Iterables, we'd need to copy the whole set.
// In this first iteration, we'll just live with hitting the bad case
// (adding everything to a Map) in for every insert/move.
// If you change this code, also update reconcileChildrenIterator() which
// uses the same algorithm.
if (__DEV__) {
// First, validate keys.
let knownKeys = null;
for (let i = 0; i < newChildren.length; i++) {
const child = newChildren[i];
knownKeys = warnOnInvalidKey(child, knownKeys, returnFiber);
}
}
let resultingFirstChild = null;
let previousNewFiber = null;
let oldFiber = currentFirstChild;
let lastPlacedIndex = 0;
let newIdx = 0;
let nextOldFiber = null;
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
lanes
);
if (newFiber === null) {
// TODO: This breaks on empty slots like null children. That's
// unfortunate because it triggers the slow path all the time. We need
// a better way to communicate whether this was a miss or null,
// boolean, undefined, etc.
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
if (shouldTrackSideEffects) {
if (oldFiber && newFiber.alternate === null) {
// We matched the slot, but we didn't reuse the existing fiber, so we
// need to delete the existing child.
deleteChild(returnFiber, oldFiber);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
// TODO: Move out of the loop. This only happens for the first run.
resultingFirstChild = newFiber;
} else {
// TODO: Defer siblings if we're not at the right index for this slot.
// I.e. if we had null values before, then we want to defer this
// for each null value. However, we also don't want to call updateSlot
// with the previous one.
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
if (newIdx === newChildren.length) {
// We've reached the end of the new children. We can delete the rest.
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
if (oldFiber === null) {
// If we don't have any more existing children we can choose a fast path
// since the rest will all be insertions.
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
if (newFiber === null) {
continue;
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
// TODO: Move out of the loop. This only happens for the first run.
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
return resultingFirstChild;
}
// Add all children to a key map for quick lookups.
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
// Keep scanning and use the map to restore deleted items as moves.
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
lanes
);
if (newFiber !== null) {
if (shouldTrackSideEffects) {
if (newFiber.alternate !== null) {
// The new fiber is a work in progress, but if there exists a
// current, that means that we reused the fiber. We need to delete
// it from the child list so that we don't add it to the deletion
// list.
existingChildren.delete(
newFiber.key === null ? newIdx : newFiber.key
);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
if (shouldTrackSideEffects) {
// Any existing children that weren't consumed above were deleted. We need
// to add them to the deletion list.
existingChildren.forEach((child) => deleteChild(returnFiber, child));
}
return resultingFirstChild;
}
function reconcileChildrenIterator(
returnFiber,
currentFirstChild,
newChildrenIterable,
lanes
) {
// This is the same implementation as reconcileChildrenArray(),
// but using the iterator instead.
const iteratorFn = getIteratorFn(newChildrenIterable);
invariant(
typeof iteratorFn === "function",
"An object is not an iterable. This error is likely caused by a bug in " +
"React. Please file an issue."
);
if (__DEV__) {
// We don't support rendering Generators because it's a mutation.
// See https://github.com/facebook/react/issues/12995
if (
typeof Symbol === "function" &&
// $FlowFixMe Flow doesn't know about toStringTag
newChildrenIterable[Symbol.toStringTag] === "Generator"
) {
if (!didWarnAboutGenerators) {
console.error(
"Using Generators as children is unsupported and will likely yield " +
"unexpected results because enumerating a generator mutates it. " +
"You may convert it to an array with `Array.from()` or the " +
"`[...spread]` operator before rendering. Keep in mind " +
"you might need to polyfill these features for older browsers."
);
}
didWarnAboutGenerators = true;
}
// Warn about using Maps as children
if (newChildrenIterable.entries === iteratorFn) {
if (!didWarnAboutMaps) {
console.error(
"Using Maps as children is not supported. " +
"Use an array of keyed ReactElements instead."
);
}
didWarnAboutMaps = true;
}
// First, validate keys.
// We'll get a different iterator later for the main pass.
const newChildren = iteratorFn.call(newChildrenIterable);
if (newChildren) {
let knownKeys = null;
let step = newChildren.next();
for (; !step.done; step = newChildren.next()) {
const child = step.value;
knownKeys = warnOnInvalidKey(child, knownKeys, returnFiber);
}
}
}
const newChildren = iteratorFn.call(newChildrenIterable);
invariant(newChildren != null, "An iterable object provided no iterator.");
let resultingFirstChild = null;
let previousNewFiber = null;
let oldFiber = currentFirstChild;
let lastPlacedIndex = 0;
let newIdx = 0;
let nextOldFiber = null;
let step = newChildren.next();
for (
;
oldFiber !== null && !step.done;
newIdx++, step = newChildren.next()
) {
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
const newFiber = updateSlot(returnFiber, oldFiber, step.value, lanes);
if (newFiber === null) {
// TODO: This breaks on empty slots like null children. That's
// unfortunate because it triggers the slow path all the time. We need
// a better way to communicate whether this was a miss or null,
// boolean, undefined, etc.
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
if (shouldTrackSideEffects) {
if (oldFiber && newFiber.alternate === null) {
// We matched the slot, but we didn't reuse the existing fiber, so we
// need to delete the existing child.
deleteChild(returnFiber, oldFiber);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
// TODO: Move out of the loop. This only happens for the first run.
resultingFirstChild = newFiber;
} else {
// TODO: Defer siblings if we're not at the right index for this slot.
// I.e. if we had null values before, then we want to defer this
// for each null value. However, we also don't want to call updateSlot
// with the previous one.
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
if (step.done) {
// We've reached the end of the new children. We can delete the rest.
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
if (oldFiber === null) {
// If we don't have any more existing children we can choose a fast path
// since the rest will all be insertions.
for (; !step.done; newIdx++, step = newChildren.next()) {
const newFiber = createChild(returnFiber, step.value, lanes);
if (newFiber === null) {
continue;
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
// TODO: Move out of the loop. This only happens for the first run.
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
return resultingFirstChild;
}
// Add all children to a key map for quick lookups.
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
// Keep scanning and use the map to restore deleted items as moves.
for (; !step.done; newIdx++, step = newChildren.next()) {
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
step.value,
lanes
);
if (newFiber !== null) {
if (shouldTrackSideEffects) {
if (newFiber.alternate !== null) {
// The new fiber is a work in progress, but if there exists a
// current, that means that we reused the fiber. We need to delete
// it from the child list so that we don't add it to the deletion
// list.
existingChildren.delete(
newFiber.key === null ? newIdx : newFiber.key
);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
if (shouldTrackSideEffects) {
// Any existing children that weren't consumed above were deleted. We need
// to add them to the deletion list.
existingChildren.forEach((child) => deleteChild(returnFiber, child));
}
return resultingFirstChild;
}
function reconcileSingleTextNode(
returnFiber,
currentFirstChild,
textContent,
lanes
) {
// There's no need to check for keys on text nodes since we don't have a
// way to define them.
if (currentFirstChild !== null && currentFirstChild.tag === HostText) {
// We already have an existing node so let's just update it and delete
// the rest.
deleteRemainingChildren(returnFiber, currentFirstChild.sibling);
const existing = useFiber(currentFirstChild, textContent);
existing.return = returnFiber;
return existing;
}
// The existing first child is not a text node so we need to create one
// and delete the existing ones.
deleteRemainingChildren(returnFiber, currentFirstChild);
const created = createFiberFromText(textContent, returnFiber.mode, lanes);
created.return = returnFiber;
return created;
}
function reconcileSingleElement(
returnFiber,
currentFirstChild,
element,
lanes
) {
const key = element.key;
let child = currentFirstChild;
while (child !== null) {
// TODO: If key === null and child.key === null, then this only applies to
// the first item in the list.
if (child.key === key) {
switch (child.tag) {
case Fragment: {
if (element.type === REACT_FRAGMENT_TYPE) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props.children);
existing.return = returnFiber;
if (__DEV__) {
existing._debugSource = element._source;
existing._debugOwner = element._owner;
}
return existing;
}
break;
}
case Block:
if (enableBlocksAPI) {
let type = element.type;
if (type.?typeof === REACT_LAZY_TYPE) {
type = resolveLazyType(type);
}
if (type.?typeof === REACT_BLOCK_TYPE) {
// The new Block might not be initialized yet. We need to initialize
// it in case initializing it turns out it would match.
if (type._render === child.type._render) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props);
existing.type = type;
existing.return = returnFiber;
if (__DEV__) {
existing._debugSource = element._source;
existing._debugOwner = element._owner;
}
return existing;
}
}
}
// We intentionally fallthrough here if enableBlocksAPI is not on.
// eslint-disable-next-lined no-fallthrough
default: {
if (
child.elementType === element.type ||
// Keep this check inline so it only runs on the false path:
(__DEV__
? isCompatibleFamilyForHotReloading(child, element)
: false)
) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props);
existing.ref = coerceRef(returnFiber, child, element);
existing.return = returnFiber;
if (__DEV__) {
existing._debugSource = element._source;
existing._debugOwner = element._owner;
}
return existing;
}
break;
}
}
// Didn't match.
deleteRemainingChildren(returnFiber, child);
break;
} else {
deleteChild(returnFiber, child);
}
child = child.sibling;
}
if (element.type === REACT_FRAGMENT_TYPE) {
const created = createFiberFromFragment(
element.props.children,
returnFiber.mode,
lanes,
element.key
);
created.return = returnFiber;
return created;
} else {
const created = createFiberFromElement(element, returnFiber.mode, lanes);
created.ref = coerceRef(returnFiber, currentFirstChild, element);
created.return = returnFiber;
return created;
}
}
function reconcileSinglePortal(
returnFiber,
currentFirstChild,
portal,
lanes
) {
const key = portal.key;
let child = currentFirstChild;
while (child !== null) {
// TODO: If key === null and child.key === null, then this only applies to
// the first item in the list.
if (child.key === key) {
if (
child.tag === HostPortal &&
child.stateNode.containerInfo === portal.containerInfo &&
child.stateNode.implementation === portal.implementation
) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, portal.children || []);
existing.return = returnFiber;
return existing;
} else {
deleteRemainingChildren(returnFiber, child);
break;
}
} else {
deleteChild(returnFiber, child);
}
child = child.sibling;
}
const created = createFiberFromPortal(portal, returnFiber.mode, lanes);
created.return = returnFiber;
return created;
}
// This API will tag the children with the side-effect of the reconciliation
// itself. They will be added to the side-effect list as we pass through the
// children and the parent.
function reconcileChildFibers(
returnFiber,
currentFirstChild,
newChild,
lanes
) {
// This function is not recursive.
// If the top level item is an array, we treat it as a set of children,
// not as a fragment. Nested arrays on the other hand will be treated as
// fragment nodes. Recursion happens at the normal flow.
// Handle top level unkeyed fragments as if they were arrays.
// This leads to an ambiguity between <>{[...]}</> and <>...</>.
// We treat the ambiguous cases above the same.
const isUnkeyedTopLevelFragment =
typeof newChild === "object" &&
newChild !== null &&
newChild.type === REACT_FRAGMENT_TYPE &&
newChild.key === null;
if (isUnkeyedTopLevelFragment) {
newChild = newChild.props.children;
}
// Handle object types
const isObject = typeof newChild === "object" && newChild !== null;
if (isObject) {
switch (newChild.?typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
lanes
)
);
case REACT_PORTAL_TYPE:
return placeSingleChild(
reconcileSinglePortal(
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
);
}
if (getIteratorFn(newChild)) {
return reconcileChildrenIterator(
returnFiber,
currentFirstChild,
newChild,
lanes
);
}
if (isObject) {
throwOnInvalidObjectType(returnFiber, newChild);
}
if (__DEV__) {
if (typeof newChild === "function") {
warnOnFunctionType(returnFiber);
}
}
if (typeof newChild === "undefined" && !isUnkeyedTopLevelFragment) {
// If the new child is undefined, and the return fiber is a composite
// component, throw an error. If Fiber return types are disabled,
// we already threw above.
switch (returnFiber.tag) {
case ClassComponent: {
if (__DEV__) {
const instance = returnFiber.stateNode;
if (instance.render._isMockFunction) {
// We allow auto-mocks to proceed as if they're returning null.
break;
}
}
}
// Intentionally fall through to the next case, which handles both
// functions and classes
// eslint-disable-next-lined no-fallthrough
case FunctionComponent: {
const Component = returnFiber.type;
invariant(
false,
"%s(...): Nothing was returned from render. This usually means a " +
"return statement is missing. Or, to render nothing, " +
"return null.",
Component.displayName || Component.name || "Component"
);
}
}
}
// Remaining cases are all treated as empty.
return deleteRemainingChildren(returnFiber, currentFirstChild);
}
return reconcileChildFibers;
}
5 completeUnitOfWork
功能:
① 协助 workLoopSync 深度优先(先序优先)遍历生成 Fiber 树,原理:当 workLoopSync 遍历到当前分支最深的节点时向上寻找父节点并判断父节点有无同级节点,有的话将 workInProgress 其设置为此节点,从而实现寻找下一个节点
② 判断 effectTag 创建副作用链表(由子结点往上指向父节点)
③ 调用 completeWork 方法
疑问 ① 为什要创建副作用链表 答:当项目复杂时 Fiber 树拥有很多很多的节点,如果通过遍历每个节点的方式去运行当前节点副作用的话时间复杂度会上升,所以在构建树的时候通过判断每个节点的 effectTag 将副作用关联起来生成一个链表可以有效的降低时间复杂度提升程序效率。
疑问 ② 为什么子结点的副作用要排在父节点的副作用之前:
答:副作用处理 componentDidMount 生命周期和 ref,根据生命周期 componentDidMount 的定义可以知道只有当子组件渲染完成时,父组件的 componentDidMount 生命周期才会被调用,所以子组件的 componentDidMount 生命周期运行一定在父组件之前,因此副作用链表排序是符合功能实现的,ref 方法也同理,父组件通过子组件的 ref 获取实例时应该确保子组件的 ref 已经被处理。
function completeUnitOfWork(unitOfWork) {
// 尝试完成当前的工作单元,然后移动到下一个同级节点
// 如果没有更多同级节点,返回到父节点
let completedWork = unitOfWork;
do {
// 当前节点,已刷新,该fiber的状态是备份的。
// 理想状态下没有什么应该以来它,是在这里依靠它意味着我们不
// 需要添加其他字段到workInProgress。
const current = completedWork.alternate;
const returnFiber = completedWork.return;
// 检查工作是否完成或是否有东西抛出。
if ((completedWork.effectTag & Incomplete) === NoEffect) {
let next;
if (
!enableProfilerTimer ||
(completedWork.mode & ProfileMode) === NoMode
) {
// true
next = completeWork(current, completedWork, subtreeRenderLanes);
} else {
next = completeWork(current, completedWork, subtreeRenderLanes);
// 假设我们没有错误,请更新渲染时间。
}
resetChildLanes(completedWork);
if (next !== null) {
// 完成这个fiber结点的时候产生了一个新的work,接下来处理这个work
workInProgress = next;
return;
}
if (
returnFiber !== null &&
// 如果通级节点未完成,不要给父节点添加effects
(returnFiber.effectTag & Incomplete) === NoEffect
) {
// 把当前节点的副作用添加到父节点
// 递归的方式替换子结点和父节点的副作用顺序
// 从而实现firstEffect为最深的拥有副作用的节点
// 而nextEffect指向父节点
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;
}
const effectTag = completedWork.effectTag;
// 如果effectTag大于1(如添加了ref或者ClassComponent等)
// 如果父节点不存在副作用链表则创建副作用链表添加到父节点(子结点副作用向上冒泡)
// 如果父节点存在副作用链表则用nextEffect指向自己然后重置lastEffect等于自己
if (effectTag > PerformedWork) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork;
} else {
returnFiber.firstEffect = completedWork;
}
returnFiber.lastEffect = completedWork;
}
}
} else {
// This fiber did not complete because something threw. Pop values off
// the stack without entering the complete phase. If this is a boundary,
// capture values if possible.
const next = unwindWork(completedWork, subtreeRenderLanes);
// Because this fiber did not complete, don't reset its expiration time.
if (
enableProfilerTimer &&
(completedWork.mode & ProfileMode) !== NoMode
) {
// Record the render duration for the fiber that errored.
stopProfilerTimerIfRunningAndRecordDelta(completedWork, false);
// Include the time spent working on failed children before continuing.
let actualDuration = completedWork.actualDuration;
let child = completedWork.child;
while (child !== null) {
actualDuration += child.actualDuration;
child = child.sibling;
}
completedWork.actualDuration = actualDuration;
}
if (next !== null) {
// If completing this work spawned new work, do that next. We'll come
// back here again.
// Since we're restarting, remove anything that is not a host effect
// from the effect tag.
next.effectTag &= HostEffectMask;
workInProgress = next;
return;
}
if (returnFiber !== null) {
// Mark the parent fiber as incomplete and clear its effect list.
returnFiber.firstEffect = returnFiber.lastEffect = null;
returnFiber.effectTag |= Incomplete;
}
}
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
// 如果父节点的同级节点还有更多的工作,把它设置为下一个
workInProgress = siblingFiber;
return;
}
// 否则,返回父节点
completedWork = returnFiber;
workInProgress = completedWork;
} while (completedWork !== null);
// 完成整棵树的构建
if (workInProgressRootExitStatus === RootIncomplete) {
workInProgressRootExitStatus = RootCompleted;
}
}
5.1 completeWork
功能:
根据当前节点的类型,实现创建、更新真实 DOM 的功能(DOM 创建完成后存储在内存中并未挂载到 container)
更新时包含对 virtual tree 的 diff 后续深入研究
function completeWork(current, workInProgress, renderLanes) {
function appendAllChildren(
parent,
workInProgress,
needsVisibilityToggle,
isHidden
) {
// We only have the top Fiber that was created but we need recurse down its
// children to find all the terminal nodes.
let node = workInProgress.child;
while (node !== null) {
if (node.tag === HostComponent || node.tag === HostText) {
appendInitialChild(parent, node.stateNode);
} else if (enableFundamentalAPI && node.tag === FundamentalComponent) {
appendInitialChild(parent, node.stateNode.instance);
} else if (node.tag === HostPortal) {
// If we have a portal child, then we don't want to traverse
// down its children. Instead, we'll get insertions from each child in
// the portal directly.
} else if (node.child !== null) {
node.child.return = node;
node = node.child;
continue;
}
if (node === workInProgress) {
return;
}
while (node.sibling === null) {
if (node.return === null || node.return === workInProgress) {
return;
}
node = node.return;
}
node.sibling.return = node.return;
node = node.sibling;
}
}
function appendInitialChild(parentInstance, child) {
parentInstance.appendChild(child);
}
function updateHostContainer(workInProgress) {
// Noop
}
function updateHostComponent(
current,
workInProgress,
type,
newProps,
rootContainerInstance
) {
// If we have an alternate, that means this is an update and we need to
// schedule a side-effect to do the updates.
const oldProps = current.memoizedProps;
if (oldProps === newProps) {
// In mutation mode, this is sufficient for a bailout because
// we won't touch this node even if children changed.
return;
}
// If we get updated because one of our children updated, we don't
// have newProps so we'll have to reuse them.
// TODO: Split the update API as separate for the props vs. children.
// Even better would be if children weren't special cased at all tho.
const instance = workInProgress.stateNode;
const currentHostContext = getHostContext();
// TODO: Experiencing an error where oldProps is null. Suggests a host
// component is hitting the resume path. Figure out why. Possibly
// related to `hidden`.
const updatePayload = prepareUpdate(
instance,
type,
oldProps,
newProps,
rootContainerInstance,
currentHostContext
);
// TODO: Type this specific to this type of component.
workInProgress.updateQueue = updatePayload;
// If the update payload indicates that there is a change or if there
// is a new ref we mark this as an update. All the work is done in commitWork.
if (updatePayload) {
markUpdate(workInProgress);
}
}
function updateHostText(current, workInProgress, oldText, newText) {
// If the text differs, mark it as an update. All the work in done in commitWork.
if (oldText !== newText) {
markUpdate(workInProgress);
}
}
function finalizeInitialChildren(
domElement,
type,
props,
rootContainerInstance,
hostContext
) {
setInitialProperties(domElement, type, props, rootContainerInstance);
return shouldAutoFocusHostComponent(type, props);
}
function createInstance(
type,
props,
rootContainerInstance,
hostContext,
internalInstanceHandle
) {
let parentNamespace;
if (__DEV__) {
// TODO: take namespace into account when validating.
const hostContextDev = hostContext;
validateDOMNesting(type, null, hostContextDev.ancestorInfo);
if (
typeof props.children === "string" ||
typeof props.children === "number"
) {
const string = "" + props.children;
const ownAncestorInfo = updatedAncestorInfo(
hostContextDev.ancestorInfo,
type
);
validateDOMNesting(null, string, ownAncestorInfo);
}
parentNamespace = hostContextDev.namespace;
} else {
parentNamespace = hostContext;
}
const domElement = createRealElement(
// origin function name: createElement
type,
props,
rootContainerInstance,
parentNamespace
);
// DOM中设置一个属性访问当前fiber备份
precacheFiberNode(internalInstanceHandle, domElement);
// DOM中设置一个属性访问当前fiber props
updateFiberProps(domElement, props);
return domElement;
}
function createRealElement(
type,
props,
rootContainerElement,
parentNamespace
) {
let isCustomComponentTag;
// We create tags in the namespace of their parent container, except HTML
// tags get no namespace.
const ownerDocument = getOwnerDocumentFromRootContainer(
rootContainerElement
);
let domElement;
let namespaceURI = parentNamespace;
if (namespaceURI === HTML_NAMESPACE) {
namespaceURI = getIntrinsicNamespace(type);
}
if (namespaceURI === HTML_NAMESPACE) {
const lowerCaseType = type.toLowerCase();
if (__DEV__) {
isCustomComponentTag = isCustomComponent(type, props);
// Should this check be gated by parent namespace? Not sure we want to
// allow <SVG> or <mATH>.
if (!isCustomComponentTag && type !== lowerCaseType) {
console.error(
"<%s /> is using incorrect casing. " +
"Use PascalCase for React components, " +
"or lowercase for HTML elements.",
type
);
}
}
if (lowerCaseType === "script") {
// Create the script via .innerHTML so its "parser-inserted" flag is
// set to true and it does not execute
const div = ownerDocument.createElement("div");
if (__DEV__) {
if (enableTrustedTypesIntegration && !didWarnScriptTags) {
console.error(
"Encountered a script tag while rendering React component. " +
"Scripts inside React components are never executed when rendering " +
"on the client. Consider using template tag instead " +
"(https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template)."
);
didWarnScriptTags = true;
}
}
div.innerHTML = "<script><" + "/script>"; // eslint-disable-line
// This is guaranteed to yield a script element.
const firstChild = div.firstChild;
domElement = div.removeChild(firstChild);
} else if (typeof props.is === "string") {
// $FlowIssue `createElement` should be updated for Web Components
domElement = ownerDocument.createElement(type, { is: props.is });
} else {
// Separate else branch instead of using `props.is || undefined` above because of a Firefox bug.
// See discussion in https://github.com/facebook/react/pull/6896
// and discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1276240
domElement = ownerDocument.createElement(type);
// Normally attributes are assigned in `setInitialDOMProperties`, however the `multiple` and `size`
// attributes on `select`s needs to be added before `option`s are inserted.
// This prevents:
// - a bug where the `select` does not scroll to the correct option because singular
// `select` elements automatically pick the first item #13222
// - a bug where the `select` set the first item as selected despite the `size` attribute #14239
// See https://github.com/facebook/react/issues/13222
// and https://github.com/facebook/react/issues/14239
if (type === "select") {
const node = domElement;
if (props.multiple) {
node.multiple = true;
} else if (props.size) {
// Setting a size greater than 1 causes a select to behave like `multiple=true`, where
// it is possible that no option is selected.
//
// This is only necessary when a select in "single selection mode".
node.size = props.size;
}
}
}
} else {
domElement = ownerDocument.createElementNS(namespaceURI, type);
}
if (__DEV__) {
if (namespaceURI === HTML_NAMESPACE) {
if (
!isCustomComponentTag &&
Object.prototype.toString.call(domElement) ===
"[object HTMLUnknownElement]" &&
!Object.prototype.hasOwnProperty.call(warnedUnknownTags, type)
) {
warnedUnknownTags[type] = true;
console.error(
"The tag <%s> is unrecognized in this browser. " +
"If you meant to render a React component, start its name with " +
"an uppercase letter.",
type
);
}
}
}
return domElement;
}
function setInitialProperties(
domElement,
tag,
rawProps,
rootContainerElement
) {
const isCustomComponentTag = isCustomComponent(tag, rawProps);
if (__DEV__) {
validatePropertiesInDevelopment(tag, rawProps);
}
// TODO: Make sure that we check isMounted before firing any of these events.
let props;
switch (tag) {
case "iframe":
case "object":
case "embed":
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_LOAD, domElement);
}
props = rawProps;
break;
case "video":
case "audio":
if (!enableModernEventSystem) {
// Create listener for each media event
for (let i = 0; i < mediaEventTypes.length; i++) {
legacyTrapBubbledEvent(mediaEventTypes[i], domElement);
}
}
props = rawProps;
break;
case "source":
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_ERROR, domElement);
}
props = rawProps;
break;
case "img":
case "image":
case "link":
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_ERROR, domElement);
legacyTrapBubbledEvent(TOP_LOAD, domElement);
}
props = rawProps;
break;
case "form":
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_RESET, domElement);
legacyTrapBubbledEvent(TOP_SUBMIT, domElement);
}
props = rawProps;
break;
case "details":
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_TOGGLE, domElement);
}
props = rawProps;
break;
case "input":
ReactDOMInputInitWrapperState(domElement, rawProps);
props = ReactDOMInputGetHostProps(domElement, rawProps);
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_INVALID, domElement);
}
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, "onChange");
break;
case "option":
ReactDOMOptionValidateProps(domElement, rawProps);
props = ReactDOMOptionGetHostProps(domElement, rawProps);
break;
case "select":
ReactDOMSelectInitWrapperState(domElement, rawProps);
props = ReactDOMSelectGetHostProps(domElement, rawProps);
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_INVALID, domElement);
}
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, "onChange");
break;
case "textarea":
ReactDOMTextareaInitWrapperState(domElement, rawProps);
props = ReactDOMTextareaGetHostProps(domElement, rawProps);
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_INVALID, domElement);
}
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, "onChange");
break;
default:
props = rawProps;
}
assertValidProps(tag, props);
setInitialDOMProperties(
tag,
domElement,
rootContainerElement,
props,
isCustomComponentTag
);
switch (tag) {
case "input":
// TODO: Make sure we check if this is still unmounted or do any clean
// up necessary since we never stop tracking anymore.
track(domElement);
ReactDOMInputPostMountWrapper(domElement, rawProps, false);
break;
case "textarea":
// TODO: Make sure we check if this is still unmounted or do any clean
// up necessary since we never stop tracking anymore.
track(domElement);
ReactDOMTextareaPostMountWrapper(domElement, rawProps);
break;
case "option":
ReactDOMOptionPostMountWrapper(domElement, rawProps);
break;
case "select":
ReactDOMSelectPostMountWrapper(domElement, rawProps);
break;
default:
if (typeof props.onClick === "function") {
// TODO: This cast may not be sound for SVG, MathML or custom elements.
trapClickOnNonInteractiveElement(domElement);
}
break;
}
}
function setValueForProperty(node, name, value, isCustomComponentTag) {
const properties = {};
function getPropertyInfo(name) {
return properties.hasOwnProperty(name) ? properties[name] : null;
}
function shouldIgnoreAttribute(name, propertyInfo, isCustomComponentTag) {
if (propertyInfo !== null) {
return propertyInfo.type === RESERVED;
}
if (isCustomComponentTag) {
return false;
}
if (
name.length > 2 &&
(name[0] === "o" || name[0] === "O") &&
(name[1] === "n" || name[1] === "N")
) {
return true;
}
return false;
}
function shouldRemoveAttributeWithWarning(
name,
value,
propertyInfo,
isCustomComponentTag
) {
if (propertyInfo !== null && propertyInfo.type === RESERVED) {
return false;
}
switch (typeof value) {
case "function":
// $FlowIssue symbol is perfectly valid here
case "symbol": // eslint-disable-line
return true;
case "boolean": {
if (isCustomComponentTag) {
return false;
}
if (propertyInfo !== null) {
return !propertyInfo.acceptsBooleans;
} else {
const prefix = name.toLowerCase().slice(0, 5);
return prefix !== "data-" && prefix !== "aria-";
}
}
default:
return false;
}
}
function shouldRemoveAttribute(
name,
value,
propertyInfo,
isCustomComponentTag
) {
const RESERVED = 0;
const STRING = 1;
const BOOLEANISH_STRING = 2;
const BOOLEAN = 3;
const OVERLOADED_BOOLEAN = 4;
const NUMERIC = 5;
const POSITIVE_NUMERIC = 6;
if (value === null || typeof value === "undefined") {
return true;
}
if (
shouldRemoveAttributeWithWarning(
name,
value,
propertyInfo,
isCustomComponentTag
)
) {
return true;
}
if (isCustomComponentTag) {
return false;
}
if (propertyInfo !== null) {
if (enableFilterEmptyStringAttributesDOM) {
if (propertyInfo.removeEmptyString && value === "") {
return true;
}
}
switch (propertyInfo.type) {
case BOOLEAN:
return !value;
case OVERLOADED_BOOLEAN:
return value === false;
case NUMERIC:
return isNaN(value);
case POSITIVE_NUMERIC:
return isNaN(value) || value < 1;
}
}
return false;
}
const hasOwnProperty = Object.prototype.hasOwnProperty;
const illegalAttributeNameCache = {};
const validatedAttributeNameCache = {};
const ATTRIBUTE_NAME_START_CHAR =
":A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD";
const ATTRIBUTE_NAME_CHAR =
ATTRIBUTE_NAME_START_CHAR +
"\\-.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040";
const VALID_ATTRIBUTE_NAME_REGEX = new RegExp(
"^[" + ATTRIBUTE_NAME_START_CHAR + "][" + ATTRIBUTE_NAME_CHAR + "]*$"
);
function isAttributeNameSafe(attributeName) {
if (hasOwnProperty.call(validatedAttributeNameCache, attributeName)) {
return true;
}
if (hasOwnProperty.call(illegalAttributeNameCache, attributeName)) {
return false;
}
if (VALID_ATTRIBUTE_NAME_REGEX.test(attributeName)) {
validatedAttributeNameCache[attributeName] = true;
return true;
}
illegalAttributeNameCache[attributeName] = true;
return false;
}
const propertyInfo = getPropertyInfo(name);
if (shouldIgnoreAttribute(name, propertyInfo, isCustomComponentTag)) {
return;
}
if (
shouldRemoveAttribute(name, value, propertyInfo, isCustomComponentTag)
) {
value = null;
}
// If the prop isn't in the special list, treat it as a simple attribute.
if (isCustomComponentTag || propertyInfo === null) {
if (isAttributeNameSafe(name)) {
const attributeName = name;
if (value === null) {
node.removeAttribute(attributeName);
} else {
node.setAttribute(
attributeName,
enableTrustedTypesIntegration ? value : "" + value
);
}
}
return;
}
const { mustUseProperty } = propertyInfo;
if (mustUseProperty) {
const { propertyName } = propertyInfo;
if (value === null) {
const { type } = propertyInfo;
node[propertyName] = type === BOOLEAN ? false : "";
} else {
// Contrary to `setAttribute`, object properties are properly
// `toString`ed by IE8/9.
node[propertyName] = value;
}
return;
}
// The rest are treated as attributes with special cases.
const { attributeName, attributeNamespace } = propertyInfo;
if (value === null) {
node.removeAttribute(attributeName);
} else {
const { type } = propertyInfo;
let attributeValue;
if (type === BOOLEAN || (type === OVERLOADED_BOOLEAN && value === true)) {
// If attribute type is boolean, we know for sure it won't be an execution sink
// and we won't require Trusted Type here.
attributeValue = "";
} else {
// `setAttribute` with objects becomes only `[object]` in IE8/9,
// ('' + value) makes it output the correct toString()-value.
if (enableTrustedTypesIntegration) {
attributeValue = value;
} else {
attributeValue = "" + value;
}
if (propertyInfo.sanitizeURL) {
sanitizeURL(attributeValue.toString());
}
}
if (attributeNamespace) {
node.setAttributeNS(attributeNamespace, attributeName, attributeValue);
} else {
node.setAttribute(attributeName, attributeValue);
}
}
}
function setInitialDOMProperties(
tag,
domElement,
rootContainerElement,
nextProps,
isCustomComponentTag
) {
const DANGEROUSLY_SET_INNER_HTML = "dangerouslySetInnerHTML";
const SUPPRESS_CONTENT_EDITABLE_WARNING = "suppressContentEditableWarning";
const SUPPRESS_HYDRATION_WARNING = "suppressHydrationWarning";
const AUTOFOCUS = "autoFocus";
const CHILDREN = "children";
const STYLE = "style";
const HTML = "__html";
const DEPRECATED_flareListeners = "DEPRECATED_flareListeners";
const registrationNameModules = {};
for (const propKey in nextProps) {
if (!nextProps.hasOwnProperty(propKey)) {
continue;
}
const nextProp = nextProps[propKey];
if (propKey === STYLE) {
if (__DEV__) {
if (nextProp) {
// Freeze the next style object so that we can assume it won't be
// mutated. We have already warned for this in the past.
Object.freeze(nextProp);
}
}
// Relies on `updateStylesByID` not mutating `styleUpdates`.
setValueForStyles(domElement, nextProp);
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
const nextHtml = nextProp ? nextProp[HTML] : undefined;
if (nextHtml != null) {
setInnerHTML(domElement, nextHtml);
}
} else if (propKey === CHILDREN) {
if (typeof nextProp === "string") {
// Avoid setting initial textContent when the text is empty. In IE11 setting
// textContent on a <textarea> will cause the placeholder to not
// show within the <textarea> until it has been focused and blurred again.
// https://github.com/facebook/react/issues/6731#issuecomment-254874553
const canSetTextContent = tag !== "textarea" || nextProp !== "";
if (canSetTextContent) {
setTextContent(domElement, nextProp);
}
} else if (typeof nextProp === "number") {
setTextContent(domElement, "" + nextProp);
}
} else if (
(enableDeprecatedFlareAPI && propKey === DEPRECATED_flareListeners) ||
propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
propKey === SUPPRESS_HYDRATION_WARNING
) {
// Noop
} else if (propKey === AUTOFOCUS) {
// We polyfill it separately on the client during commit.
// We could have excluded it in the property list instead of
// adding a special case here, but then it wouldn't be emitted
// on server rendering (but we *do* want to emit it in SSR).
} else if (registrationNameModules.hasOwnProperty(propKey)) {
if (nextProp != null) {
if (__DEV__ && typeof nextProp !== "function") {
warnForInvalidEventListener(propKey, nextProp);
}
ensureListeningTo(rootContainerElement, propKey);
}
} else if (nextProp != null) {
setValueForProperty(
domElement,
propKey,
nextProp,
isCustomComponentTag
);
}
}
}
function isCustomComponent(tagName, props) {
if (tagName.indexOf("-") === -1) {
return typeof props.is === "string";
}
switch (tagName) {
// These are reserved SVG and MathML elements.
// We don't mind this whitelist too much because we expect it to never grow.
// The alternative is to track the namespace in a few places which is convoluted.
// https://w3c.github.io/webcomponents/spec/custom/#custom-elements-core-concepts
case "annotation-xml":
case "color-profile":
case "font-face":
case "font-face-src":
case "font-face-uri":
case "font-face-format":
case "font-face-name":
case "missing-glyph":
return false;
default:
return true;
}
}
function shouldAutoFocusHostComponent(type, props) {
switch (type) {
case "button":
case "input":
case "select":
case "textarea":
return !!props.autoFocus;
}
return false;
}
function markRef(workInProgress) {
workInProgress.effectTag |= Ref;
}
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: {
const Component = workInProgress.type;
if (isLegacyContextProvider(Component)) {
popLegacyContext(workInProgress);
}
return null;
}
case HostRoot: {
popHostContainer(workInProgress);
popTopLevelLegacyContextObject(workInProgress);
resetMutableSourceWorkInProgressVersions();
const fiberRoot = workInProgress.stateNode;
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.
const 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.hydrate) {
// 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 the container would already be empty).
workInProgress.effectTag |= Snapshot;
}
}
updateHostContainer(workInProgress);
return null;
}
case HostComponent: {
popHostContext(workInProgress);
const rootContainerInstance = getRootHostContainer();
const type = workInProgress.type;
if (current !== null && workInProgress.stateNode != null) {
updateHostComponent(
current,
workInProgress,
type,
newProps,
rootContainerInstance
);
if (enableDeprecatedFlareAPI) {
const prevListeners = current.memoizedProps.DEPRECATED_flareListeners;
const nextListeners = newProps.DEPRECATED_flareListeners;
if (prevListeners !== nextListeners) {
markUpdate(workInProgress);
}
}
if (current.ref !== workInProgress.ref) {
markRef(workInProgress);
}
} else {
if (!newProps) {
invariant(
workInProgress.stateNode !== null,
"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.
return null;
}
const 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.
const 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);
}
if (enableDeprecatedFlareAPI) {
const listeners = newProps.DEPRECATED_flareListeners;
if (listeners != null) {
updateDeprecatedEventListeners(
listeners,
workInProgress,
rootContainerInstance
);
}
}
} else {
const instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress
);
// fixme
appendAllChildren(instance, workInProgress, false, false);
// This needs to be set before we mount Flare event listeners
workInProgress.stateNode = instance;
if (enableDeprecatedFlareAPI) {
const listeners = newProps.DEPRECATED_flareListeners;
if (listeners != null) {
updateDeprecatedEventListeners(
listeners,
workInProgress,
rootContainerInstance
);
}
}
// 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,
currentHostContext
)
) {
markUpdate(workInProgress);
}
}
if (workInProgress.ref !== null) {
// If there is a ref on a host node we need to schedule a callback
markRef(workInProgress);
}
}
return null;
}
case HostText: {
const newText = newProps;
if (current && workInProgress.stateNode != null) {
const 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") {
invariant(
workInProgress.stateNode !== null,
"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.
}
const rootContainerInstance = getRootHostContainer();
const currentHostContext = getHostContext();
const wasHydrated = popHydrationState(workInProgress);
if (wasHydrated) {
if (prepareToHydrateHostTextInstance(workInProgress)) {
markUpdate(workInProgress);
}
} else {
workInProgress.stateNode = createTextInstance(
newText,
rootContainerInstance,
currentHostContext,
workInProgress
);
}
}
return null;
}
case SuspenseComponent: {
popSuspenseContext(workInProgress);
const nextState = workInProgress.memoizedState;
if (enableSuspenseServerRenderer) {
if (nextState !== null && nextState.dehydrated !== null) {
if (current === null) {
const wasHydrated = popHydrationState(workInProgress);
invariant(
wasHydrated,
"A dehydrated suspense component was completed without a hydrated node. " +
"This is probably a bug in React."
);
prepareToHydrateHostSuspenseInstance(workInProgress);
if (enableSchedulerTracing) {
markSpawnedWork(OffscreenLane);
}
return null;
} else {
// We should never have been in a hydration state if we didn't have a current.
// However, in some of those paths, we might have reentered a hydration state
// and then we might be inside a hydration state. In that case, we'll need to exit out of it.
resetHydrationState();
if ((workInProgress.effectTag & DidCapture) === NoEffect) {
// 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.effectTag |= Update;
return null;
}
}
}
if ((workInProgress.effectTag & DidCapture) !== NoEffect) {
// Something suspended. Re-render with the fallback children.
workInProgress.lanes = renderLanes;
// Do not reset the effect list.
return workInProgress;
}
const nextDidTimeout = nextState !== null;
let prevDidTimeout = false;
if (current === null) {
if (workInProgress.memoizedProps.fallback !== undefined) {
popHydrationState(workInProgress);
}
} else {
const prevState = current.memoizedState;
prevDidTimeout = prevState !== null;
}
if (nextDidTimeout && !prevDidTimeout) {
// If this subtreee is running in blocking mode we can suspend,
// otherwise we won't suspend.
// 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 & BlockingMode) !== 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.
const 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();
}
}
}
if (supportsPersistence) {
// TODO: Only schedule updates if not prevDidTimeout.
if (nextDidTimeout) {
// If this boundary just timed out, schedule an effect to attach a
// retry listener to the promise. This flag is also used to hide the
// primary children.
workInProgress.effectTag |= Update;
}
}
if (supportsMutation) {
// TODO: Only schedule updates if these values are non equal, i.e. it changed.
if (nextDidTimeout || prevDidTimeout) {
// If this boundary just timed out, schedule an effect to attach a
// retry listener to the promise. This flag is also used to hide the
// primary children. In mutation mode, we also need the flag to
// *unhide* children that were previously hidden, so check if this
// is currently timed out, too.
workInProgress.effectTag |= Update;
}
}
if (
enableSuspenseCallback &&
workInProgress.updateQueue !== null &&
workInProgress.memoizedProps.suspenseCallback != null
) {
// Always notify the callback
workInProgress.effectTag |= Update;
}
return null;
}
case HostPortal:
popHostContainer(workInProgress);
updateHostContainer(workInProgress);
if (current === null) {
preparePortalMount(workInProgress.stateNode.containerInfo);
}
return null;
case ContextProvider:
// Pop provider fiber
popProvider(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.
const Component = workInProgress.type;
if (isLegacyContextProvider(Component)) {
popLegacyContext(workInProgress);
}
return null;
}
case SuspenseListComponent: {
popSuspenseContext(workInProgress);
const renderState = workInProgress.memoizedState;
if (renderState === null) {
// We're running in the default, "independent" mode.
// We don't do anything in this mode.
return null;
}
let didSuspendAlready =
(workInProgress.effectTag & DidCapture) !== NoEffect;
const 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.
const cannotBeSuspended =
renderHasNotSuspendedYet() &&
(current === null || (current.effectTag & DidCapture) === NoEffect);
if (!cannotBeSuspended) {
let row = workInProgress.child;
while (row !== null) {
const suspended = findFirstSuspended(row);
if (suspended !== null) {
didSuspendAlready = true;
workInProgress.effectTag |= 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 thennables. Instead, we'll transfer its thennables 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.
const newThennables = suspended.updateQueue;
if (newThennables !== null) {
workInProgress.updateQueue = newThennables;
workInProgress.effectTag |= Update;
}
// Rerender the whole list, but this time, we'll force fallbacks
// to stay in place.
// Reset the effect list before doing the second pass since that's now invalid.
if (renderState.lastEffect === null) {
workInProgress.firstEffect = null;
}
workInProgress.lastEffect = renderState.lastEffect;
// Reset the child fibers to their original state.
resetChildFibers(workInProgress, renderLanes);
// Set up the Suspense Context to force suspense and immediately
// rerender the children.
pushSuspenseContext(
workInProgress,
setShallowSuspenseContext(
suspenseStackCursor.current,
ForceSuspenseFallback
)
);
return workInProgress.child;
}
row = row.sibling;
}
}
} else {
cutOffTailIfNeeded(renderState, false);
}
// Next we're going to render the tail.
} else {
// Append the rendered row to the child list.
if (!didSuspendAlready) {
const suspended = findFirstSuspended(renderedTail);
if (suspended !== null) {
workInProgress.effectTag |= 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.
const newThennables = suspended.updateQueue;
if (newThennables !== null) {
workInProgress.updateQueue = newThennables;
workInProgress.effectTag |= Update;
}
cutOffTailIfNeeded(renderState, true);
// This might have been modified.
if (
renderState.tail === null &&
renderState.tailMode === "hidden" &&
!renderedTail.alternate
) {
// We need to delete the row we just rendered.
// Reset the effect list to what it was before we rendered this
// child. The nested children have already appended themselves.
const lastEffect = (workInProgress.lastEffect =
renderState.lastEffect);
// Remove any effects that were appended after this point.
if (lastEffect !== null) {
lastEffect.nextEffect = null;
}
// We're done.
return null;
}
} else if (
// The time it took to render last row is greater than time until
// the expiration.
now() * 2 - renderState.renderingStartTime >
renderState.tailExpiration &&
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.effectTag |= 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. If we can show
// them, then they really have the same priority as this render.
// So we'll pick it back up the very next render pass once we've had
// an opportunity to yield for paint.
workInProgress.lanes = renderLanes;
if (enableSchedulerTracing) {
markSpawnedWork(renderLanes);
}
}
}
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 {
const 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.
if (renderState.tailExpiration === 0) {
// Heuristic for how long we're willing to spend rendering rows
// until we just give up and show what we have so far.
const TAIL_EXPIRATION_TIMEOUT_MS = 500;
renderState.tailExpiration = now() + TAIL_EXPIRATION_TIMEOUT_MS;
// TODO: This is meant to mimic the train model or JND but this
// is a per component value. It should really be since the start
// of the total render or last commit. Consider using something like
// globalMostRecentFallbackTime. That doesn't account for being
// suspended for part of the time or when it's a new render.
// It should probably use a global start time value instead.
}
// Pop a row.
const next = renderState.tail;
renderState.rendering = next;
renderState.tail = next.sibling;
renderState.lastEffect = workInProgress.lastEffect;
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.
let suspenseContext = suspenseStackCursor.current;
if (didSuspendAlready) {
suspenseContext = setShallowSuspenseContext(
suspenseContext,
ForceSuspenseFallback
);
} else {
suspenseContext = setDefaultShallowSuspenseContext(suspenseContext);
}
pushSuspenseContext(workInProgress, suspenseContext);
// Do a pass over the next row.
return next;
}
return null;
}
case FundamentalComponent: {
if (enableFundamentalAPI) {
const fundamentalImpl = workInProgress.type.impl;
let fundamentalInstance = workInProgress.stateNode;
if (fundamentalInstance === null) {
const getInitialState = fundamentalImpl.getInitialState;
let fundamentalState;
if (getInitialState !== undefined) {
fundamentalState = getInitialState(newProps);
}
fundamentalInstance = workInProgress.stateNode = createFundamentalStateInstance(
workInProgress,
newProps,
fundamentalImpl,
fundamentalState || {}
);
const instance = getFundamentalComponentInstance(fundamentalInstance);
fundamentalInstance.instance = instance;
if (fundamentalImpl.reconcileChildren === false) {
return null;
}
appendAllChildren(instance, workInProgress, false, false);
mountFundamentalComponent(fundamentalInstance);
} else {
// We fire update in commit phase
const prevProps = fundamentalInstance.props;
fundamentalInstance.prevProps = prevProps;
fundamentalInstance.props = newProps;
fundamentalInstance.currentFiber = workInProgress;
if (supportsPersistence) {
const instance = cloneFundamentalInstance(fundamentalInstance);
fundamentalInstance.instance = instance;
appendAllChildren(instance, workInProgress, false, false);
}
const shouldUpdate = shouldUpdateFundamentalComponent(
fundamentalInstance
);
if (shouldUpdate) {
markUpdate(workInProgress);
}
}
return null;
}
break;
}
case ScopeComponent: {
if (enableScopeAPI) {
if (current === null) {
const scopeInstance = createScopeInstance();
workInProgress.stateNode = scopeInstance;
if (enableDeprecatedFlareAPI) {
const listeners = newProps.DEPRECATED_flareListeners;
if (listeners != null) {
const rootContainerInstance = getRootHostContainer();
updateDeprecatedEventListeners(
listeners,
workInProgress,
rootContainerInstance
);
}
}
prepareScopeUpdate(scopeInstance, workInProgress);
if (workInProgress.ref !== null) {
markRef(workInProgress);
markUpdate(workInProgress);
}
} else {
if (enableDeprecatedFlareAPI) {
const prevListeners =
current.memoizedProps.DEPRECATED_flareListeners;
const nextListeners = newProps.DEPRECATED_flareListeners;
if (
prevListeners !== nextListeners ||
workInProgress.ref !== null
) {
markUpdate(workInProgress);
}
} else {
if (workInProgress.ref !== null) {
markUpdate(workInProgress);
}
}
if (current.ref !== workInProgress.ref) {
markRef(workInProgress);
}
}
return null;
}
break;
}
case Block:
if (enableBlocksAPI) {
return null;
}
break;
case OffscreenComponent:
case LegacyHiddenComponent: {
popRenderLanes(workInProgress);
if (current !== null) {
const nextState = workInProgress.memoizedState;
const prevState = current.memoizedState;
const prevIsHidden = prevState !== null;
const nextIsHidden = nextState !== null;
if (prevIsHidden !== nextIsHidden) {
workInProgress.effectTag |= Update;
}
}
return null;
}
}
invariant(
false,
"Unknown unit of work tag (%s). This error is likely caused by a bug in " +
"React. Please file an issue.",
workInProgress.tag
);
}
小结: \
function resetChildLanes(completedWork) {
if (
(completedWork.tag === LegacyHiddenComponent ||
completedWork.tag === OffscreenComponent) &&
completedWork.memoizedState !== null &&
!includesSomeLane(subtreeRenderLanes, OffscreenLane)
) {
// 该组件的子组件被隐藏,不要冒泡它们的到期时间
return;
}
let newChildLanes = NoLanes;
if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode) {
// false
let actualDuration = completedWork.actualDuration;
let treeBaseDuration = completedWork.selfBaseDuration;
const shouldBubbleActualDurations =
completedWork.alternate === null ||
completedWork.child !== completedWork.alternate.child;
let child = completedWork.child;
while (child !== null) {
newChildLanes = mergeLanes(
newChildLanes,
mergeLanes(child.lanes, child.childLanes)
);
if (shouldBubbleActualDurations) {
actualDuration += child.actualDuration;
}
treeBaseDuration += child.treeBaseDuration;
child = child.sibling;
}
completedWork.actualDuration = actualDuration;
completedWork.treeBaseDuration = treeBaseDuration;
} else {
// 合并所有同级结点及它们的子组件的lane
let child = completedWork.child;
while (child !== null) {
newChildLanes = mergeLanes(
newChildLanes,
mergeLanes(child.lanes, child.childLanes)
);
child = child.sibling;
}
}
// 重置当前fiber结点的childLanes
completedWork.childLanes = newChildLanes;
}
上下文相关,涉及到 ClassComponent 的 context,暂时不深入研究
6 commitRoot
功能:初次渲染最后的提交阶段,调用副作用链表中的节点的生命周期方法,将此前构建好的内存中保存的整棵 DOM 树 append 到 root container(根容器中)
function commitRoot(root) {
const renderPriorityLevel = getCurrentPriorityLevel();
runWithPriority(
ImmediatePriority,
commitRootImpl.bind(null, root, renderPriorityLevel)
);
return null;
}
function markRootFinished(root, remainingLanes) {
root.pendingLanes = remainingLanes;
// 让我们再试一次
root.suspendedLanes = 0;
root.pingedLanes = 0;
root.expiredLanes &= remainingLanes;
root.mutableReadLanes &= remainingLanes;
}
function commitRootImpl(root, renderPriorityLevel) {
do {
// `flushPassiveEffects` will call `flushSyncUpdateQueue` at the end, which
// means `flushPassiveEffects` will sometimes result in additional
// passive effects. So we need to keep flushing in a loop until there are
// no more pending effects.
// TODO: Might be better if `flushPassiveEffects` did not automatically
// flush synchronous work at the end, to avoid factoring hazards like this.
flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null);
const finishedWork = root.finishedWork;
const lanes = root.finishedLanes;
if (finishedWork === null) {
return null;
}
root.finishedWork = null;
root.finishedLanes = NoLanes;
// commitRoot永远不会反回延续;它总是同步完成的
// 因此,我们现在可以清除这些内容以允许安排新的回调。
root.callbackNode = null;
root.callbackId = NoLanes;
// TODO: Use LanePriority instead of SchedulerPriority
if (renderPriorityLevel < ImmediatePriority) {
// 如果这是并发渲染,则可以重置到期时间。
root.expiresAt = -1;
}
// 在root上更新第一个和最后一个pending times。
// 最新一个pending time就是root fiber剩余的时间
let 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;
} else {
// This indicates that the last root we worked on is not the same one that
// we're committing now. This most commonly happens when a suspended root
// times out.
}
// 获取副作用链表
let firstEffect;
if (finishedWork.effectTag > PerformedWork) {
// Fiber的副作用链表仅包含拥有副作用的子结点,而不是他本身。
// 因此root有一个副作用,我们需要添加它到链表尾部。
// 最终的链表的顺序是子结点在父结点之前的,fiber树中所有的副作用都包含在这个链表中
if (finishedWork.lastEffect !== null) {
finishedWork.lastEffect.nextEffect = finishedWork;
firstEffect = finishedWork.firstEffect;
} else {
firstEffect = finishedWork;
}
} else {
// root没有副作用
firstEffect = finishedWork.firstEffect;
}
if (firstEffect !== null) {
const prevExecutionContext = executionContext;
executionContext |= CommitContext;
const prevInteractions = pushInteractions(root);
// 在调用生命周期之前将此值重置为null
ReactCurrentOwner.current = null;
// commit阶段分为几个子阶段。
// 我们对每个节点的副作用列表都要做一次对应的处理:
// 所有的mutation effects都比layout effects优先
// 第一个阶段叫做”before mutation“阶段(mutate应该是指DOM树host tree已经构建完成了在内存中存储但是还未append到根容器)
// 我们用这个阶段去读取host tree的状态在我们mutate它之前
// 在这个阶段会调用getSnapshotBeforeUpdate
focusedInstanceHandle = prepareForCommit(root.containerInfo);
shouldFireAfterActiveInstanceBlur = false;
nextEffect = firstEffect;
do {
if (__DEV__) {
invokeGuardedCallback(null, commitBeforeMutationEffects, null);
if (hasCaughtError()) {
invariant(nextEffect !== null, "Should be working on an effect.");
const error = clearCaughtError();
captureCommitPhaseError(nextEffect, error);
nextEffect = nextEffect.nextEffect;
}
} else {
try {
commitBeforeMutationEffects();
} catch (error) {
console.error(error);
invariant(nextEffect !== null, "Should be working on an effect.");
captureCommitPhaseError(nextEffect, error);
nextEffect = nextEffect.nextEffect;
}
}
} while (nextEffect !== null);
// We no longer need to track the active instance fiber
focusedInstanceHandle = null;
if (enableProfilerTimer) {
// Mark the current commit time to be shared by all Profilers in this
// batch. This enables them to be grouped later.
recordCommitTime();
}
// 下一个阶段是”mutation“阶段,在这个阶段我们会把构建好的DOM树(host tree)append到根容器
// 重要!只有到了这个阶段react的渲染效果才会呈现在浏览器上
nextEffect = firstEffect;
do {
if (__DEV__) {
invokeGuardedCallback(
null,
commitMutationEffects,
null,
root,
renderPriorityLevel
);
if (hasCaughtError()) {
invariant(nextEffect !== null, "Should be working on an effect.");
const error = clearCaughtError();
captureCommitPhaseError(nextEffect, error);
nextEffect = nextEffect.nextEffect;
}
} else {
try {
commitMutationEffects(root, renderPriorityLevel);
} catch (error) {
console.error(error);
invariant(nextEffect !== null, "Should be working on an effect.");
captureCommitPhaseError(nextEffect, error);
nextEffect = nextEffect.nextEffect;
}
}
} while (nextEffect !== null);
if (shouldFireAfterActiveInstanceBlur) {
afterActiveInstanceBlur();
}
resetAfterCommit(root.containerInfo);
// The work-in-progress tree is now the current tree. This must come after
// the mutation phase, so that the previous tree is still current during
// componentWillUnmount, but before the layout phase, so that the finished
// work is current during componentDidMount/Update.
root.current = finishedWork;
// 下一个阶段是“layout”阶段,我们调用副作用方法在host tree被挂载进更容器后。
// 这个阶段的习惯用法是用于布局,但出于遗留原因,类组件生命周期也会触发。
nextEffect = firstEffect;
do {
if (__DEV__) {
invokeGuardedCallback(null, commitLayoutEffects, null, root, lanes);
if (hasCaughtError()) {
invariant(nextEffect !== null, "Should be working on an effect.");
const error = clearCaughtError();
captureCommitPhaseError(nextEffect, error);
nextEffect = nextEffect.nextEffect;
}
} else {
try {
commitLayoutEffects(root, lanes);
} catch (error) {
console.error(error);
invariant(nextEffect !== null, "Should be working on an effect.");
captureCommitPhaseError(nextEffect, error);
nextEffect = nextEffect.nextEffect;
}
}
} while (nextEffect !== null);
nextEffect = null;
// Tell Scheduler to yield at the end of the frame, so the browser has an
// opportunity to paint.
// requestPaint();
if (enableSchedulerTracing) {
popInteractions(prevInteractions);
}
executionContext = prevExecutionContext;
} else {
// No effects.
root.current = finishedWork;
// Measure these anyway so the flamegraph explicitly shows that there were
// no effects.
// TODO: Maybe there's a better way to report this.
if (enableProfilerTimer) {
recordCommitTime();
}
}
}
6.1 commitBeforeMutationEffects
let focusedInstanceHandle = null;
let shouldFireAfterActiveInstanceBlur = false;
function commitBeforeMutationEffects() {
while (nextEffect !== null) {
if (
!shouldFireAfterActiveInstanceBlur &&
focusedInstanceHandle !== null &&
isFiberHiddenOrDeletedAndContains(nextEffect, focusedInstanceHandle)
) {
shouldFireAfterActiveInstanceBlur = true;
beforeActiveInstanceBlur();
}
const effectTag = nextEffect.effectTag;
if ((effectTag & Snapshot) !== NoEffect) {
const current = nextEffect.alternate;
commitBeforeMutationEffectOnFiber(current, nextEffect);
}
if ((effectTag & Passive) !== NoEffect) {
// If there are passive effects, schedule a callback to flush at
// the earliest opportunity.
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
}
}
nextEffect = nextEffect.nextEffect;
}
}
function commitBeforeMutationEffectOnFiber(current, finishedWork) {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block: {
return;
}
case ClassComponent: {
if (finishedWork.effectTag & Snapshot) {
if (current !== null) {
const prevProps = current.memoizedProps;
const prevState = current.memoizedState;
const instance = finishedWork.stateNode;
// We could update instance props and state here,
// but instead we rely on them being set during last render.
// TODO: revisit this when we implement resuming.
if (__DEV__) {
// ,,,
}
// 生命周期三(getSnapshotBeforeUpdate):挂载真实DOM进去之前的处理,常用于列表的扩容
const snapshot = instance.getSnapshotBeforeUpdate(
finishedWork.elementType === finishedWork.type
? prevProps
: resolveDefaultProps(finishedWork.type, prevProps),
prevState
);
if (__DEV__) {
const didWarnSet = didWarnAboutUndefinedSnapshotBeforeUpdate;
if (snapshot === undefined && !didWarnSet.has(finishedWork.type)) {
didWarnSet.add(finishedWork.type);
console.error(
"%s.getSnapshotBeforeUpdate(): A snapshot value (or null) " +
"must be returned. You have returned undefined.",
getComponentName(finishedWork.type)
);
}
}
instance.__reactInternalSnapshotBeforeUpdate = snapshot;
}
}
return;
}
case HostRoot: {
if (supportsMutation) {
if (finishedWork.effectTag & Snapshot) {
const root = finishedWork.stateNode;
clearContainer(root.containerInfo);
}
}
return;
}
case HostComponent:
case HostText:
case HostPortal:
case IncompleteClassComponent:
// Nothing to do for these component types
return;
}
invariant(
false,
"This unit of work tag should not have side-effects. This error is " +
"likely caused by a bug in React. Please file an issue."
);
}
6.2 commitMutationEffects
将构建好的 DOM 树 append 到根容器
function commitMutationEffects(root, renderPriorityLevel) {
// TODO: Should probably move the bulk of this function to commitWork.
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
if (effectTag & ContentReset) {
commitResetTextContent(nextEffect);
}
if (effectTag & Ref) {
const current = nextEffect.alternate;
if (current !== null) {
// 清除当前节点的ref属性设置为null
commitDetachRef(current);
}
}
// The following switch statement is only concerned about placement,
// updates, and deletions. To avoid needing to add a case for every possible
// bitmap value, we remove the secondary effects from the effect tag and
// switch on that value.
const primaryEffectTag =
effectTag & (Placement | Update | Deletion | Hydrating);
switch (primaryEffectTag) {
case Placement: {
commitPlacement(nextEffect);
// Clear the "placement" from effect tag so that we know that this is
// inserted, before any life-cycles like componentDidMount gets called.
// TODO: findDOMNode doesn't rely on this any more but isMounted does
// and isMounted is deprecated anyway so we should be able to kill this.
nextEffect.effectTag &= ~Placement;
break;
}
case PlacementAndUpdate: {
// Placement
commitPlacement(nextEffect);
// Clear the "placement" from effect tag so that we know that this is
// inserted, before any life-cycles like componentDidMount gets called.
nextEffect.effectTag &= ~Placement;
// Update
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Hydrating: {
nextEffect.effectTag &= ~Hydrating;
break;
}
case HydratingAndUpdate: {
nextEffect.effectTag &= ~Hydrating;
// Update
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Update: {
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Deletion: {
commitDeletion(root, nextEffect, renderPriorityLevel);
break;
}
}
nextEffect = nextEffect.nextEffect;
}
}
function commitResetTextContent(current) {
if (!supportsMutation) {
return;
}
resetTextContent(current.stateNode);
}
function resetTextContent(domElement) {
setTextContent(domElement, "");
}
const setTextContent = function (node, text) {
if (text) {
const firstChild = node.firstChild;
if (
firstChild &&
firstChild === node.lastChild &&
firstChild.nodeType === TEXT_NODE
) {
firstChild.nodeValue = text;
return;
}
}
node.textContent = text;
};
function commitDetachRef(current) {
const currentRef = current.ref;
if (currentRef !== null) {
if (typeof currentRef === "function") {
currentRef(null);
} else {
currentRef.current = null;
}
}
}
function commitPlacement(finishedWork) {
if (!supportsMutation) {
return;
}
// Recursively insert all host nodes into the parent.
const parentFiber = getHostParentFiber(finishedWork);
let parent;
let isContainer;
const parentStateNode = parentFiber.stateNode;
switch (parentFiber.tag) {
case HostComponent:
parent = parentStateNode;
isContainer = false;
break;
case HostRoot:
// 只有根节点设置isContainer为true
parent = parentStateNode.containerInfo;
isContainer = true;
break;
case HostPortal:
parent = parentStateNode.containerInfo;
isContainer = true;
break;
case FundamentalComponent:
if (enableFundamentalAPI) {
parent = parentStateNode.instance;
isContainer = false;
}
// eslint-disable-next-line-no-fallthrough
default:
invariant(
false,
"Invalid host parent fiber. This error is likely caused by a bug " +
"in React. Please file an issue."
);
}
if (parentFiber.effectTag & ContentReset) {
// 在插入任何东西之前重置父节点文本内容
resetTextContent(parent);
// 亦或操作符代表:在effect tag中清除ContentReset标志位,代表已经处理过了下次无需处理
parentFiber.effectTag &= ~ContentReset;
}
const before = getHostSibling(finishedWork);
// We only have the top Fiber that was inserted but we need to recurse down its
// children to find all the terminal nodes.
if (isContainer) {
insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
} else {
insertOrAppendPlacementNode(finishedWork, before, parent);
}
}
function insertOrAppendPlacementNodeIntoContainer(node, before, parent) {
const { tag } = node;
const isHost = tag === HostComponent || tag === HostText;
if (isHost || (enableFundamentalAPI && tag === FundamentalComponent)) {
const stateNode = isHost ? node.stateNode : node.stateNode.instance;
if (before) {
insertInContainerBefore(parent, stateNode, before);
} else {
// true
appendChildToContainer(parent, stateNode);
}
} else if (tag === HostPortal) {
// If the insertion itself is a portal, then we don't want to traverse
// down its children. Instead, we'll get insertions from each child in
// the portal directly.
} else {
const child = node.child;
if (child !== null) {
insertOrAppendPlacementNodeIntoContainer(child, before, parent);
let sibling = child.sibling;
while (sibling !== null) {
insertOrAppendPlacementNodeIntoContainer(sibling, before, parent);
sibling = sibling.sibling;
}
}
}
}
function insertOrAppendPlacementNode(node, before, parent) {
const { tag } = node;
const isHost = tag === HostComponent || tag === HostText;
if (isHost || (enableFundamentalAPI && tag === FundamentalComponent)) {
const stateNode = isHost ? node.stateNode : node.stateNode.instance;
if (before) {
insertBefore(parent, stateNode, before);
} else {
appendChild(parent, stateNode);
}
} else if (tag === HostPortal) {
// If the insertion itself is a portal, then we don't want to traverse
// down its children. Instead, we'll get insertions from each child in
// the portal directly.
} else {
const child = node.child;
if (child !== null) {
insertOrAppendPlacementNode(child, before, parent);
let sibling = child.sibling;
while (sibling !== null) {
insertOrAppendPlacementNode(sibling, before, parent);
sibling = sibling.sibling;
}
}
}
}
// 有before
function insertInContainerBefore(container, child, beforeChild) {
if (container.nodeType === COMMENT_NODE) {
container.parentNode.insertBefore(child, beforeChild);
} else {
container.insertBefore(child, beforeChild);
}
}
// 没有before
function appendChildToContainer(container, child) {
let parentNode;
if (container.nodeType === COMMENT_NODE) {
parentNode = container.parentNode;
parentNode.insertBefore(child, container);
} else {
// true
parentNode = container;
// 最终挂载,此时效果展示在浏览器上。
parentNode.appendChild(child);
}
// This container might be used for a portal.
// If something inside a portal is clicked, that click should bubble
// through the React tree. However, on Mobile Safari the click would
// never bubble through the *DOM* tree unless an ancestor with onclick
// event exists. So we wouldn't see it and dispatch it.
// This is why we ensure that non React root containers have inline onclick
// defined.
// https://github.com/facebook/react/issues/11918
const reactRootContainer = container._reactRootContainer;
if (
(reactRootContainer === null || reactRootContainer === undefined) &&
parentNode.onclick === null
) {
// TODO: This cast may not be sound for SVG, MathML or custom elements.
trapClickOnNonInteractiveElement(parentNode);
}
}
6.3 commitLayoutEffects
function commitLayoutEffects(root, committedLanes) {
// TODO: Should probably move the bulk of this function to commitWork.
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
if (effectTag & (Update | Callback)) {
const current = nextEffect.alternate;
commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes);
}
if (effectTag & Ref) {
commitAttachRef(nextEffect);
}
nextEffect = nextEffect.nextEffect;
}
}
function commitLayoutEffectOnFiber(
finishedRoot,
current,
finishedWork,
committedLanes
) {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block: {
// At this point layout effects have already been destroyed (during mutation phase).
// This is done to prevent sibling component effects from interfering with each other,
// e.g. a destroy function in one component should never override a ref set
// by a create function in another component during the same commit.
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
finishedWork.mode & ProfileMode
) {
try {
startLayoutEffectTimer();
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
} finally {
recordLayoutEffectDuration(finishedWork);
}
} else {
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
}
if (runAllPassiveEffectDestroysBeforeCreates) {
schedulePassiveEffects(finishedWork);
}
return;
}
case ClassComponent: {
const instance = finishedWork.stateNode;
if (finishedWork.effectTag & Update) {
if (current === null) {
if (__DEV__) {
// ...
}
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
finishedWork.mode & ProfileMode
) {
try {
startLayoutEffectTimer();
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;
// We could update instance props and state here,
// but instead we rely on them being set during last render.
// TODO: revisit this when we implement resuming.
if (__DEV__) {
// ...
}
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
finishedWork.mode & ProfileMode
) {
try {
startLayoutEffectTimer();
instance.componentDidUpdate(
prevProps,
prevState,
instance.__reactInternalSnapshotBeforeUpdate
);
} finally {
recordLayoutEffectDuration(finishedWork);
}
} else {
// true
// 生命周期四(componentDidUpdate):挂载完成
instance.componentDidUpdate(
prevProps,
prevState,
instance.__reactInternalSnapshotBeforeUpdate
);
}
}
}
// TODO: I think this is now always non-null by the time it reaches the
// commit phase. Consider removing the type check.
const updateQueue = finishedWork.updateQueue;
if (updateQueue !== null) {
if (__DEV__) {
// ...
}
// We could update instance props and state here,
// but instead we rely on them being set during last render.
// TODO: revisit this when we implement resuming.
commitUpdateQueue(finishedWork, updateQueue, instance);
}
return;
}
case HostRoot: {
// TODO: I think this is now always non-null by the time it reaches the
// commit phase. Consider removing the type check.
const updateQueue = finishedWork.updateQueue;
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(finishedWork, updateQueue, instance);
}
return;
}
case HostComponent: {
const instance = finishedWork.stateNode;
// Renderers may schedule work to be done after host components are mounted
// (eg DOM renderer may schedule auto-focus for inputs and form controls).
// These effects should only be committed when components are first mounted,
// aka when there is no current/alternate.
if (current === null && finishedWork.effectTag & Update) {
const type = finishedWork.type;
const props = finishedWork.memoizedProps;
commitMount(instance, type, props, finishedWork);
}
return;
}
case HostText: {
// We have no life-cycles associated with text.
return;
}
case HostPortal: {
// We have no life-cycles associated with portals.
return;
}
case Profiler: {
if (enableProfilerTimer) {
const { onCommit, onRender } = finishedWork.memoizedProps;
const { effectDuration } = finishedWork.stateNode;
const commitTime = getCommitTime();
if (typeof onRender === "function") {
if (enableSchedulerTracing) {
onRender(
finishedWork.memoizedProps.id,
current === null ? "mount" : "update",
finishedWork.actualDuration,
finishedWork.treeBaseDuration,
finishedWork.actualStartTime,
commitTime,
finishedRoot.memoizedInteractions
);
} else {
onRender(
finishedWork.memoizedProps.id,
current === null ? "mount" : "update",
finishedWork.actualDuration,
finishedWork.treeBaseDuration,
finishedWork.actualStartTime,
commitTime
);
}
}
if (enableProfilerCommitHooks) {
if (typeof onCommit === "function") {
if (enableSchedulerTracing) {
onCommit(
finishedWork.memoizedProps.id,
current === null ? "mount" : "update",
effectDuration,
commitTime,
finishedRoot.memoizedInteractions
);
} else {
onCommit(
finishedWork.memoizedProps.id,
current === null ? "mount" : "update",
effectDuration,
commitTime
);
}
}
// Schedule a passive effect for this Profiler to call onPostCommit hooks.
// This effect should be scheduled even if there is no onPostCommit callback for this Profiler,
// because the effect is also where times bubble to parent Profilers.
enqueuePendingPassiveProfilerEffect(finishedWork);
// Propagate layout effect durations to the next nearest Profiler ancestor.
// Do not reset these values until the next render so DevTools has a chance to read them first.
let parentFiber = finishedWork.return;
while (parentFiber !== null) {
if (parentFiber.tag === Profiler) {
const parentStateNode = parentFiber.stateNode;
parentStateNode.effectDuration += effectDuration;
break;
}
parentFiber = parentFiber.return;
}
}
}
return;
}
case SuspenseComponent: {
commitSuspenseHydrationCallbacks(finishedRoot, finishedWork);
return;
}
case SuspenseListComponent:
case IncompleteClassComponent:
case FundamentalComponent:
case ScopeComponent:
case OffscreenComponent:
case LegacyHiddenComponent:
return;
}
invariant(
false,
"This unit of work tag should not have side-effects. This error is " +
"likely caused by a bug in React. Please file an issue."
);
}
7 Context
Context Api 相关。不深入了解 具体使用方法,请看context
// 索引
let index = -1;
// 值栈
const valueStack = [];
let fiberStack
if (__DEV__) {
fiberStack = [];
}
// 创建一种游标的数据结构
function createCursor(defaultValue) {
return {
current: defaultValue,
};
}
function push(cursor, value, fiber) {
index++;
// 保存当前值
valueStack[index] = cursor.current;
if (__DEV__) {
fiberStack[index] = fiber;
}
// 修改游标当前值
cursor.current = value;
}
function pop(cursor, fiber) {
if (index < 0) {
return;
}
// 恢复上一个保存的值
cursor.current = valueStack[index];
// 删除数据
valueStack[index] = null;
if (__DEV__) {
fiberStack[index] = null;
}
index--;
}
onst NO_CONTEXT = {};
const contextStackCursor = createCursor(NO_CONTEXT);
const contextFiberStackCursor = createCursor(NO_CONTEXT);
const rootInstanceStackCursor = createCursor(NO_CONTEXT);
const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
const MATH_NAMESPACE = "http://www.w3.org/1998/Math/MathML";
const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
function pushHostRootContext(workInProgress) {
function getIntrinsicNamespace(type) {
switch (type) {
case "svg":
return SVG_NAMESPACE;
case "math":
return MATH_NAMESPACE;
default:
return HTML_NAMESPACE;
}
}
function getChildNamespace(parentNamespace, type) {
if (parentNamespace == null || parentNamespace === HTML_NAMESPACE) {
// No (or default) parent namespace: potential entry point.
return getIntrinsicNamespace(type);
}
if (parentNamespace === SVG_NAMESPACE && type === "foreignObject") {
// We're leaving SVG.
return HTML_NAMESPACE;
}
// By default, pass namespace below.
return parentNamespace;
}
function getRootHostContext(rootContainerInstance) {
let type;
let namespace;
const nodeType = rootContainerInstance.nodeType;
switch (nodeType) {
case DOCUMENT_NODE:
case DOCUMENT_FRAGMENT_NODE: {
type = nodeType === DOCUMENT_NODE ? "#document" : "#fragment";
const root = rootContainerInstance.documentElement;
namespace = root ? root.namespaceURI : getChildNamespace(null, "");
break;
}
default: {
const container =
nodeType === COMMENT_NODE
? rootContainerInstance.parentNode
: rootContainerInstance;
const ownNamespace = container.namespaceURI || null;
type = container.tagName;
namespace = getChildNamespace(ownNamespace, type);
break;
}
}
return namespace;
}
function pushHostContainer(fiber, nextRootInstance) {
// 推入当前根节点实例到栈里面
// 这允许我们重置根节点当portals退出的时候
push(rootInstanceStackCursor, nextRootInstance, fiber);
// Track the context and the Fiber that provided it.
// This enables us to pop only Fibers that provide unique contexts.
push(contextFiberStackCursor, fiber, fiber);
// 最终我们需要推入host context到栈里面
// 然而我们不能只调用getRootHostContext方法并且推入它
// 因为我们有一个不一样的数字
// Finally, we need to push the host context to the stack.
// However, we can't just call getRootHostContext() and push it because
// we'd have a different number of entries on the stack depending on
// whether getRootHostContext() throws somewhere in renderer code or not.
// So we push an empty value first. This lets us safely unwind on errors.
push(contextStackCursor, NO_CONTEXT, fiber);
const nextRootContext = getRootHostContext(nextRootInstance);
// Now that we know this function doesn't throw, replace it.
pop(contextStackCursor, fiber);
push(contextStackCursor, nextRootContext, fiber);
}
const root = workInProgress.stateNode;
pushHostContainer(workInProgress, root.containerInfo);
}
8 后续需要了解的内容:
- React16 diff 功能源码剖析
- scheduler 是如何工作的,渲染的同步异步问题
- hooks 的实现原理