上一章讲解了React源码解析系列(六) -- Vdom到Rdom的解读,我们知道了对于一个基础标签React是怎么创建出来的,到目前为止我们生成的fiber树上面具有属性、状态、副作用标签等,那么应该怎么样去创建并执行一个完整的React应用呢?这一章就讲一讲整个react应用的commit阶段吧。针对于commit阶段的执行流程我也画了一个简略的图:
render阶段执行完毕
在render阶段的reconciler与scheduler执行过程中,如果当前的fiber树遍历完毕或者线程没有中断之后,那么react就会提交任务去commit了:
commit入口函数就是执行commitRoot,那么我们来看一下commitRoot的源码:
commitRoot
// packages/react-reconciler/src/ReactFiberWorkLoop.old.js
function commitRoot(root) {
const renderPriorityLevel = getCurrentPriorityLevel();
runWithPriority(
ImmediateSchedulerPriority,
//主要执行逻辑
commitRootImpl.bind(null, root, renderPriorityLevel),
);
return null;
}
commitRoot进一步调用commitRootImpl。
commitRootImpl
function commitRootImpl(root, renderPriorityLevel) {
...
root.finishedWork = null;
root.finishedLanes = NoLanes;
// Get the list of effects.
// 获取副作用列表
let firstEffect;
if (finishedWork.flags > PerformedWork) {
// 如果root上有副作用,则要把副作用添加到链表当中去
if (finishedWork.lastEffect !== null) {
finishedWork.lastEffect.nextEffect = finishedWork;
firstEffect = finishedWork.firstEffect;
} else {
firstEffect = finishedWork;
}
} else {
// There is no effect on the root.
// 如果没有就把root作为链表的开头
firstEffect = finishedWork.firstEffect;
}
...
nextEffect = firstEffect;
do {
if (__DEV__) {
...
} else {
try {
// 第一次遍历副作用链表
commitBeforeMutationEffects();
} catch (error) {
invariant(nextEffect !== null, 'Should be working on an effect.');
captureCommitPhaseError(nextEffect, error);
nextEffect = nextEffect.nextEffect;
}
}
} while (nextEffect !== null);
...
// The next phase is the mutation phase, where we mutate the host tree.
nextEffect = firstEffect;
do {
if (__DEV__) {
...
} else {
try {
// 第二次遍历副作用链表
commitMutationEffects(root, renderPriorityLevel);
} catch (error) {
invariant(nextEffect !== null, 'Should be working on an effect.');
captureCommitPhaseError(nextEffect, error);
nextEffect = nextEffect.nextEffect;
}
}
} while (nextEffect !== null);
if (shouldFireAfterActiveInstanceBlur) {
afterActiveInstanceBlur();
}
...
root.current = finishedWork;
...
nextEffect = firstEffect; // 把副作用链表的下一项变为头一项,重新遍历
do {
if (__DEV__) {
...
} else {
try {
// 第三次遍历副作用链表
commitLayoutEffects(root, lanes);
} catch (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();
...
} 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();
}
}
...
return null;
}
这个函数的代码确实有点长,但是主要做的事情只有三件:
- 获取
effectLists副作用链表,如果root上有副作用,则把副作用插到链表的尾部,如果没有则用副作用链表的头部为头部进行遍历。 - 进行三次遍历,分别执行
commitBeforeMutationEffects、commitMutationEffects、commitLayoutEffects。
第一次遍历执行commitBeforeMutationEffects
function commitBeforeMutationEffects() {
// 根据nextEffect进行遍历
while (nextEffect !== null) {
// 改变指针
const current = nextEffect.alternate;
...
// 获取当前的标记
const flags = nextEffect.flags;
if ((flags & Snapshot) !== NoFlags) {
setCurrentDebugFiberInDEV(nextEffect);
// 处理class组件的props和state,执行getSnapshotBeforeUpdate函数
commitBeforeMutationEffectOnFiber(current, nextEffect);
resetCurrentDebugFiberInDEV();
}
if ((flags & Passive) !== NoFlags) {
// 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;
}
}
commitBeforeMutationEffects函数中,根据nextEffect进行遍历,如果有副作用,便会去执行commitBeforeMutationEffectOnFiber函数,处理class组件上的props,state并且会执行getSnapshotBeforeUpdate,如果对getSnapshotBeforeUpdate不清楚,请戳 >> 组件的生命周期 ,
commitBeforeMutationLifeCycles
// import commitBeforeMutationLifeCycles as commitBeforeMutationEffectOnFiber
function commitBeforeMutationLifeCycles(
current: Fiber | null,
finishedWork: Fiber,
): void {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block: {
return;
}
case ClassComponent: {
if (finishedWork.flags & Snapshot) {
if (current !== null) {
// 上一次的props、state
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__) {
if (
finishedWork.type === finishedWork.elementType &&
!didWarnAboutReassigningProps
) {
if (instance.props !== finishedWork.memoizedProps) {
console.error(
'Expected %s props to match memoized props before ' +
'getSnapshotBeforeUpdate. ' +
'This might either be because of a bug in React, or because ' +
'a component reassigns its own `this.props`. ' +
'Please file an issue.',
getComponentName(finishedWork.type) || 'instance',
);
}
if (instance.state !== finishedWork.memoizedState) {
console.error(
'Expected %s state to match memoized state before ' +
'getSnapshotBeforeUpdate. ' +
'This might either be because of a bug in React, or because ' +
'a component reassigns its own `this.state`. ' +
'Please file an issue.',
getComponentName(finishedWork.type) || 'instance',
);
}
}
}
// 执行钩子
const snapshot = instance.getSnapshotBeforeUpdate(
finishedWork.elementType === finishedWork.type
? prevProps
: resolveDefaultProps(finishedWork.type, prevProps),
prevState,
);
if (__DEV__) {
const didWarnSet = ((didWarnAboutUndefinedSnapshotBeforeUpdate: any): Set<mixed>);
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.flags & 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.',
);
}
第二次遍历执行commitMutationEffects
function commitMutationEffects(
root: FiberRoot,
renderPriorityLevel: ReactPriorityLevel,
) {
// TODO: Should probably move the bulk of this function to commitWork.
while (nextEffect !== null) {
setCurrentDebugFiberInDEV(nextEffect);
const flags = nextEffect.flags;
// if flags == 'ContentReset 重置文本
if (flags & ContentReset) {
// 如果文本发生改变执行commitResetTextContent,更新文本
commitResetTextContent(nextEffect);
}
// flags == 'Ref' ,更新ref current
if (flags & Ref) {
const current = nextEffect.alternate;
if (current !== null) {
//更新ref的值
commitDetachRef(current);
}
if (enableScopeAPI) {
// TODO: This is a temporary solution that allowed us to transition away
// from React Flare on www.
if (nextEffect.tag === ScopeComponent) {
commitAttachRef(nextEffect);
}
}
}
// 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 primaryFlags = flags & (Placement | Update | Deletion | Hydrating);
switch (primaryFlags) {
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.flags &= ~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.flags &= ~Placement;
// Update
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
// 混合类型
case Hydrating: {
nextEffect.flags &= ~Hydrating;
break;
}
case HydratingAndUpdate: {
nextEffect.flags &= ~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;
}
}
resetCurrentDebugFiberInDEV();
nextEffect = nextEffect.nextEffect;
}
}
commitMutationEffects 函数会第二次遍历effectLists,根据flags的类型与定义类型进行位运算。
-
其中
ContentReset、Ref、ScopeComponent为特殊类型,如果当前副作用flags匹配上。则回去执行对应的函数。ContentReset:执行commitResetTextContent函数,进行文本内容重置更新。Ref:执行commitDetachRef函数,获取更新最新的ref值。ScopeComponent:commitAttachRef
-
PlaceMent、Deletion、Update、Hydrating等为不同的处理类型。PlaceMent:commitPlacement插入dom操作。Update:commitWork更新dom操作。Deletion:commitDeletion删除dom操作。
commitPlacement
function commitPlacement(finishedWork: Fiber): void {
if (!supportsMutation) {
return;
}
// Recursively insert all host nodes into the parent.
// 获取父级fiber
const parentFiber = getHostParentFiber(finishedWork);
// Note: these two variables *must* always be updated together.
let parent;
let isContainer;
const parentStateNode = parentFiber.stateNode;
switch (parentFiber.tag) {
case HostComponent:
parent = parentStateNode;
isContainer = false;
break;
case HostRoot:
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.flags & ContentReset) {
// Reset the text content of the parent before doing any insertions
// 如果父级的内容是文本,清空文本
resetTextContent(parent);
// Clear ContentReset from the effect tag
parentFiber.flags &= ~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);
}
}
insertOrAppendPlacementNodeIntoContainer上一章讲到创建真实dom之后的dom插入,就是在这个函数里面去进行的。
insertOrAppendPlacementNodeIntoContainer
function insertOrAppendPlacementNodeIntoContainer(
node: Fiber,
before: ?Instance,
parent: Container,
): void {
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 {
// 追加
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) {
// 子元素非null,继续递归
insertOrAppendPlacementNodeIntoContainer(child, before, parent);
let sibling = child.sibling;
while (sibling !== null) {
// 兄弟不为null,继续递归
insertOrAppendPlacementNodeIntoContainer(sibling, before, parent);
sibling = sibling.sibling;
}
}
}
}
insertInContainerBefore
当before不为null的时候,则需要在某节点前面插入dom节点。
// packages/react-dom/src/client/ReactDOMHostConfig.js
export function insertInContainerBefore(
container: Container,
child: Instance | TextInstance,
beforeChild: Instance | TextInstance | SuspenseInstance,
): void {
// COMMENT_NODE注释类型
if (container.nodeType === COMMENT_NODE) {
// 如果当前节点的父节点为注释类型,就在父级的父级插入
(container.parentNode: any).insertBefore(child, beforeChild);
} else {
// 在当前父级插入新的 dom
container.insertBefore(child, beforeChild);
}
}
appendChildToContainer
当before为null的时候,则需要在某节点后面追加dom节点。
export function appendChildToContainer(
container: Container,
child: Instance | TextInstance,
): void {
let parentNode;
// COMMENT_NODE注释类型
if (container.nodeType === COMMENT_NODE) {
// 如果当前节点的父节点为注释类型,就在父级的父级插入
parentNode = (container.parentNode: any);
parentNode.insertBefore(child, container);
} else {
// 在当前父级插入新的 dom
parentNode = container;
parentNode.appendChild(child);
}
// ...
}
insertOrAppendPlacementNode
function insertOrAppendPlacementNode(
node: Fiber,
before: ?Instance,
parent: Instance,
): void {
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;
}
}
}
}
insertBefore
export function insertBefore(
parentInstance: Instance,
child: Instance | TextInstance,
beforeChild: Instance | TextInstance | SuspenseInstance,
): void {
// 这里没有需要判断父节点为注释
parentInstance.insertBefore(child, beforeChild);
}
那么commitPlaceMent的执行流程如下:
commitWork
commitWork的作用就是更新dom节点,那么我们一起来看看他的源码:
function commitWork(current: Fiber | null, finishedWork: Fiber): void {
...
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent:
case Block: {
...
}
case ClassComponent: {
return;
}
case HostComponent: {
// 获取真实dom实例
const instance: Instance = finishedWork.stateNode;
if (instance != null) {
// Commit the work prepared earlier.
// 提前准备新的props
const newProps = finishedWork.memoizedProps;
// For hydration we reuse the update path but we treat the oldProps
// as the newProps. The updatePayload will contain the real change in
// this case.
// 获取老的props
const oldProps = current !== null ? current.memoizedProps : newProps;
const type = finishedWork.type;
// TODO: Type the updateQueue to be specific to host components.
// 获取更新队列
const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);
finishedWork.updateQueue = null;
if (updatePayload !== null) {
// 提交更新
commitUpdate(
instance,
updatePayload,
type,
oldProps,
newProps,
finishedWork,
);
}
}
return;
}
case HostText: {
invariant(
finishedWork.stateNode !== null,
'This should have a text node initialized. This error is likely ' +
'caused by a bug in React. Please file an issue.',
);
// 获取文本节点dom实例
const textInstance: TextInstance = finishedWork.stateNode;
// 新的内容
const newText: string = finishedWork.memoizedProps;
// For hydration we reuse the update path but we treat the oldProps
// as the newProps. The updatePayload will contain the real change in
// this case.
// 老的内容
const oldText: string =
current !== null ? current.memoizedProps : newText;
// 内容更新
commitTextUpdate(textInstance, oldText, newText);
return;
}
case HostRoot: {
...
}
}
由此可见commitWork中,也是根据不同的组件类型去做不同的处理的。但是主要的还是处理HostComponent和HostText。
- 处理
HostComponent的时候,一开始就会获取当前dom的实例,新的老的props,之后便是调用commitUpdate去提交更新。 HostText中,也是获取节点实例与节点的新老内容,之后调用commitTextUpdate去提交更新。
commitUpdate
export function commitUpdate(
domElement: Instance,
updatePayload: Array<mixed>,
type: string,
oldProps: Props,
newProps: Props,
internalInstanceHandle: Object,
): void {
// Update the props handle so that we know which props are the ones with
// with current event handlers.
// 绑定对应的props
updateFiberProps(domElement, newProps);
// Apply the diff to the DOM node.
//应用到dom上
updateProperties(domElement, updatePayload, type, oldProps, newProps);
}
commitUpdate中通过updateFiberProps给当前的fiber绑定上已知的props,再通过updateProperties应用到dom上。
updateFiberProps
export function updateFiberProps(
node: Instance | TextInstance | SuspenseInstance,
props: Props,
): void {
// 一对一绑定props
(node: any)[internalPropsKey] = props;
}
updateProperties
// packages/react-dom/src/client/ReactDOMComponent.js
export function updateProperties(
domElement: Element,
updatePayload: Array<any>,
tag: string,
lastRawProps: Object,
nextRawProps: Object,
): void {
// Update checked *before* name.
// In the middle of an update, it is possible to have multiple checked.
// When a checked radio tries to change name, browser makes another radio's checked false.
// 表单特殊处理
if (
tag === 'input' &&
nextRawProps.type === 'radio' &&
nextRawProps.name != null
) {
ReactDOMInputUpdateChecked(domElement, nextRawProps);
}
// 用户自定义组件
const wasCustomComponentTag = isCustomComponent(tag, lastRawProps);
const isCustomComponentTag = isCustomComponent(tag, nextRawProps);
// Apply the diff.
// 应用diff后的结果到dom上
updateDOMProperties(
domElement,
updatePayload,
wasCustomComponentTag,
isCustomComponentTag,
);
// TODO: Ensure that an update gets scheduled if any of the special props
// changed.
//处理表单特殊情况
switch (tag) {
case 'input':
// Update the wrapper around inputs *after* updating props. This has to
// happen after `updateDOMProperties`. Otherwise HTML5 input validations
// raise warnings and prevent the new value from being assigned.
ReactDOMInputUpdateWrapper(domElement, nextRawProps);
break;
case 'textarea':
ReactDOMTextareaUpdateWrapper(domElement, nextRawProps);
break;
case 'select':
// <select> value update needs to occur after <option> children
// reconciliation
ReactDOMSelectPostUpdateWrapper(domElement, nextRawProps);
break;
}
}
updateProperties会根据fiber的tag类型,去对表单类的组件做特殊处理,并通过updateDOMProperties函数去将diff的结果,应用到真实dom上。
updateDOMProperties
function updateDOMProperties(
domElement: Element,
updatePayload: Array<any>,
wasCustomComponentTag: boolean,
isCustomComponentTag: boolean,
): void {
// TODO: Handle wasCustomComponentTag
for (let i = 0; i < updatePayload.length; i += 2) {
const propKey = updatePayload[i];
const propValue = updatePayload[i + 1];
// 处理style
if (propKey === STYLE) {
setValueForStyles(domElement, propValue);
// 处理内容
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
setInnerHTML(domElement, propValue);
} else if (propKey === CHILDREN) {
//处理文本
setTextContent(domElement, propValue);
} else {
// 其他处理
setValueForProperty(domElement, propKey, propValue, isCustomComponentTag);
}
}
}
updateDOMProperties函数中会去遍历updatePayload执行,直接映射到真实dom节点属性上。并且处理key为STYLE、DANGEROUSLY_SET_INNER_HTML、CHILDREN的情况,从而完善了dom节点的更新。
commitTextUpdate
export function commitTextUpdate(
textInstance: TextInstance,
oldText: string,
newText: string,
): void {
//处理文本内容直接赋值
textInstance.nodeValue = newText;
}
所以commitWork大致处理流程如下:
commitDeletion
function commitDeletion(
finishedRoot: FiberRoot,
current: Fiber,
renderPriorityLevel: ReactPriorityLevel,
): void {
if (supportsMutation) {
// Recursively delete all host nodes from the parent.
// Detach refs and call componentWillUnmount() on the whole subtree.
unmountHostComponents(finishedRoot, current, renderPriorityLevel);
} else {
// Detach refs and call componentWillUnmount() on the whole subtree.
commitNestedUnmounts(finishedRoot, current, renderPriorityLevel);
}
// Reset fiber
const alternate = current.alternate;
detachFiberMutation(current);
if (alternate !== null) 1{
detachFiberMutation(alternate);
}
}
commitDeletion为删除副作用节点的入口函数,在实际的React项目中,组件的组件的删除包括组件的节点挂在时候的删除、生命周期的卸载、执行上下文的销毁。在这里我们还是一起来看看unmountHostComponents和commitNestedUnmounts究竟做了什么事情。
unmountHostComponents
// packages/react-reconciler/src/ReactFiberCommitWork.old.js
function unmountHostComponents(
finishedRoot: FiberRoot,
current: Fiber,
renderPriorityLevel: ReactPriorityLevel,
): void {
let node: Fiber = current;
let currentParentIsValid = false;
let currentParent;
let currentParentIsContainer;
while (true) {
if (!currentParentIsValid) {
// 父节点非法,找父节点的上一个父级
let parent = node.return;
// 深度优先遍历
findParent: while (true) {
invariant(
parent !== null,
'Expected to find a host parent. This error is likely caused by ' +
'a bug in React. Please file an issue.',
);
const parentStateNode = parent.stateNode;
switch (parent.tag) {
case HostComponent:
currentParent = parentStateNode;
currentParentIsContainer = false;
break findParent;
case HostRoot:
currentParent = parentStateNode.containerInfo;
currentParentIsContainer = true;
break findParent;
case HostPortal:
currentParent = parentStateNode.containerInfo;
currentParentIsContainer = true;
break findParent;
case FundamentalComponent:
if (enableFundamentalAPI) {
currentParent = parentStateNode.instance;
currentParentIsContainer = false;
}
}
parent = parent.return;
}
currentParentIsValid = true;
}
// 原生组件处理
if (node.tag === HostComponent || node.tag === HostText) {
commitNestedUnmounts(finishedRoot, node, renderPriorityLevel);
// 当前父级是容器
if (currentParentIsContainer) {
// 把child从container中移除
removeChildFromContainer(
((currentParent: any): Container),
(node.stateNode: Instance | TextInstance),
);
} else {
// 是节点,把child从父节点中移除
removeChild(
((currentParent: any): Instance),
(node.stateNode: Instance | TextInstance),
);
}
// Don't visit children because we already visited them.
} else if (enableFundamentalAPI && node.tag === FundamentalComponent) {
const fundamentalNode = node.stateNode.instance;
commitNestedUnmounts(finishedRoot, node, renderPriorityLevel);
// After all the children have unmounted, it is now safe to remove the
// node from the tree.
if (currentParentIsContainer) {
removeChildFromContainer(
((currentParent: any): Container),
(fundamentalNode: Instance),
);
} else {
removeChild(
((currentParent: any): Instance),
(fundamentalNode: Instance),
);
}
} else if (
enableSuspenseServerRenderer &&
node.tag === DehydratedFragment
) {
if (enableSuspenseCallback) {
const hydrationCallbacks = finishedRoot.hydrationCallbacks;
if (hydrationCallbacks !== null) {
const onDeleted = hydrationCallbacks.onDeleted;
if (onDeleted) {
onDeleted((node.stateNode: SuspenseInstance));
}
}
}
// Delete the dehydrated suspense boundary and all of its content.
if (currentParentIsContainer) {
clearSuspenseBoundaryFromContainer(
((currentParent: any): Container),
(node.stateNode: SuspenseInstance),
);
} else {
clearSuspenseBoundary(
((currentParent: any): Instance),
(node.stateNode: SuspenseInstance),
);
}
} else if (node.tag === HostPortal) {
if (node.child !== null) {
// When we go into a portal, it becomes the parent to remove from.
// We will reassign it back when we pop the portal on the way up.
currentParent = node.stateNode.containerInfo;
currentParentIsContainer = true;
// Visit children because portals might contain host components.
node.child.return = node;
node = node.child;
continue;
}
} else {
commitUnmount(finishedRoot, node, renderPriorityLevel);
// Visit children because we may find more host components below.
// 深度优先遍历子节点
if (node.child !== null) {
node.child.return = node;
node = node.child;
continue;
}
}
// 当前fiber树遍历完毕
if (node === current) {
return;
}
// 兄弟节点遍历完毕,当前子树遍历完毕
while (node.sibling === null) {
if (node.return === null || node.return === current) {
return;
}
node = node.return;
if (node.tag === HostPortal) {
// When we go out of the portal, we need to restore the parent.
// Since we don't keep a stack of them, we will search for it.
currentParentIsValid = false;
}
}
// 继续遍历兄弟节点
node.sibling.return = node.return;
node = node.sibling;
}
}
commitNestedUnmounts
function commitNestedUnmounts(
finishedRoot: FiberRoot,
root: Fiber,
renderPriorityLevel: ReactPriorityLevel,
): void {
let node: Fiber = root;
while (true) {
// 深度优先,执行commitUnmount
commitUnmount(finishedRoot, node, renderPriorityLevel);
...
if (
node.child !== null &&
// If we use mutation we drill down into portals using commitUnmount above.
// If we don't use mutation we drill down into portals here instead.
(!supportsMutation || node.tag !== HostPortal)
) {
// 深度优先遍历子节点
node.child.return = node;
node = node.child;
continue;
}
// 当前fiber遍历完毕
if (node === root) {
return;
}
// 兄弟节点没有,当前子树遍历完毕
while (node.sibling === null) {
if (node.return === null || node.return === root) {
return;
}
node = node.return;
}
// 继续遍历当前子树的兄弟
node.sibling.return = node.return;
node = node.sibling;
}
}
这两个函数的区别就在于没有判断合法父级,但是之后都会去执行commitUnmount函数。
commitUnmount
function commitUnmount(
finishedRoot: FiberRoot,
current: Fiber,
renderPriorityLevel: ReactPriorityLevel,
): void {
onCommitUnmount(current);
switch (current.tag) {
...
case ClassComponent: {
// ref = null
safelyDetachRef(current);
const instance = current.stateNode;
if (typeof instance.componentWillUnmount === 'function') {
// componentWillUnMount()
safelyCallComponentWillUnmount(current, instance);
}
return;
}
case HostComponent: {
// ref =null
safelyDetachRef(current);
return;
}
case HostPortal: {
// TODO: this is recursive.
// We are also not using this parent because
// the portal will get pushed immediately.
if (supportsMutation) {
// 遍历子树
unmountHostComponents(finishedRoot, current, renderPriorityLevel);
} else if (supportsPersistence) {
emptyPortalContainer(current);
}
return;
}
...
}
}
commitUmount函数会对组件的ref进行卸载,类组件则会执行componentWillUnMount钩子函数。他的执行流程大致如下:
那么经理上述过程就完成了dom的一系列操作。
第三次遍历执行commitLayoutEffects
第三次遍历effectList,则会去执行一系列生命周期函数,如componentDidMount、componentDidUpdate、对于函数组件来讲此刻会执行useLayoutEffect(create, deps)的create,并且会调度useEffect的create、destroy(这个在hooks里面再去讲)。让我们来一起看看源码
function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) {
...
// TODO: Should probably move the bulk of this function to commitWork.
// 第三次遍历effect
while (nextEffect !== null) {
setCurrentDebugFiberInDEV(nextEffect);
const flags = nextEffect.flags;
if (flags & (Update | Callback)) {
const current = nextEffect.alternate;
// 执行生命周期钩子
commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes);
}
if (enableScopeAPI) {
// TODO: This is a temporary solution that allowed us to transition away
// from React Flare on www.
// 更新ref
if (flags & Ref && nextEffect.tag !== ScopeComponent) {
commitAttachRef(nextEffect);
}
} else {
if (flags & Ref) {
commitAttachRef(nextEffect);
}
}
resetCurrentDebugFiberInDEV();
nextEffect = nextEffect.nextEffect;
}
...
}
commitLayoutEffectOnFiber函数里面实例调用了componentDidMount或者componentDidUpdate,这是根据,react应用执行是挂载还是更新决定的。执行文件函数为commitLifeCycles
commitLayoutEffectOnFiber
function commitLifeCycles(
finishedRoot: FiberRoot,
current: Fiber | null,
finishedWork: Fiber,
committedLanes: Lanes,
): void {
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);
}
schedulePassiveEffects(finishedWork);
return;
}
case ClassComponent: {
const instance = finishedWork.stateNode;
if (finishedWork.flags & Update) {
if (current === null) {
// 如果 current是null的话,那么是挂载,首次渲染
// 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 (
finishedWork.type === finishedWork.elementType &&
!didWarnAboutReassigningProps
) {
if (instance.props !== finishedWork.memoizedProps) {
console.error(
'Expected %s props to match memoized props before ' +
'componentDidMount. ' +
'This might either be because of a bug in React, or because ' +
'a component reassigns its own `this.props`. ' +
'Please file an issue.',
getComponentName(finishedWork.type) || 'instance',
);
}
if (instance.state !== finishedWork.memoizedState) {
console.error(
'Expected %s state to match memoized state before ' +
'componentDidMount. ' +
'This might either be because of a bug in React, or because ' +
'a component reassigns its own `this.state`. ' +
'Please file an issue.',
getComponentName(finishedWork.type) || 'instance',
);
}
}
}
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
finishedWork.mode & ProfileMode
) {
try {
startLayoutEffectTimer();
instance.componentDidMount();
} finally {
recordLayoutEffectDuration(finishedWork);
}
} else {
instance.componentDidMount();
}
} else {
// 如果不是则是更新,根据current来区分mount还是update
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 (
finishedWork.type === finishedWork.elementType &&
!didWarnAboutReassigningProps
) {
if (instance.props !== finishedWork.memoizedProps) {
console.error(
'Expected %s props to match memoized props before ' +
'componentDidUpdate. ' +
'This might either be because of a bug in React, or because ' +
'a component reassigns its own `this.props`. ' +
'Please file an issue.',
getComponentName(finishedWork.type) || 'instance',
);
}
if (instance.state !== finishedWork.memoizedState) {
console.error(
'Expected %s state to match memoized state before ' +
'componentDidUpdate. ' +
'This might either be because of a bug in React, or because ' +
'a component reassigns its own `this.state`. ' +
'Please file an issue.',
getComponentName(finishedWork.type) || 'instance',
);
}
}
}
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
finishedWork.mode & ProfileMode
) {
try {
startLayoutEffectTimer();
instance.componentDidUpdate(
prevProps,
prevState,
instance.__reactInternalSnapshotBeforeUpdate,
);
} finally {
recordLayoutEffectDuration(finishedWork);
}
} else {
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: UpdateQueue<
*,
> | null = (finishedWork.updateQueue: any);
if (updateQueue !== null) {
if (__DEV__) {
if (
finishedWork.type === finishedWork.elementType &&
!didWarnAboutReassigningProps
) {
if (instance.props !== finishedWork.memoizedProps) {
console.error(
'Expected %s props to match memoized props before ' +
'processing the update queue. ' +
'This might either be because of a bug in React, or because ' +
'a component reassigns its own `this.props`. ' +
'Please file an issue.',
getComponentName(finishedWork.type) || 'instance',
);
}
if (instance.state !== finishedWork.memoizedState) {
console.error(
'Expected %s state to match memoized state before ' +
'processing the update queue. ' +
'This might either be because of a bug in React, or because ' +
'a component reassigns its own `this.state`. ' +
'Please file an issue.',
getComponentName(finishedWork.type) || 'instance',
);
}
}
}
// 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: UpdateQueue<
*,
> | null = (finishedWork.updateQueue: any);
if (updateQueue !== null) {
let instance = null;
if (finishedWork.child !== null) {
switch (finishedWork.child.tag) {
case HostComponent:
instance = getPublicInstance(finishedWork.child.stateNode);
break;
case ClassComponent:
instance = finishedWork.child.stateNode;
break;
}
}
commitUpdateQueue(finishedWork, updateQueue, instance);
}
return;
}
case HostComponent: {
const instance: 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.flags & 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.',
);
}
我们看到了在ClassComponent还去执行了commitUpdateQueue,在HostComponent中执行了commitMount,这是因为前面我们在处理副作用的时候,只是根据副作用进行了处理,但是并没有针对处理完后的节点进行副作用清除,所以commitUpdateQueue完成了这一点。
commitUpdateQueue
export function commitUpdateQueue<State>(
finishedWork: Fiber,
finishedQueue: UpdateQueue<State>,
instance: any,
): void {
// Commit the effects
const effects = finishedQueue.effects;
finishedQueue.effects = null;
if (effects !== null) {
for (let i = 0; i < effects.length; i++) {
const effect = effects[i];
const callback = effect.callback;
if (callback !== null) {
effect.callback = null;
callCallback(callback, instance);
}
}
}
}
此函数会对finishedQueue上的effect进行遍历,如果副作用里面有callback,执行它并把当前的effect置为null。
commitMount
而在基本标准标签挂载的时候,我们会去特殊处理一些自闭合的标签为单标签。
export function commitMount(
domElement: Instance,
type: string,
newProps: Props,
internalInstanceHandle: Object,
): void {
...
// 特殊处理闭合标签为单标签
if (shouldAutoFocusHostComponent(type, newProps)) {
((domElement: any):
| HTMLButtonElement
| HTMLInputElement
| HTMLSelectElement
| HTMLTextAreaElement).focus();
}
}
那么这就是React Fiber的整个commit流程了。
总结
本文主要是介绍了commit的主要流程,整个流程遍历了三次effectLists,对fiber树的组件进行了分类处理。写到这里对react的大致执行机制有了一定的了解,接下来就是做一些修修补补的工作。
目录:React源码解析系列(零) -- 全局概况