React commit源碼分析詳解
總覽
commit 階段相比于 render 階段要簡(jiǎn)單很多,因?yàn)榇蟛糠指碌那捌诓僮鞫荚?render 階段做好了,commit 階段主要做的是根據(jù)之前生成的 effectList,對(duì)相應(yīng)的真實(shí) dom 進(jìn)行更新和渲染,這個(gè)階段是不可中斷的。
commit 階段大致可以分為以下幾個(gè)過程:
- 獲取 effectList 鏈表,如果 root 上有 effect,則將其也添加進(jìn) effectList 中
- 對(duì) effectList 進(jìn)行第一次遍歷,執(zhí)行
commitBeforeMutationEffects
函數(shù)來更新class組件實(shí)例上的state、props 等,以及執(zhí)行 getSnapshotBeforeUpdate 生命周期函數(shù) - 對(duì) effectList 進(jìn)行第二次遍歷,執(zhí)行
commitMutationEffects
函數(shù)來完成副作用的執(zhí)行,主要包括重置文本節(jié)點(diǎn)以及真實(shí) dom 節(jié)點(diǎn)的插入、刪除和更新等操作。 - 對(duì) effectList 進(jìn)行第三次遍歷,執(zhí)行
commitLayoutEffects
函數(shù),去觸發(fā) componentDidMount、componentDidUpdate 以及各種回調(diào)函數(shù)等 - 最后進(jìn)行一點(diǎn)變量還原之類的收尾,就完成了 commit 階段
我們從 commit 階段的入口函數(shù) commitRoot
開始看:
// packages/react-reconciler/src/ReactFiberWorkLoop.old.js function commitRoot(root) { const renderPriorityLevel = getCurrentPriorityLevel(); runWithPriority( ImmediateSchedulerPriority, commitRootImpl.bind(null, root, renderPriorityLevel), ); return null; }
它調(diào)用了 commitRootImpl
函數(shù),所要做的工作都在這個(gè)函數(shù)中:
// packages/react-reconciler/src/ReactFiberWorkLoop.old.js function commitRootImpl(root, renderPriorityLevel) { // ... const finishedWork = root.finishedWork; const lanes = root.finishedLanes; // ... // 獲取 effectList 鏈表 let firstEffect; if (finishedWork.flags > PerformedWork) { // 如果 root 上有 effect,則將其添加進(jìn) effectList 鏈表中 if (finishedWork.lastEffect !== null) { finishedWork.lastEffect.nextEffect = finishedWork; firstEffect = finishedWork.firstEffect; } else { firstEffect = finishedWork; } } else { // 如果 root 上沒有 effect,直接使用 finishedWork.firstEffect 作用鏈表頭節(jié)點(diǎn) firstEffect = finishedWork.firstEffect; } if (firstEffect !== null) { // ... // 第一次遍歷,執(zhí)行 commitBeforeMutationEffects nextEffect = firstEffect; do { if (__DEV__) { invokeGuardedCallback(null, commitBeforeMutationEffects, null); // ... } else { try { commitBeforeMutationEffects(); } catch (error) { // ... } } } while (nextEffect !== null); // ... // 第二次遍歷,執(zhí)行 commitMutationEffects nextEffect = firstEffect; do { if (__DEV__) { invokeGuardedCallback( null, commitMutationEffects, null, root, renderPriorityLevel, ); // ... } else { try { commitMutationEffects(root, renderPriorityLevel); } catch (error) { // ... } } } while (nextEffect !== null); // 第三次遍歷,執(zhí)行 commitLayoutEffects nextEffect = firstEffect; do { if (__DEV__) { invokeGuardedCallback(null, commitLayoutEffects, null, root, lanes); // ... } else { try { commitLayoutEffects(root, lanes); } catch (error) { // ... } } } while (nextEffect !== null); nextEffect = null; // ... } else { // 沒有任何副作用 root.current = finishedWork; if (enableProfilerTimer) { recordCommitTime(); } } // ... }
commitBeforeMutationEffects
commitBeforeMutationEffects
中,會(huì)從 firstEffect 開始,通過 nextEffect 不斷對(duì) effectList 鏈表進(jìn)行遍歷,若是當(dāng)前的 fiber 節(jié)點(diǎn)有 flags 副作用,則執(zhí)行 commitBeforeMutationEffectOnFiber
節(jié)點(diǎn)去對(duì)針對(duì) class 組件單獨(dú)處理。
相關(guān)參考視頻講解:傳送門
// packages/react-reconciler/src/ReactFiberWorkLoop.old.js function commitBeforeMutationEffects() { while (nextEffect !== null) { // ... const flags = nextEffect.flags; if ((flags & Snapshot) !== NoFlags) { // 如果當(dāng)前 fiber 節(jié)點(diǎn)有 flags 副作用 commitBeforeMutationEffectOnFiber(current, nextEffect); // ... } // ... nextEffect = nextEffect.nextEffect; } }
然后看一下 commitBeforeMutationEffectOnFiber
,它里面根據(jù) fiber 的 tag 屬性,主要是對(duì) ClassComponent 組件進(jìn)行處理,更新 ClassComponent 實(shí)例上的state、props 等,以及執(zhí)行 getSnapshotBeforeUpdate 生命周期函數(shù):
// packages/react-reconciler/src/ReactFiberCommitWork.old.js 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; // 獲取當(dāng)前 class 組件實(shí)例 const instance = finishedWork.stateNode; // ... // 調(diào)用 getSnapshotBeforeUpdate 生命周期方法 const snapshot = instance.getSnapshotBeforeUpdate( finishedWork.elementType === finishedWork.type ? prevProps : resolveDefaultProps(finishedWork.type, prevProps), prevState, ); // ... // 將生成的 snapshot 保存到 instance.__reactInternalSnapshotBeforeUpdate 上,供 DidUpdate 生命周期使用 instance.__reactInternalSnapshotBeforeUpdate = snapshot; } } return; } // ... } }
commitMutationEffects
commitMutationEffects
中會(huì)根據(jù)對(duì) effectList 進(jìn)行第二次遍歷,根據(jù) flags 的類型進(jìn)行二進(jìn)制與操作,然后根據(jù)結(jié)果去執(zhí)行不同的操作,對(duì)真實(shí) dom 進(jìn)行修改:相關(guān)參考視頻講解:進(jìn)入學(xué)習(xí)
- ContentReset: 如果 flags 中包含 ContentReset 類型,代表文本節(jié)點(diǎn)內(nèi)容改變,則執(zhí)行
commitResetTextContent
重置文本節(jié)點(diǎn)的內(nèi)容 - Ref: 如果 flags 中包含 Ref 類型,則執(zhí)行
commitDetachRef
更改 ref 對(duì)應(yīng)的 current 的值 - Placement: 上一章 diff 中講過 Placement 代表插入,會(huì)執(zhí)行
commitPlacement
去插入 dom 節(jié)點(diǎn) - Update: flags 包含 Update 則會(huì)執(zhí)行
commitWork
執(zhí)行更新操作 - Deletion: flags 包含 Deletion 則會(huì)執(zhí)行
commitDeletion
執(zhí)行更新操作
// packages/react-reconciler/src/ReactFiberWorkLoop.old.js function commitMutationEffects( root: FiberRoot, renderPriorityLevel: ReactPriorityLevel, ) { // 對(duì) effectList 進(jìn)行遍歷 while (nextEffect !== null) { setCurrentDebugFiberInDEV(nextEffect); const flags = nextEffect.flags; // ContentReset:重置文本節(jié)點(diǎn) if (flags & ContentReset) { commitResetTextContent(nextEffect); } // Ref:commitDetachRef 更新 ref 的 current 值 if (flags & Ref) { const current = nextEffect.alternate; if (current !== null) { commitDetachRef(current); } if (enableScopeAPI) { if (nextEffect.tag === ScopeComponent) { commitAttachRef(nextEffect); } } } // 執(zhí)行更新、插入、刪除操作 const primaryFlags = flags & (Placement | Update | Deletion | Hydrating); switch (primaryFlags) { case Placement: { // 插入 commitPlacement(nextEffect); nextEffect.flags &= ~Placement; break; } case PlacementAndUpdate: { // 插入并更新 // 插入 commitPlacement(nextEffect); nextEffect.flags &= ~Placement; // 更新 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; } }
下面我們重點(diǎn)來看一下 react 是如何對(duì)真實(shí) dom 節(jié)點(diǎn)進(jìn)行操作的。
插入 dom 節(jié)點(diǎn)
獲取父節(jié)點(diǎn)及插入位置
插入 dom 節(jié)點(diǎn)的操作以 commitPlacement
為入口函數(shù), commitPlacement
中會(huì)首先獲取當(dāng)前 fiber 的父 fiber 對(duì)應(yīng)的真實(shí) dom 節(jié)點(diǎn)以及在父節(jié)點(diǎn)下要插入的位置,根據(jù)父節(jié)點(diǎn)對(duì)應(yīng)的 dom 是否為 container,去執(zhí)行 insertOrAppendPlacementNodeIntoContainer
或者 insertOrAppendPlacementNode
進(jìn)行節(jié)點(diǎn)的插入。
// packages/react-reconciler/src/ReactFiberCommitWork.old.js function commitPlacement(finishedWork: Fiber): void { if (!supportsMutation) { return; } // 獲取當(dāng)前 fiber 的父 fiber const parentFiber = getHostParentFiber(finishedWork); let parent; let isContainer; // 獲取父 fiber 對(duì)應(yīng)真實(shí) dom 節(jié)點(diǎn) const parentStateNode = parentFiber.stateNode; // 獲取父 fiber 對(duì)應(yīng)的 dom 是否可以作為 container 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; } default: invariant( false, 'Invalid host parent fiber. This error is likely caused by a bug ' + 'in React. Please file an issue.', ); } // 如果父 fiber 有 ContentReset 的 flags 副作用,則重置其文本內(nèi)容 if (parentFiber.flags & ContentReset) { resetTextContent(parent); parentFiber.flags &= ~ContentReset; } // 獲取要在哪個(gè)兄弟 fiber 之前插入 const before = getHostSibling(finishedWork); if (isContainer) { insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent); } else { insertOrAppendPlacementNode(finishedWork, before, parent); } }
判斷當(dāng)前節(jié)點(diǎn)是否為單節(jié)點(diǎn)
我們以 insertOrAppendPlacementNodeIntoContainer
為例看一下其源碼,里面通過 tag 屬性判斷了當(dāng)前的 fiber 是否為原生 dom 節(jié)點(diǎn)。若是,則調(diào)用 insertInContainerBefore
或 appendChildToContainer
在相應(yīng)位置插入真實(shí) dom;若不是,則對(duì)當(dāng)前 fiber 的所有子 fiber 調(diào)用 insertOrAppendPlacementNodeIntoContainer
進(jìn)行遍歷:
// packages/react-reconciler/src/ReactFiberCommitWork.old.js function insertOrAppendPlacementNodeIntoContainer( node: Fiber, before: ?Instance, parent: Container, ): void { const {tag} = node; // 判斷當(dāng)前節(jié)點(diǎn)是否為原生的 dom 節(jié)點(diǎn) const isHost = tag === HostComponent || tag === HostText; if (isHost || (enableFundamentalAPI && tag === FundamentalComponent)) { // 是原生 dom 節(jié)點(diǎn),在父節(jié)點(diǎn)的對(duì)應(yīng)位置插入當(dāng)前節(jié)點(diǎn) const stateNode = isHost ? node.stateNode : node.stateNode.instance; if (before) { insertInContainerBefore(parent, stateNode, before); } else { appendChildToContainer(parent, stateNode); } } else if (tag === HostPortal) { // 如是 Portal 不做處理 } else { // 不是原生 dom 節(jié)點(diǎn),則遍歷插入當(dāng)前節(jié)點(diǎn)的各個(gè)子節(jié)點(diǎn) 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; } } } }
在對(duì)應(yīng)位置插入節(jié)點(diǎn)
before 不為 null 時(shí),說明要在某個(gè) dom 節(jié)點(diǎn)之前插入新的 dom,調(diào)用 insertInContainerBefore
去進(jìn)行插入,根據(jù)父節(jié)點(diǎn)是否注釋類型,選擇在父節(jié)點(diǎn)的父節(jié)點(diǎn)下插入新的 dom,還是直接在父節(jié)點(diǎn)下插入新的 dom:
// packages/react-dom/src/client/ReactDOMHostConfig.js export function insertInContainerBefore( container: Container, child: Instance | TextInstance, beforeChild: Instance | TextInstance | SuspenseInstance, ): void { if (container.nodeType === COMMENT_NODE) { // 如果父節(jié)點(diǎn)為注釋類型,則在父節(jié)點(diǎn)的父節(jié)點(diǎn)下插入新的 dom (container.parentNode: any).insertBefore(child, beforeChild); } else { // 否則直接插入新的 dom container.insertBefore(child, beforeChild); } }
before 為 null 時(shí),調(diào)用 appendChildToContainer
方法,直接在父節(jié)點(diǎn)(如果父節(jié)點(diǎn)為注釋類型則在父節(jié)點(diǎn)的父節(jié)點(diǎn))的最后位置插入新的 dom:
export function appendChildToContainer( container: Container, child: Instance | TextInstance, ): void { let parentNode; if (container.nodeType === COMMENT_NODE) { // 如果父節(jié)點(diǎn)為注釋類型,則在父節(jié)點(diǎn)的父節(jié)點(diǎn)下插入新的 dom parentNode = (container.parentNode: any); parentNode.insertBefore(child, container); } else { // 否則直接插入新的 dom parentNode = container; parentNode.appendChild(child); } // ... }
這幾步都是以 insertOrAppendPlacementNodeIntoContainer
為例看源碼,insertOrAppendPlacementNode
和它的唯一區(qū)別就是最后在對(duì)應(yīng)位置插入節(jié)點(diǎn)時(shí),不需要額外判斷父節(jié)點(diǎn) (container) 是否為 COMMENT_TYPE 了。
更新 dom 節(jié)點(diǎn)
更新操作以 commitWork
為入口函數(shù),更新主要是針對(duì) HostComponent 和 HostText 兩種類型進(jìn)行更新。
// packages/react-reconciler/src/ReactFiberCommitWork.old.js function commitWork(current: Fiber | null, finishedWork: Fiber): void { // ... switch (finishedWork.tag) { // ... case ClassComponent: { return; } case HostComponent: { // 獲取真實(shí) dom 節(jié)點(diǎn) const instance: Instance = finishedWork.stateNode; if (instance != null) { // 獲取新的 props const newProps = finishedWork.memoizedProps; // 獲取老的 props const oldProps = current !== null ? current.memoizedProps : newProps; const type = finishedWork.type; // 取出 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: { // 獲取真實(shí)文本節(jié)點(diǎn) const textInstance: TextInstance = finishedWork.stateNode; // 獲取新的文本內(nèi)容 const newText: string = finishedWork.memoizedProps; // 獲取老的文本內(nèi)容 const oldText: string = current !== null ? current.memoizedProps : newText; // 提交更新 commitTextUpdate(textInstance, oldText, newText); return; } case HostRoot: { // ssr操作,暫不關(guān)注 if (supportsHydration) { const root: FiberRoot = finishedWork.stateNode; if (root.hydrate) { root.hydrate = false; commitHydratedContainer(root.containerInfo); } } return; } case Profiler: { return; } // ... }
更新 HostComponent
根據(jù)上面的 commitWork 的源碼,更新 HostComponent 時(shí),獲取了真實(shí) dom 節(jié)點(diǎn)實(shí)例、props 以及 updateQueue 之后,就調(diào)用 commitUpdate
對(duì) dom 進(jìn)行更新,它通過 updateProperties
函數(shù)將 props 變化應(yīng)用到真實(shí) dom 上。
// packages/react-dom/src/client/ReactDOMHostConfig.js export function commitUpdate( domElement: Instance, updatePayload: Array<mixed>, type: string, oldProps: Props, newProps: Props, internalInstanceHandle: Object, ): void { // 做了 domElement[internalPropsKey] = props 的操作 updateFiberProps(domElement, newProps); // 應(yīng)用給真實(shí) dom updateProperties(domElement, updatePayload, type, oldProps, newProps); }
updateProperties
中,通過 updateDOMProperties
將 diff 結(jié)果應(yīng)用于真實(shí)的 dom 節(jié)點(diǎn)。另外根據(jù) fiber 的 tag 屬性,如果判斷對(duì)應(yīng)的 dom 的節(jié)點(diǎn)為表單類型,例如 radio、textarea、input、select 等,會(huì)做特定的處理:
// packages/react-dom/src/client/ReactDOMComponent.js export function updateProperties( domElement: Element, updatePayload: Array<any>, tag: string, lastRawProps: Object, nextRawProps: Object, ): void { // 針對(duì)表單組件進(jìn)行特殊處理,例如更新 radio 的 checked 值 if ( tag === 'input' && nextRawProps.type === 'radio' && nextRawProps.name != null ) { ReactDOMInputUpdateChecked(domElement, nextRawProps); } // 判斷是否為用戶自定義的組件,即是否包含 "-" const wasCustomComponentTag = isCustomComponent(tag, lastRawProps); const isCustomComponentTag = isCustomComponent(tag, nextRawProps); // 將 diff 結(jié)果應(yīng)用于真實(shí) dom updateDOMProperties( domElement, updatePayload, wasCustomComponentTag, isCustomComponentTag, ); // 針對(duì)表單的特殊處理 switch (tag) { case 'input': ReactDOMInputUpdateWrapper(domElement, nextRawProps); break; case 'textarea': ReactDOMTextareaUpdateWrapper(domElement, nextRawProps); break; case 'select': ReactDOMSelectPostUpdateWrapper(domElement, nextRawProps); break; } }
updateDOMProperties
中,會(huì)遍歷之前 render 階段生成的 updatePayload,將其映射到真實(shí)的 dom 節(jié)點(diǎn)屬性上,另外會(huì)針對(duì) style、dangerouslySetInnerHTML 以及 textContent 做一些處理,從而實(shí)現(xiàn)了 dom 的更新:
// packages/react-dom/src/client/ReactDOMHostConfig.js function updateDOMProperties( domElement: Element, updatePayload: Array<any>, wasCustomComponentTag: boolean, isCustomComponentTag: boolean, ): void { // 對(duì) updatePayload 遍歷 for (let i = 0; i < updatePayload.length; i += 2) { const propKey = updatePayload[i]; const propValue = updatePayload[i + 1]; if (propKey === STYLE) { // 處理 style 樣式更新 setValueForStyles(domElement, propValue); } else if (propKey === DANGEROUSLY_SET_INNER_HTML) { // 處理 innerHTML 改變 setInnerHTML(domElement, propValue); } else if (propKey === CHILDREN) { // 處理 textContent setTextContent(domElement, propValue); } else { // 處理其他節(jié)點(diǎn)屬性 setValueForProperty(domElement, propKey, propValue, isCustomComponentTag); } } }
更新 HostText
HostText 的更新處理十分簡(jiǎn)單,調(diào)用 commitTextUpdate
,里面直接將 dom 的 nodeValue 設(shè)置為 newText 的值:
// packages/react-dom/src/client/ReactDOMHostConfig.js export function commitTextUpdate( textInstance: TextInstance, oldText: string, newText: string, ): void { textInstance.nodeValue = newText; }
刪除 dom 節(jié)點(diǎn)
刪除 dom 節(jié)點(diǎn)的操作以 commitDeletion
為入口函數(shù),它所要做的事情最復(fù)雜。react 會(huì)采用深度優(yōu)先遍歷去遍歷整顆 fiber 樹,找到需要?jiǎng)h除的 fiber,除了要將對(duì)應(yīng)的 dom 節(jié)點(diǎn)刪除,還需要考慮 ref 的卸載、componentWillUnmount 等生命周期的調(diào)用:
// packages/react-reconciler/src/ReactFiberCommitWork.old.js function commitDeletion( finishedRoot: FiberRoot, current: Fiber, renderPriorityLevel: ReactPriorityLevel, ): void { if (supportsMutation) { // 支持 useMutation unmountHostComponents(finishedRoot, current, renderPriorityLevel); } else { // 不支持 useMutation commitNestedUnmounts(finishedRoot, current, renderPriorityLevel); } const alternate = current.alternate; // 重置 fiber 的各項(xiàng)屬性 detachFiberMutation(current); if (alternate !== null) { detachFiberMutation(alternate); } }
unmountHostComponents
unmountHostComponents
首先判斷當(dāng)前父節(jié)點(diǎn)是否合法,若是不合法尋找合法的父節(jié)點(diǎn),然后通過深度優(yōu)先遍歷,去遍歷整棵樹,通過 commitUnmount
卸載 ref、執(zhí)行生命周期。遇到是原生 dom 類型的節(jié)點(diǎn),還會(huì)從對(duì)應(yīng)的父節(jié)點(diǎn)下刪除該節(jié)點(diǎn)。
// 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) { // 若當(dāng)前的父節(jié)點(diǎn)不是非法的 dom 節(jié)點(diǎn),尋找一個(gè)合法的 dom 父節(jié)點(diǎn) 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) { // 若果是原生 dom 節(jié)點(diǎn),調(diào)用 commitNestedUnmounts 方法 commitNestedUnmounts(finishedRoot, node, renderPriorityLevel); if (currentParentIsContainer) { // 若當(dāng)前的 parent 是 container,則將 child 從 container 中移除(通過 dom.removeChild 方法) removeChildFromContainer( ((currentParent: any): Container), (node.stateNode: Instance | TextInstance), ); } else { // 從 parent 中移除 child(通過 dom.removeChild 方法) removeChild( ((currentParent: any): Instance), (node.stateNode: Instance | TextInstance), ); } } // ... else if (node.tag === HostPortal) { // 若是 portal 節(jié)點(diǎn),直接向下遍歷 child,因?yàn)樗鼪]有 ref 和生命周期等額外要處理的事情 if (node.child !== null) { currentParent = node.stateNode.containerInfo; currentParentIsContainer = true; node.child.return = node; node = node.child; continue; } } else { // 其他 react 節(jié)點(diǎn),調(diào)用 commitUnmount,里面會(huì)卸載 ref、執(zhí)行生命周期等 commitUnmount(finishedRoot, node, renderPriorityLevel); // 深度優(yōu)先遍歷子節(jié)點(diǎn) if (node.child !== null) { node.child.return = node; node = node.child; continue; } } // node 和 current 相等時(shí)說明整顆樹的深度優(yōu)先遍歷完成 if (node === current) { return; } // 如果沒有兄弟節(jié)點(diǎn),說明當(dāng)前子樹遍歷完畢,返回到父節(jié)點(diǎn)繼續(xù)深度優(yōu)先遍歷 while (node.sibling === null) { if (node.return === null || node.return === current) { return; } node = node.return; if (node.tag === HostPortal) { currentParentIsValid = false; } } // 繼續(xù)遍歷兄弟節(jié)點(diǎn) node.sibling.return = node.return; node = node.sibling; } }
commitNestedUnmounts
commitNestedUnmounts
相比 unmountHostComponents
不需要額外做當(dāng)前父節(jié)點(diǎn)是否合法的判斷以及 react 節(jié)點(diǎn)類型的判斷,直接采用深度優(yōu)先遍歷,去執(zhí)行 commitUnmount
方法即可:
// packages/react-reconciler/src/ReactFiberCommitWork.old.js function commitNestedUnmounts( finishedRoot: FiberRoot, root: Fiber, renderPriorityLevel: ReactPriorityLevel, ): void { let node: Fiber = root; while (true) { // 調(diào)用 commitUnmount 去卸載 ref、執(zhí)行生命周期 commitUnmount(finishedRoot, node, renderPriorityLevel); if ( node.child !== null && (!supportsMutation || node.tag !== HostPortal) ) { // 深度優(yōu)先遍歷向下遍歷子樹 node.child.return = node; node = node.child; continue; } if (node === root) { // node 為 root 時(shí)說明整棵樹的深度優(yōu)先遍歷完成 return; } while (node.sibling === null) { // node.sibling 為 null 時(shí)說明當(dāng)前子樹遍歷完成,返回上級(jí)節(jié)點(diǎn)繼續(xù)深度優(yōu)先遍歷 if (node.return === null || node.return === root) { return; } node = node.return; } // 遍歷兄弟節(jié)點(diǎn) node.sibling.return = node.return; node = node.sibling; } }
commitUnmount
commitUnmount 中會(huì)完成對(duì) react 組件 ref 的卸載,若果是類組件,執(zhí)行 componentWillUnmount 生命周期等操作:
// packages/react-reconciler/src/ReactFiberCommitWork.old.js function commitUnmount( finishedRoot: FiberRoot, current: Fiber, renderPriorityLevel: ReactPriorityLevel, ): void { onCommitUnmount(current); switch (current.tag) { case FunctionComponent: case ForwardRef: case MemoComponent: case SimpleMemoComponent: // ... case ClassComponent: { // 卸載 ref safelyDetachRef(current); const instance = current.stateNode; // 執(zhí)行 componentWillUnmount 生命周期 if (typeof instance.componentWillUnmount === 'function') { safelyCallComponentWillUnmount(current, instance); } return; } case HostComponent: { // 卸載 ref safelyDetachRef(current); return; } case HostPortal: { if (supportsMutation) { // 遞歸遍歷子樹 unmountHostComponents(finishedRoot, current, renderPriorityLevel); } else if (supportsPersistence) { emptyPortalContainer(current); } return; } // ... } }
最終通過以上操作,react 就完成了 dom 的刪除工作。
commitLayoutEffects
接下來通過 commitLayoutEffects
為入口函數(shù),執(zhí)行第三次遍歷,這里會(huì)遍歷 effectList,執(zhí)行 componentDidMount
、componentDidUpdate
等生命周期,另外會(huì)執(zhí)行 componentUpdateQueue
函數(shù)去執(zhí)行回調(diào)函數(shù)。
// packages/react-reconciler/src/ReactFiberWorkLoop.old.js function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) { // ... // 遍歷 effectList while (nextEffect !== null) { setCurrentDebugFiberInDEV(nextEffect); const flags = nextEffect.flags; if (flags & (Update | Callback)) { const current = nextEffect.alternate; // 執(zhí)行 componentDidMount、componentDidUpdate 以及 componentUpdateQueue commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes); } // 更新 ref if (enableScopeAPI) { if (flags & Ref && nextEffect.tag !== ScopeComponent) { commitAttachRef(nextEffect); } } else { if (flags & Ref) { commitAttachRef(nextEffect); } } resetCurrentDebugFiberInDEV(); nextEffect = nextEffect.nextEffect; } }
執(zhí)行生命周期
commitLayoutEffectOnFiber
調(diào)用了 packages/react-reconciler/src/ReactFiberCommitWork.old.js
路徑下的 commitLifeCycles
函數(shù),里面針對(duì)首次渲染和非首次渲染分別執(zhí)行 componentDidMount
和 componentDidUpdate
生命周期,以及調(diào)用 commitUpdateQueue
去觸發(fā)回調(diào):
// packages/react-reconciler/src/ReactFiberCommitWork.old.js function commitLifeCycles( finishedRoot: FiberRoot, current: Fiber | null, finishedWork: Fiber, committedLanes: Lanes, ): void { switch (finishedWork.tag) { case FunctionComponent: case ForwardRef: case SimpleMemoComponent: // ... case ClassComponent: { const instance = finishedWork.stateNode; if (finishedWork.flags & Update) { if (current === null) { // 首次渲染,執(zhí)行 componentDidMount 生命周期 if ( enableProfilerTimer && enableProfilerCommitHooks && finishedWork.mode & ProfileMode ) { try { startLayoutEffectTimer(); instance.componentDidMount(); } finally { recordLayoutEffectDuration(finishedWork); } } else { instance.componentDidMount(); } } else { // 非首次渲染,執(zhí)行 componentDidUpdate 生命周期 const prevProps = finishedWork.elementType === finishedWork.type ? current.memoizedProps : resolveDefaultProps(finishedWork.type, current.memoizedProps); const prevState = current.memoizedState; // ... if ( enableProfilerTimer && enableProfilerCommitHooks && finishedWork.mode & ProfileMode ) { try { startLayoutEffectTimer(); instance.componentDidUpdate( prevProps, prevState, instance.__reactInternalSnapshotBeforeUpdate, ); } finally { recordLayoutEffectDuration(finishedWork); } } else { instance.componentDidUpdate( prevProps, prevState, instance.__reactInternalSnapshotBeforeUpdate, ); } } } // ... if (updateQueue !== null) { // ... // 執(zhí)行 commitUpdateQueue 處理回調(diào) commitUpdateQueue(finishedWork, updateQueue, instance); } return; } case HostRoot: { const updateQueue: UpdateQueue< *, > | null = (finishedWork.updateQueue: any); if (updateQueue !== null) { // ... // 調(diào)用 commitUpdateQueue 處理 ReactDOM.render 的回調(diào) commitUpdateQueue(finishedWork, updateQueue, instance); } return; } case HostComponent: { const instance: Instance = finishedWork.stateNode; // ... // commitMount 處理 input 標(biāo)簽有 auto-focus 的情況 if (current === null && finishedWork.flags & Update) { const type = finishedWork.type; const props = finishedWork.memoizedProps; commitMount(instance, type, props, finishedWork); } return; } // ... }
處理回調(diào)
處理回調(diào)是在 commitUpdateQueue
中做的,它會(huì)對(duì) finishedQueue 上面的 effects 進(jìn)行遍歷,若有 callback,則執(zhí)行 callback。同時(shí)會(huì)重置 finishedQueue 上面的 effects 為 null:
// packages/react-reconciler/src/ReactUpdateQueue.old.js export function commitUpdateQueue<State>( finishedWork: Fiber, finishedQueue: UpdateQueue<State>, instance: any, ): void { const effects = finishedQueue.effects; // 清空 effects finishedQueue.effects = null; // 對(duì) effect 遍歷 if (effects !== null) { for (let i = 0; i < effects.length; i++) { const effect = effects[i]; const callback = effect.callback; // 執(zhí)行回調(diào) if (callback !== null) { effect.callback = null; callCallback(callback, instance); } } } }
在這之后就是進(jìn)行最后一點(diǎn)變量還原等收尾工作,然后整個(gè) commit 過程就完成了!
總結(jié)
接 render 階段的流程圖,補(bǔ)充上 commit 階段的流程圖,就構(gòu)成了完整的 react 執(zhí)行圖了:
到此這篇關(guān)于React commit源碼分析詳解的文章就介紹到這了,更多相關(guān)React commit內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用 React 和 Threejs 創(chuàng)建一個(gè)VR全景項(xiàng)目的過程詳解
這篇文章主要介紹了使用 React 和 Threejs 創(chuàng)建一個(gè)VR全景項(xiàng)目的過程詳解,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-04-04Shopee在React?Native?架構(gòu)方面的探索及發(fā)展歷程
這篇文章主要介紹了Shopee在React?Native?架構(gòu)方面的探索,本文會(huì)從發(fā)展歷史、架構(gòu)模型、系統(tǒng)設(shè)計(jì)、遷移方案四個(gè)方向逐一介紹我們?nèi)绾我徊讲降貪M足多團(tuán)隊(duì)在復(fù)雜業(yè)務(wù)中的開發(fā)需求,需要的朋友可以參考下2022-07-07React實(shí)現(xiàn)一個(gè)倒計(jì)時(shí)hook組件實(shí)戰(zhàn)示例
這篇文章主要為大家介紹了React實(shí)現(xiàn)一個(gè)倒計(jì)時(shí)hook組件,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02React+TypeScript項(xiàng)目中使用CodeMirror的步驟
CodeMirror被廣泛應(yīng)用于許多Web應(yīng)用程序和開發(fā)工具,之前做需求用到過codeMirror這個(gè)工具,覺得還不錯(cuò),功能很強(qiáng)大,所以記錄一下改工具的基礎(chǔ)用法,對(duì)React+TypeScript項(xiàng)目中使用CodeMirror的步驟感興趣的朋友跟隨小編一起看看吧2023-07-07React Native中ScrollView組件輪播圖與ListView渲染列表組件用法實(shí)例分析
這篇文章主要介紹了React Native中ScrollView組件輪播圖與ListView渲染列表組件用法,結(jié)合實(shí)例形式詳細(xì)分析了ScrollView組件輪播圖與ListView渲染列表組件具體功能、使用方法與操作注意事項(xiàng),需要的朋友可以參考下2020-01-01ReactHook使用useState更新變量后,如何拿到變量更新后的值
這篇文章主要介紹了ReactHook使用useState更新變量后,如何拿到變量更新后的值問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-03-03