React源碼解析系列文章歡迎閱讀:
React16源碼解析(一)- 圖解Fiber架構
React16源碼解析(二)-創建更新
React16源碼解析(三)-ExpirationTime
React16源碼解析(四)-Scheduler
React16源碼解析(五)-更新流程渲染階段1
React16源碼解析(六)-更新流程渲染階段2
React16源碼解析(七)-更新流程渲染階段3
React16源碼解析(八)-更新流程提交階段
正在更新中...
提交階段相比於渲染階段要簡單很多,因爲大部分更新的前期操作都在渲染階段做好了。提交階段的主要任務也就是把之前記錄好的更新操作反映到真實的dom上,並且這個過程是不能中斷的。
commitRoot
1、檢查 finishedWork 是否也有 effect ,如有插入 effect 鏈表中
2、第一次遍歷effect鏈,更新class組件實例上的state,props,執行getSnapshotBeforeUpdate生命週期
3、第二次遍歷effect鏈,不同的effectTag,執行不同的操作,比如重置文本節點,執行 插入、更新、刪除等的 effect 操作,真正的對 dom 進行修改。
4、第三次遍歷effect鏈,這次遍歷就是做一些收尾工作。執行componentDidMount、componentDidUpdate,更新的回調函數等。
5、做一些 還原變量 等的收尾工作。
下面會着重講解effect鏈表的三次遍歷。
function commitRoot(root: FiberRoot, finishedWork: Fiber): void {
isWorking = true;
isCommitting = true;
// ......
// 檢查 finishedWork 是否也有 effect ,如有插入 effect 鏈表中
let firstEffect;
if (finishedWork.effectTag > PerformedWork) {
// A fiber's effect list consists only of its children, not itself. So if
// the root has an effect, we need to add it to the end of the list. The
// resulting list is the set that would belong to the root's parent, if
// it had one; that is, all the effects in the tree including the root.
if (finishedWork.lastEffect !== null) {
finishedWork.lastEffect.nextEffect = finishedWork;
firstEffect = finishedWork.firstEffect;
} else {
firstEffect = finishedWork;
}
} else {
// There is no effect on the root.
firstEffect = finishedWork.firstEffect;
}
prepareForCommit(root.containerInfo);
// Invoke instances of getSnapshotBeforeUpdate before mutation.
nextEffect = firstEffect;
startCommitSnapshotEffectsTimer();
// 第一次遍歷
while (nextEffect !== null) {
let didError = false;
let error;
commitBeforeMutationLifecycles();
// ......
}
// ......
// Commit all the side-effects within a tree. We'll do this in two passes.
// The first pass performs all the host insertions, updates, deletions and
// ref unmounts.
nextEffect = firstEffect;
startCommitHostEffectsTimer();
// 第二次遍歷
while (nextEffect !== null) {
let didError = false;
let error;
// ......
commitAllHostEffects();
// ......
}
stopCommitHostEffectsTimer();
resetAfterCommit(root.containerInfo);
// The work-in-progress tree is now the current tree. This must come after
// the first pass of the commit phase, so that the previous tree is still
// current during componentWillUnmount, but before the second pass, so that
// the finished work is current during componentDidMount/Update.
root.current = finishedWork;
// In the second pass we'll perform all life-cycles and ref callbacks.
// Life-cycles happen as a separate pass so that all placements, updates,
// and deletions in the entire tree have already been invoked.
// This pass also triggers any renderer-specific initial effects.
nextEffect = firstEffect;
startCommitLifeCyclesTimer();
// 第三次遍歷
while (nextEffect !== null) {
commitAllLifeCycles(root, committedExpirationTime);
}
// 下面做一些 還原變量 等的收尾工作
isCommitting = false;
isWorking = false;
stopCommitLifeCyclesTimer();
stopCommitTimer();
onCommitRoot(finishedWork.stateNode);
const updateExpirationTimeAfterCommit = finishedWork.expirationTime;
const childExpirationTimeAfterCommit = finishedWork.childExpirationTime;
const earliestRemainingTimeAfterCommit =
updateExpirationTimeAfterCommit === NoWork ||
(childExpirationTimeAfterCommit !== NoWork &&
childExpirationTimeAfterCommit < updateExpirationTimeAfterCommit)
? childExpirationTimeAfterCommit
: updateExpirationTimeAfterCommit;
if (earliestRemainingTimeAfterCommit === NoWork) {
// If there's no remaining work, we can clear the set of already failed
// error boundaries.
legacyErrorBoundariesThatAlreadyFailed = null;
}
onCommit(root, earliestRemainingTimeAfterCommit);
// ......
}
第一次遍歷-commitBeforeMutationLifecycles
遍歷effect鏈
在這個循環中,組件的state已經更新,但是節點還沒有更新。
function commitBeforeMutationLifecycles() {
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
if (effectTag & Snapshot) {
recordEffect();
const current = nextEffect.alternate;
// 這裏調用的是下面的方法,名字一樣
commitBeforeMutationLifeCycles(current, nextEffect);
}
// Don't cleanup effects yet;
// This will be done by commitAllLifeCycles()
nextEffect = nextEffect.nextEffect;
}
}
以下方法做的主要事情:
1、在 class 組件中通過 prevProps, prevState 獲取狀態快照,用於 componentDidUpdate 生命週期
2、狀態快照的獲取通過 getSnapshotBeforeUpdate 生命週期執行後的返回值
commitBeforeMutationLifeCycles 中只有在更新任務是 classComponent 時纔有工作
function commitBeforeMutationLifeCycles(
current: Fiber | null,
finishedWork: Fiber,
): void {
switch (finishedWork.tag) {
case ClassComponent: {
if (finishedWork.effectTag & Snapshot) {
if (current !== null) {
// 不是初次加載
// 組件初次加載執行 DidMount 生命週期函數不走 DidUpdate 不需要保存快照對象
const prevProps = current.memoizedProps;
const prevState = current.memoizedState;
startPhaseTimer(finishedWork, 'getSnapshotBeforeUpdate');
// 得到當前class組件的實例
const instance = finishedWork.stateNode;
// 更新實例上的props和state
instance.props = finishedWork.memoizedProps;
instance.state = finishedWork.memoizedState;
// 調用getSnapshotBeforeUpdate生命週期方法
const snapshot = instance.getSnapshotBeforeUpdate(
prevProps,
prevState,
);
// 保存到instance.__reactInternalSnapshotBeforeUpdate 給 DidUpdate生命週期方法使用
instance.__reactInternalSnapshotBeforeUpdate = snapshot;
stopPhaseTimer();
}
}
return;
}
case HostRoot:
case HostComponent:
case HostText:
case HostPortal:
case IncompleteClassComponent:
// Nothing to do for these component types
return;
default: {
// ......
}
}
}
第二次遍歷-commitAllHostEffects
1、遍歷effect鏈
2、不同的effectTag,執行不同的操作,比如重置文本節點,執行 插入、更新、刪除等的 effect 操作,真正的對 dom 進行修改。
下面我開始講解三種節點的操作:
1、插入節點 - commitPlacement
2、更新節點 - commitWork
3、刪除節點 - commitDeletion
function commitAllHostEffects() {
// 遍歷effect鏈
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
// 重置文本節點
if (effectTag & ContentReset) {
commitResetTextContent(nextEffect);
}
if (effectTag & Ref) {
const current = nextEffect.alternate;
if (current !== 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.
let primaryEffectTag = effectTag & (Placement | Update | Deletion);
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.
// 刪除effectTag
nextEffect.effectTag &= ~Placement;
break;
}
case PlacementAndUpdate: {
// 插入並且更新
// 插入節點
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;
// 更新節點
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Update: {
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Deletion: {
// 刪除節點
commitDeletion(nextEffect);
break;
}
}
nextEffect = nextEffect.nextEffect;
}
}
commitPlacement
1、找到 finishedWork 的父節點 parentFiber。尋找的是原生的dom節點對應的fiber。如果父級不是原生dom,則繼續往上尋找。
2、得到parentFiber對應的原生dom節點parent
3、找到插入節點的後一個節點,因爲我要插在它前面
4、用insertBefore或者appendChild進行子節點插入操作
注:第4步插入操作需要分情況,比如如果是原生dom節點是直接插入,如果是Class子節點是需要深度優先遍歷子節點進行插入的。
function commitPlacement(finishedWork: Fiber): void {
if (!supportsMutation) {
return;
}
// Recursively insert all host nodes into the parent.
// 找到 finishedWork 的父節點 parentFiber。尋找的是原生的dom節點對應的fiber。如果父級不是原生dom,則繼續往上尋找
// 所以parentFiber只有三種類型的節點:HostComponent、HostRoot、HostPortal
const parentFiber = getHostParentFiber(finishedWork);
// Note: these two variables *must* always be updated together.
let parent;
let isContainer;
// 得到原生dom節點 : parent
switch (parentFiber.tag) {
case HostComponent:
parent = parentFiber.stateNode;
isContainer = false;
break;
case HostRoot:
parent = parentFiber.stateNode.containerInfo;
isContainer = true;
break;
case HostPortal:
parent = parentFiber.stateNode.containerInfo;
isContainer = true;
break;
default:
// ......
}
if (parentFiber.effectTag & ContentReset) {
// Reset the text content of the parent before doing any insertions
resetTextContent(parent);
// Clear ContentReset from the effect tag
parentFiber.effectTag &= ~ContentReset;
}
// 這裏操作 dom 使用的是 insertBefore 原生api
// 所以需要得到插入節點的後一個節點,因爲我要插在它前面
const before = getHostSibling(finishedWork);
// We only have the top Fiber that was inserted but we need recurse down its
// children to find all the terminal nodes.
let node: Fiber = finishedWork;
while (true) {
// 如果當前節點是原始dom節點就直接進行插入
if (node.tag === HostComponent || node.tag === HostText) {
if (before) {
// 有before就用insertBefore
if (isContainer) {
insertInContainerBefore(parent, node.stateNode, before);
} else {
insertBefore(parent, node.stateNode, before);
}
} else {
// 沒有before就用appendChild
if (isContainer) {
appendChildToContainer(parent, node.stateNode);
} else {
appendChild(parent, node.stateNode);
}
}
} else if (node.tag === HostPortal) {
// 是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 if (node.child !== null) {
// 上面條件不滿足其實就是class組件
// 查看子節點是否存在,存在的話把子節點進行插入 continue
node.child.return = node;
node = node.child;
continue;
}
if (node === finishedWork) {
// 對finishedWork已經結束
return;
}
// 其實這是一個深度優先遍歷
// 深度優先遍歷class組件的子節點,把class組件的原生dom節點全部進行插入操作
// 下面是深度優先遍歷的退回過程,如果沒有兄弟節點,就往上退回
while (node.sibling === null) {
if (node.return === null || node.return === finishedWork) {
return;
}
node = node.return;
}
node.sibling.return = node.return;
node = node.sibling;
}
}
getHostSibling:
這個函數其實是找我要插入的節點(finishedWork)的下一個dom節點,因爲我要插在這個節點前面嘛。所以這個函數做的事情就是:剔除掉所有的非原始dom節點,找到我想要的dom節點。
注:HostPortal也是要被剔除的,因爲它不是掛載在這個地方的嘛。
function getHostSibling(fiber: Fiber): ?Instance {
// We're going to search forward into the tree until we find a sibling host
// node. Unfortunately, if multiple insertions are done in a row we have to
// search past them. This leads to exponential search for the next sibling.
// TODO: Find a more efficient way to do this.
let node: Fiber = fiber;
// 外層while循環
siblings: while (true) {
// If we didn't find anything, let's try the next sibling.
// 如果沒有兄弟節點,向上查找父節點,但是這個父節點不能是原生dom節點
while (node.sibling === null) {
if (node.return === null || isHostParent(node.return)) {
// 如果到了根節點root了 或者 是原生dom節點 返回 null 說明在真實的dom中 插入的這個節點沒有兄弟節點
// If we pop out of the root or hit the parent the fiber we are the
// last sibling.
return null;
}
node = node.return;
}
// 下面是有兄弟節點的情況
node.sibling.return = node.return;
node = node.sibling;
// 兄弟節點不是HostComponent也不是HostText
while (node.tag !== HostComponent && node.tag !== HostText) {
// If it is not host node and, we might have a host node inside it.
// Try to search down until we find one.
// 兄弟節點也是將要插入的節點,跳過這個節點查找下一個兄弟節點
if (node.effectTag & Placement) {
// If we don't have a child, try the siblings instead.
continue siblings;
}
// If we don't have a child, try the siblings instead.
// We also skip portals because they are not part of this host tree.
// 如果沒有子節點或者是HostPortal也跳過這個節點查找下一個兄弟節點
if (node.child === null || node.tag === HostPortal) {
continue siblings;
} else {
// 否則返回兄弟節點的子節點
node.child.return = node;
node = node.child;
}
}
// Check if this host node is stable or about to be placed.
// 如果兄弟節點也是新增節點,尋找下一個兄弟節點
// 否則,就找到了!
if (!(node.effectTag & Placement)) {
// Found it!
return node.stateNode;
}
}
}
commitWork
1、commitWork 只會更新 HostComponent(dom 節點) 和 HostText(文本節點)
2、HostComponent調用commitUpdate更新
3、HostText調用commitTextUpdate更新
function commitWork(current: Fiber | null, finishedWork: Fiber): void {
// ......
switch (finishedWork.tag) {
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.
// 取出updateQueue
const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);
// 清空updateQueue
finishedWork.updateQueue = null;
if (updatePayload !== null) {
// 進行更新
commitUpdate(
instance,
updatePayload,
type,
oldProps,
newProps,
finishedWork,
);
}
}
return;
}
case HostText: {
// 更新文本節點
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: {
return;
}
case Profiler: {
return;
}
case SuspenseComponent: {
return;
}
case IncompleteClassComponent: {
return;
}
default: {
// ......
}
}
}
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.
// 這裏只是: domElement[__reactEventHandlers$] = newProps
updateFiberProps(domElement, newProps);
// Apply the diff to the DOM node.
updateProperties(domElement, updatePayload, type, oldProps, newProps);
}
updateProperties:
1、針對特殊的標籤有特殊的處理,比如表單
2、調用updateDOMProperties把之前的diff結果應用的真實dom上面
// Apply the diff.
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
) {
ReactDOMInput.updateChecked(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.
ReactDOMInput.updateWrapper(domElement, nextRawProps);
break;
case 'textarea':
ReactDOMTextarea.updateWrapper(domElement, nextRawProps);
break;
case 'select':
// <select> value update needs to occur after <option> children
// reconciliation
ReactDOMSelect.postUpdateWrapper(domElement, nextRawProps);
break;
}
}
updateDOMProperties:
1、把我們之前渲染階段生成的updatePayload: [k1,null,k2,v2,k3,v3]應用到真實的dom上。
2、對一些dom節點上特殊的屬性做特殊的處理,比如style、dangerouslySetInnerHTML等
function updateDOMProperties(
domElement: Element,
updatePayload: Array<any>,
wasCustomComponentTag: boolean,
isCustomComponentTag: boolean,
): void {
// TODO: Handle wasCustomComponentTag
// 遍歷updatePayload
for (let i = 0; i < updatePayload.length; i += 2) {
// 取出key 和 value
const propKey = updatePayload[i];
const propValue = updatePayload[i + 1];
if (propKey === STYLE) {
// 如果是樣式更新
CSSPropertyOperations.setValueForStyles(domElement, propValue);
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
// 如果是dangerouslySetInnerHTML
setInnerHTML(domElement, propValue);
} else if (propKey === CHILDREN) {
// 如果是文本
setTextContent(domElement, propValue);
} else {
// 否則正常的更新節點的屬性
DOMPropertyOperations.setValueForProperty(
domElement,
propKey,
propValue,
isCustomComponentTag,
);
}
}
}
commitTextUpdate:
文本節點的更新就非常簡單了
export function commitTextUpdate(
textInstance: TextInstance,
oldText: string,
newText: string,
): void {
textInstance.nodeValue = newText;
}
commitDeletion
在我們刪除一個節點時候,你可能簡單的想一下,就把一個節點刪除就好了呀。其實沒這麼簡單哦。
刪除一個節點時候,是需要去遍歷整顆子樹的。第一,如果dom節點下面還有class組件,那麼我們是要調用它的生命週期方法的(componentWillUnmount),第二,如果有HostPortal,那麼還要去刪除HostPortal中的節點,所以我們必須要遍歷子樹的。這裏的遍歷採用樹的深度優先遍歷。
react用了三個函數用循環+遞歸深度優先遍歷了整顆子樹。
function commitDeletion(current: Fiber): void {
// Recursively delete all host nodes from the parent.
// Detach refs and call componentWillUnmount() on the whole subtree.
unmountHostComponents(current);
// ......
detachFiber(current);
}
unmountHostComponents:
function unmountHostComponents(current): void {
// We only have the top Fiber that was deleted but we need recurse down its
// children to find all the terminal nodes.
let node: Fiber = current;
// Each iteration, currentParent is populated with node's host parent if not
// currentParentIsValid.
let currentParentIsValid = false;
// Note: these two variables *must* always be updated together.
let currentParent;
let currentParentIsContainer;
while (true) {
// 找的父節點,這個父節點一定是個dom節點
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.',
);
switch (parent.tag) {
case HostComponent:
currentParent = parent.stateNode;
currentParentIsContainer = false;
break findParent;
case HostRoot:
currentParent = parent.stateNode.containerInfo;
currentParentIsContainer = true;
break findParent;
case HostPortal:
currentParent = parent.stateNode.containerInfo;
currentParentIsContainer = true;
break findParent;
}
parent = parent.return;
}
currentParentIsValid = true;
}
if (node.tag === HostComponent || node.tag === HostText) {
// 如果是原生dom節點
commitNestedUnmounts(node);
// After all the children have unmounted, it is now safe to remove the
// node from the tree.
if (currentParentIsContainer) {
removeChildFromContainer((currentParent: any), node.stateNode);
} else {
removeChild((currentParent: any), node.stateNode);
}
// Don't visit children because we already visited them.
} else if (node.tag === HostPortal) {
// 如果是HostPortal不會做什麼操作,直接向下遍歷子節點 因爲它沒有ref,也沒有生命週期
// 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.
if (node.child !== null) {
node.child.return = node;
node = node.child;
continue;
}
} else {
// 到這裏的話,其實就是react組件節點了,調用commitUnmount,這個方法裏會有生命週期的調用,ref的卸載
commitUnmount(node);
// Visit children because we may find more host components below.
// 繼續遍歷子節點
if (node.child !== null) {
node.child.return = node;
node = node.child;
continue;
}
}
// 整棵樹遍歷完成
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:
這個方法使用深度優先遍歷整顆dom節點爲父節點的樹。子節點還有可能是react組件或者HostPortal。
這裏對每個節點都要調用commitUnmount。如果遇到了HostPortal就會停止對它下面的子樹進行遍歷,因爲在commitUnmount中會對HostPortal類型有個特殊的處理。下面再看。
function commitNestedUnmounts(root: Fiber): void {
// While we're inside a removed host node we don't want to call
// removeChild on the inner nodes because they're removed by the top
// call anyway. We also want to call componentWillUnmount on all
// composites before this host node is removed from the tree. Therefore
// we do an inner loop while we're still inside the host node.
let node: Fiber = root;
while (true) {
commitUnmount(node);
// Visit children because they may contain more composite or host nodes.
// Skip portals because commitUnmount() currently visits them recursively.
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)
) {
// 如果是HostPortal就直接跳過子樹的遍歷,所以下面不執行
node.child.return = node;
node = node.child;
continue;
}
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:
這個方法會對不同的節點做不同的處理,對ClassComponent會執行它的生命週期以及卸載ref,對HostComponent卸載ref,對HostPortal重新調用unmountHostComponents,這裏我們就明白了爲什麼上一個方法遇到HostPortal就停止了對它的子樹的遍歷,因爲它會重新遞歸調用unmountHostComponents遍歷子樹。
function commitUnmount(current: Fiber): void {
onCommitUnmount(current);
switch (current.tag) {
case ClassComponent: {
// 卸載ref
safelyDetachRef(current);
const instance = current.stateNode;
// 執行WillUnmount方法,這個時候真實dom還沒有卸載,即將卸載
if (typeof instance.componentWillUnmount === 'function') {
safelyCallComponentWillUnmount(current, instance);
}
return;
}
case HostComponent: {
// 卸載ref
safelyDetachRef(current);
return;
}
case HostPortal: {
// TODO: this is recursive.
// We are also not using this parent because
// the portal will get pushed immediately.
// 遞歸調用,遍歷子樹
unmountHostComponents(current);
// ......
return;
}
}
}
第三次遍歷-commitAllLifeCycles
這次遍歷就是做一些收尾工作。
1、首次渲染執行componentDidMount
2、更新渲染執行 componentDidUpdate
3、執行 setState 的 callback 回調函數
4、清空commitUpdateQueue
function commitAllLifeCycles(
finishedRoot: FiberRoot,
committedExpirationTime: ExpirationTime,
) {
// 循環effect鏈
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
// 有更新或者有回調函數
if (effectTag & (Update | Callback)) {
recordEffect();
const current = nextEffect.alternate;
commitLifeCycles(
finishedRoot,
current,
nextEffect,
committedExpirationTime,
);
}
if (effectTag & Ref) {
recordEffect();
commitAttachRef(nextEffect);
}
const next = nextEffect.nextEffect;
// Ensure that we clean these up so that we don't accidentally keep them.
// I'm not actually sure this matters because we can't reset firstEffect
// and lastEffect since they're on every node, not just the effectful
// ones. So we have to clean everything as we reuse nodes anyway.
nextEffect.nextEffect = null;
// Ensure that we reset the effectTag here so that we can rely on effect
// tags to reason about the current life-cycle.
nextEffect = next;
}
}
commitLifeCycles:
這個方法會執行componentDidMount或者componentDidUpdate生命週期方法,最後調用commitUpdateQueue。
function commitLifeCycles(
finishedRoot: FiberRoot,
current: Fiber | null,
finishedWork: Fiber,
committedExpirationTime: ExpirationTime,
): void {
switch (finishedWork.tag) {
case ClassComponent: {
const instance = finishedWork.stateNode;
if (finishedWork.effectTag & Update) {
if (current === null) {
// 首次渲染
startPhaseTimer(finishedWork, 'componentDidMount');
instance.props = finishedWork.memoizedProps;
instance.state = finishedWork.memoizedState;
instance.componentDidMount();
stopPhaseTimer();
} else {
// 組件更新
const prevProps = current.memoizedProps;
const prevState = current.memoizedState;
startPhaseTimer(finishedWork, 'componentDidUpdate');
instance.props = finishedWork.memoizedProps;
instance.state = finishedWork.memoizedState;
instance.componentDidUpdate(
prevProps,
prevState,
instance.__reactInternalSnapshotBeforeUpdate,
);
stopPhaseTimer();
}
}
const updateQueue = finishedWork.updateQueue;
if (updateQueue !== null) {
instance.props = finishedWork.memoizedProps;
instance.state = finishedWork.memoizedState;
commitUpdateQueue(
finishedWork,
updateQueue,
instance,
committedExpirationTime,
);
}
return;
}
case HostRoot: {
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;
}
}
// 因爲ReactDOM.render也有回調函數 所以也要調用commitUpdateQueue
commitUpdateQueue(
finishedWork,
updateQueue,
instance,
committedExpirationTime,
);
}
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.effectTag & Update) {
const type = finishedWork.type;
const props = finishedWork.memoizedProps;
// 這個函數只是對input標籤有 auto-focus 的情況進行處理
commitMount(instance, type, props, finishedWork);
}
return;
}
case HostText: ......
case HostPortal: ......
case Profiler: ......
case SuspenseComponent: ......
case IncompleteClassComponent:......
default: {
// ......
}
}
}
commitUpdateQueue:
1、調用commitUpdateEffects
2、清空commitUpdateQueue
export function commitUpdateQueue<State>(
finishedWork: Fiber,
finishedQueue: UpdateQueue<State>,
instance: any,
renderExpirationTime: ExpirationTime,
): void {
// ......
// Commit the effects
commitUpdateEffects(finishedQueue.firstEffect, instance);
// 清空commitUpdateQueue
finishedQueue.firstEffect = finishedQueue.lastEffect = null;
// ......
}
commitUpdateEffects:
調用effect上的回調函數。
function commitUpdateEffects<State>(
effect: Update<State> | null,
instance: any,
): void {
while (effect !== null) {
const callback = effect.callback;
if (callback !== null) {
// 調用回調函數,也就是setState的回調函數
effect.callback = null;
callCallback(callback, instance);
}
effect = effect.nextEffect;
}
}
任世界紛繁複雜,仍舊保持可愛。
我是小柚子小仙女。文章如有不妥,歡迎指正~