通過(guò)示例源碼解讀React首次渲染流程
說(shuō)明
本文結(jié)論均基于 React 16.13.1 得出,若有出入請(qǐng)參考對(duì)應(yīng)版本源碼。參考了 React 技術(shù)揭秘。
題目
在開(kāi)始進(jìn)行源碼分析前,我們先來(lái)看幾個(gè)題目:
題目一:
渲染下面的組件,打印順序是什么?
import React from 'react' const channel = new MessageChannel() // onmessage 是一個(gè)宏任務(wù) channel.port1.onmessage = () => { console.log('1 message channel') } export default function App() { React.useEffect(() => { console.log('2 use effect') }, []) Promise.resolve().then(() => { console.log('3 promise') }) React.useLayoutEffect(() => { console.log('4 use layout effect') channel.port2.postMessage('') }, []) return <div>App</div> }
答案:4 3 2 1
題目二:
點(diǎn)擊 p
標(biāo)簽后,下面事件發(fā)生的順序
- 頁(yè)面顯示 xingzhi
- console.log('useLayoutEffect ayou')
- console.log('useLayoutEffect xingzhi')
- console.log('useEffect ayou')
- console.log('useEffect xingzhi')
import React from 'react' import {useState} from 'react' function Name({name}) { React.useEffect(() => { console.log(`useEffect ${name}`) return () => { console.log(`useEffect destroy ${name}`) } }, [name]) React.useLayoutEffect(() => { console.log(`useLayoutEffect ${name}`) return () => { console.log(`useLayoutEffect destroy ${name}`) } }, [name]) return <span>{name}</span> } // 點(diǎn)擊后,下面事件發(fā)生的順序 // 1. 頁(yè)面顯示 xingzhi // 2. console.log('useLayoutEffect ayou') // 3. console.log('useLayoutEffect xingzhi') // 4. console.log('useEffect ayou') // 5. console.log('useEffect xingzhi') export default function App() { const [name, setName] = useState('ayou') const onClick = React.useCallback(() => setName('xingzhi'), []) return ( <div> <Name name={name} /> <p onClick={onClick}>I am 18</p> </div> ) }
答案:1 2 3 4 5
你是不是都答對(duì)了呢?
首次渲染流程
我們以下面這個(gè)例子來(lái)闡述下首次渲染的流程:
function Name({name}) { React.useEffect(() => { console.log(`useEffect ${name}`) return () => { console.log('useEffect destroy') } }, [name]) React.useLayoutEffect(() => { console.log(`useLayoutEffect ${name}`) return () => { console.log('useLayoutEffect destroy') } }, [name]) return <span>{name}</span> } function Gender() { return <i>Male</i> } export default function App() { const [name, setName] = useState('ayou') return ( <div> <Name name={name} /> <p onClick={() => setName('xingzhi')}>I am 18</p> <Gender /> </div> ) } ... ReactDOM.render(<App />, document.getElementById('root'))
首先,我們看看 render
,它是從 ReactDOMLegacy
中導(dǎo)出的,并最后調(diào)用了 legacyRenderSubtreeIntoContainer
:
function legacyRenderSubtreeIntoContainer( parentComponent: ?React$Component<any, any>, children: ReactNodeList, container: Container, forceHydrate: boolean, callback: ?Function ) { // TODO: Without `any` type, Flow says "Property cannot be accessed on any // member of intersection type." Whyyyyyy. let root: RootType = (container._reactRootContainer: any) let fiberRoot if (!root) { // 首次渲染 root = container._reactRootContainer = legacyCreateRootFromDOMContainer( container, forceHydrate ) fiberRoot = root._internalRoot if (typeof callback === 'function') { const originalCallback = callback callback = function () { const instance = getPublicRootInstance(fiberRoot) originalCallback.call(instance) } } // Initial mount should not be batched. unbatchedUpdates(() => { updateContainer(children, fiberRoot, parentComponent, callback) }) } else { // 更新 fiberRoot = root._internalRoot if (typeof callback === 'function') { const originalCallback = callback callback = function () { const instance = getPublicRootInstance(fiberRoot) originalCallback.call(instance) } } updateContainer(children, fiberRoot, parentComponent, callback) } return getPublicRootInstance(fiberRoot) }
首次渲染時(shí),經(jīng)過(guò)下面這一系列的操作,會(huì)初始化一些東西:
ReactDOMLegacy.js function legacyCreateRootFromDOMContainer( container: Container, forceHydrate: boolean ): RootType { ... return createLegacyRoot( container, shouldHydrate ? { hydrate: true, } : undefined ) } ReactDOMRoot.js function createLegacyRoot( container: Container, options?: RootOptions, ): RootType { return new ReactDOMBlockingRoot(container, LegacyRoot, options); } function ReactDOMBlockingRoot( container: Container, tag: RootTag, options: void | RootOptions, ) { this._internalRoot = createRootImpl(container, tag, options); } function createRootImpl( container: Container, tag: RootTag, options: void | RootOptions, ) { ... const root = createContainer(container, tag, hydrate, hydrationCallbacks) ... } ReactFiberReconciler.old.js function createContainer( containerInfo: Container, tag: RootTag, hydrate: boolean, hydrationCallbacks: null | SuspenseHydrationCallbacks, ): OpaqueRoot { return createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks); } ReactFiberRoot.old.js function createFiberRoot( containerInfo: any, tag: RootTag, hydrate: boolean, hydrationCallbacks: null | SuspenseHydrationCallbacks, ): FiberRoot { ... const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any) const uninitializedFiber = createHostRootFiber(tag) root.current = uninitializedFiber uninitializedFiber.stateNode = root initializeUpdateQueue(uninitializedFiber) return root }
經(jīng)過(guò)這一系列的操作以后,會(huì)形成如下的數(shù)據(jù)結(jié)構(gòu):
然后,會(huì)來(lái)到:
unbatchedUpdates(() => { // 這里的 children 是 App 對(duì)應(yīng)的這個(gè) ReactElement updateContainer(children, fiberRoot, parentComponent, callback) })
這里 unbatchedUpdates
會(huì)設(shè)置當(dāng)前的 executionContext
:
export function unbatchedUpdates<A, R>(fn: (a: A) => R, a: A): R { const prevExecutionContext = executionContext // 去掉 BatchedContext executionContext &= ~BatchedContext // 加上 LegacyUnbatchedContext executionContext |= LegacyUnbatchedContext try { return fn(a) } finally { executionContext = prevExecutionContext if (executionContext === NoContext) { // Flush the immediate callbacks that were scheduled during this batch flushSyncCallbackQueue() } } }
然后執(zhí)行 updateContainer
:
export function updateContainer( element: ReactNodeList, container: OpaqueRoot, parentComponent: ?React$Component<any, any>, callback: ?Function ): ExpirationTime { const current = container.current const currentTime = requestCurrentTimeForUpdate() const suspenseConfig = requestCurrentSuspenseConfig() const expirationTime = computeExpirationForFiber( currentTime, current, suspenseConfig ) const context = getContextForSubtree(parentComponent) if (container.context === null) { container.context = context } else { container.pendingContext = context } const update = createUpdate(expirationTime, suspenseConfig) // Caution: React DevTools currently depends on this property // being called "element". update.payload = {element} callback = callback === undefined ? null : callback if (callback !== null) { update.callback = callback } enqueueUpdate(current, update) scheduleUpdateOnFiber(current, expirationTime) return expirationTime }
這里,會(huì)創(chuàng)建一個(gè) update
,然后入隊(duì),我們的數(shù)據(jù)結(jié)構(gòu)會(huì)變成這樣:
接下來(lái)就到了 scheduleUpdateOnFiber
:
export function scheduleUpdateOnFiber( fiber: Fiber, expirationTime: ExpirationTime ) { checkForNestedUpdates() warnAboutRenderPhaseUpdatesInDEV(fiber) const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime) if (root === null) { warnAboutUpdateOnUnmountedFiberInDEV(fiber) return } // TODO: computeExpirationForFiber also reads the priority. Pass the // priority as an argument to that function and this one. const priorityLevel = getCurrentPriorityLevel() if (expirationTime === Sync) { if ( // Check if we're inside unbatchedUpdates (executionContext & LegacyUnbatchedContext) !== NoContext && // Check if we're not already rendering (executionContext & (RenderContext | CommitContext)) === NoContext ) { // Register pending interactions on the root to avoid losing traced interaction data. schedulePendingInteractions(root, expirationTime) // This is a legacy edge case. The initial mount of a ReactDOM.render-ed // root inside of batchedUpdates should be synchronous, but layout updates // should be deferred until the end of the batch. performSyncWorkOnRoot(root) } else { // 暫時(shí)不看 } } else { // 暫時(shí)不看 } }
最后走到了 performSyncWorkOnRoot
:
function performSyncWorkOnRoot(root) { invariant( (executionContext & (RenderContext | CommitContext)) === NoContext, 'Should not already be working.' ) flushPassiveEffects() const lastExpiredTime = root.lastExpiredTime let expirationTime if (lastExpiredTime !== NoWork) { ... } else { // There's no expired work. This must be a new, synchronous render. expirationTime = Sync } let exitStatus = renderRootSync(root, expirationTime) ... const finishedWork: Fiber = (root.current.alternate: any); root.finishedWork = finishedWork; root.finishedExpirationTime = expirationTime; root.nextKnownPendingLevel = getRemainingExpirationTime(finishedWork); commitRoot(root); return null }
這里,可以分為兩個(gè)大的步驟:
render
commit
render
首先看看 renderRootSync
:
function renderRootSync(root, expirationTime) { const prevExecutionContext = executionContext executionContext |= RenderContext const prevDispatcher = pushDispatcher(root) // If the root or expiration time have changed, throw out the existing stack // and prepare a fresh one. Otherwise we'll continue where we left off. if (root !== workInProgressRoot || expirationTime !== renderExpirationTime) { // 主要是給 workInProgress 賦值 prepareFreshStack(root, expirationTime) startWorkOnPendingInteractions(root, expirationTime) } const prevInteractions = pushInteractions(root) do { try { 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 return workInProgressRootExitStatus }
這里首先調(diào)用 prepareFreshStack(root, expirationTime)
,這一句主要是通過(guò) root.current
來(lái)創(chuàng)建 workInProgress
。調(diào)用后,數(shù)據(jù)結(jié)構(gòu)成了這樣:
跳過(guò)中間的一些語(yǔ)句,我們來(lái)到 workLoopSync
:
function workLoopSync() { // Already timed out, so perform work without checking if we need to yield. while (workInProgress !== null) { performUnitOfWork(workInProgress) } }
function performUnitOfWork(unitOfWork: Fiber): void { // The current, flushed, state of this fiber is the alternate. Ideally // nothing should rely on this, but relying on it here means that we don't // need an additional field on the work in progress. const current = unitOfWork.alternate setCurrentDebugFiberInDEV(unitOfWork) let next if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) { startProfilerTimer(unitOfWork) next = beginWork(current, unitOfWork, renderExpirationTime) stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true) } else { next = beginWork(current, unitOfWork, renderExpirationTime) } resetCurrentDebugFiberInDEV() unitOfWork.memoizedProps = unitOfWork.pendingProps if (next === null) { // If this doesn't spawn new work, complete the current work. completeUnitOfWork(unitOfWork) } else { workInProgress = next } ReactCurrentOwner.current = null }
這里又分為兩個(gè)步驟:
beginWork
,傳入當(dāng)前Fiber
節(jié)點(diǎn),創(chuàng)建子Fiber
節(jié)點(diǎn)。completeUnitOfWork
,通過(guò)Fiber
節(jié)點(diǎn)創(chuàng)建真實(shí) DOM 節(jié)點(diǎn)。
這兩個(gè)步驟會(huì)交替的執(zhí)行,其目標(biāo)是:
- 構(gòu)建出新的 Fiber 樹(shù)
- 與舊 Fiber 比較得到 effect 鏈表(插入、更新、刪除、useEffect 等都會(huì)產(chǎn)生 effect)
beginWork
function beginWork( current: Fiber | null, workInProgress: Fiber, renderExpirationTime: ExpirationTime ): Fiber | null { const updateExpirationTime = workInProgress.expirationTime if (current !== null) { const oldProps = current.memoizedProps const newProps = workInProgress.pendingProps if ( oldProps !== newProps || hasLegacyContextChanged() || // Force a re-render if the implementation changed due to hot reload: (__DEV__ ? workInProgress.type !== current.type : false) ) { // 略 } else if (updateExpirationTime < renderExpirationTime) { // 略 } else { // An update was scheduled on this fiber, but there are no new props // nor legacy context. Set this to false. If an update queue or context // consumer produces a changed value, it will set this to true. Otherwise, // the component will assume the children have not changed and bail out. didReceiveUpdate = false } } else { didReceiveUpdate = false } // Before entering the begin phase, clear pending update priority. // TODO: This assumes that we're about to evaluate the component and process // the update queue. However, there's an exception: SimpleMemoComponent // sometimes bails out later in the begin phase. This indicates that we should // move this assignment out of the common path and into each branch. workInProgress.expirationTime = NoWork switch (workInProgress.tag) { case IndeterminateComponent: // ...省略 case LazyComponent: // ...省略 case FunctionComponent: // ...省略 case ClassComponent: // ...省略 case HostRoot: return updateHostRoot(current, workInProgress, renderExpirationTime) case HostComponent: // ...省略 case HostText: // ...省略 // ...省略其他類型 } }
這里因?yàn)槭?rootFiber
,所以會(huì)走到 updateHostRoot
:
function updateHostRoot(current, workInProgress, renderExpirationTime) { // 暫時(shí)不看 pushHostRootContext(workInProgress) const updateQueue = workInProgress.updateQueue const nextProps = workInProgress.pendingProps const prevState = workInProgress.memoizedState const prevChildren = prevState !== null ? prevState.element : null cloneUpdateQueue(current, workInProgress) processUpdateQueue(workInProgress, nextProps, null, renderExpirationTime) const nextState = workInProgress.memoizedState // Caution: React DevTools currently depends on this property // being called "element". const nextChildren = nextState.element if (nextChildren === prevChildren) { // 省略 } const root: FiberRoot = workInProgress.stateNode if (root.hydrate && enterHydrationState(workInProgress)) { // 省略 } else { // 給 rootFiber 生成子 fiber reconcileChildren( current, workInProgress, nextChildren, renderExpirationTime ) resetHydrationState() } return workInProgress.child }
經(jīng)過(guò) updateHostRoot
后,會(huì)返回 workInProgress.child
作為下一個(gè) workInProgress
,最后的數(shù)據(jù)結(jié)構(gòu)如下(這里先忽略 reconcileChildren
這個(gè)比較復(fù)雜的函數(shù)):
接著會(huì)繼續(xù)進(jìn)行 beginWork
,這次會(huì)來(lái)到 mountIndeterminateComponent
(暫時(shí)忽略)??傊?jīng)過(guò)不斷的 beginWork
后,我們會(huì)得到如下的一個(gè)結(jié)構(gòu):
此時(shí) next
為空,我們會(huì)走到:
if (next === null) { // If this doesn't spawn new work, complete the current work. completeUnitOfWork(unitOfWork) } else { ... }
completeUnitOfWork
function completeUnitOfWork(unitOfWork: Fiber): void { // Attempt to complete the current unit of work, then move to the next // sibling. If there are no more siblings, return to the parent fiber. let completedWork = unitOfWork do { // The current, flushed, state of this fiber is the alternate. Ideally // nothing should rely on this, but relying on it here means that we don't // need an additional field on the work in progress. const current = completedWork.alternate const returnFiber = completedWork.return // Check if the work completed or if something threw. if ((completedWork.effectTag & Incomplete) === NoEffect) { setCurrentDebugFiberInDEV(completedWork) let next if ( !enableProfilerTimer || (completedWork.mode & ProfileMode) === NoMode ) { next = completeWork(current, completedWork, renderExpirationTime) } else { startProfilerTimer(completedWork) next = completeWork(current, completedWork, renderExpirationTime) // Update render duration assuming we didn't error. stopProfilerTimerIfRunningAndRecordDelta(completedWork, false) } resetCurrentDebugFiberInDEV() resetChildExpirationTime(completedWork) if (next !== null) { // Completing this fiber spawned new work. Work on that next. workInProgress = next return } if ( returnFiber !== null && // Do not append effects to parents if a sibling failed to complete (returnFiber.effectTag & Incomplete) === NoEffect ) { // Append all the effects of the subtree and this fiber onto the effect // list of the parent. The completion order of the children affects the // side-effect order. 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 } // If this fiber had side-effects, we append it AFTER the children's // side-effects. We can perform certain side-effects earlier if needed, // by doing multiple passes over the effect list. We don't want to // schedule our own side-effect on our own list because if end up // reusing children we'll schedule this effect onto itself since we're // at the end. const effectTag = completedWork.effectTag // Skip both NoWork and PerformedWork tags when creating the effect // list. PerformedWork effect is read by React DevTools but shouldn't be // committed. 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, renderExpirationTime) // 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) { // If there is more work to do in this returnFiber, do that next. workInProgress = siblingFiber return } // Otherwise, return to the parent completedWork = returnFiber // Update the next thing we're working on in case something throws. workInProgress = completedWork } while (completedWork !== null) // We've reached the root. if (workInProgressRootExitStatus === RootIncomplete) { workInProgressRootExitStatus = RootCompleted } }
此時(shí)這里的 unitOfWork
是 span
對(duì)應(yīng)的 fiber
。從函數(shù)頭部的注釋我們可以大致知道該函數(shù)的功能:
// Attempt to complete the current unit of work, then move to the next // sibling. If there are no more siblings, return to the parent fiber. // 嘗試去完成當(dāng)前的工作單元,然后處理下一個(gè) sibling。如果沒(méi)有 sibling 了,就返回去完成父 fiber
這里一路走下去最后會(huì)來(lái)到 completeWork
這里 :
case HostComponent: ... // 會(huì)調(diào)用 ReactDOMComponent.js 中的 createELement 方法創(chuàng)建 span 標(biāo)簽 const instance = createInstance( type, newProps, rootContainerInstance, currentHostContext, workInProgress ) // 將子元素 append 到 instance 中 appendAllChildren(instance, workInProgress, false, false) workInProgress.stateNode = instance;
執(zhí)行完后,我們的結(jié)構(gòu)如下所示(我們用綠色的圓來(lái)表示真實(shí) dom):
此時(shí) next
將會(huì)是 null
,我們需要往上找到下一個(gè) completedWork
,即 Name
,因?yàn)?Name
是一個(gè) FunctionComponent
,所以在 completeWork
中直接返回了 null
。又因?yàn)樗?sibling
,所以會(huì)將它的 sibling
賦值給 workInProgress
,并返回對(duì)其進(jìn)行 beginWork
。
const siblingFiber = completedWork.sibling if (siblingFiber !== null) { // If there is more work to do in this returnFiber, do that next. // workInProgress 更新為 sibling workInProgress = siblingFiber // 直接返回,回到了 performUnitOfWork return }
function performUnitOfWork(unitOfWork: Fiber): void { ... if (next === null) { // If this doesn't spawn new work, complete the current work. // 上面的代碼回到了這里 completeUnitOfWork(unitOfWork) } else { workInProgress = next } ReactCurrentOwner.current = null }
這樣 beginWork
和 completeWork
不斷交替的執(zhí)行,當(dāng)我們執(zhí)行到 div
的時(shí)候,我們的結(jié)構(gòu)如下所示:
之所以要額外的分析 div
的 complete
過(guò)程,是因?yàn)檫@個(gè)例子方便我們分析 appendAllChildren
:
appendAllChildren = function ( parent: Instance, workInProgress: Fiber, needsVisibilityToggle: boolean, isHidden: boolean ) { // 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 } }
由于 workInProgress
指向 div
這個(gè) fiber
,他的 child
是 Name
,會(huì)進(jìn)入 else if (node.child !== null)
這個(gè)條件分支。然后繼續(xù)下一個(gè)循環(huán),此時(shí) node
為 span
這個(gè) fiber
,會(huì)進(jìn)入第一個(gè)分支,將 span
對(duì)應(yīng)的 dom
元素插入到 parent
之中。
這樣不停的循環(huán),最后會(huì)執(zhí)行到 if (node === workInProgress)
退出,此時(shí)所有的子元素都 append 到了 parent
之中:
然后繼續(xù) beginWork
和 completeWork
,最后會(huì)來(lái)到 rootFiber
。不同的是,該節(jié)點(diǎn)的 alternate
并不為空,且該節(jié)點(diǎn) tag
為 HootRoot
,所以 completeWork
時(shí)會(huì)來(lái)到這里:
case HostRoot: { ... updateHostContainer(workInProgress); return null; }
updateHostContainer = function (workInProgress: Fiber) { // Noop }
看來(lái)幾乎沒(méi)有做什么事情,到這我們的 render
階段就結(jié)束了,最后的結(jié)構(gòu)如下所示:
其中藍(lán)色表示是有 effect 的 Fiber 節(jié)點(diǎn),他們組成了一個(gè)鏈表,方便 commit 過(guò)程進(jìn)行遍歷。
可以查看 render
過(guò)程動(dòng)畫(huà)。
commit
commit
大致可分為以下過(guò)程:
- 準(zhǔn)備階段
- before mutation 階段(執(zhí)行 DOM 操作前)
- mutation 階段(執(zhí)行 DOM 操作)
- 切換 Fiber Tree
- layout 階段(執(zhí)行 DOM 操作后)
- 收尾階段
準(zhǔn)備階段
do { // 觸發(fā)useEffect回調(diào)與其他同步任務(wù)。由于這些任務(wù)可能觸發(fā)新的渲染,所以這里要一直遍歷執(zhí)行直到?jīng)]有任務(wù) flushPassiveEffects() // 暫時(shí)沒(méi)有復(fù)現(xiàn)出 rootWithPendingPassiveEffects !== null 的情景 // 首次渲染 rootWithPendingPassiveEffects 為 null } while (rootWithPendingPassiveEffects !== null) // finishedWork 就是正在工作的 rootFiber const finishedWork = root. // 優(yōu)先級(jí)相關(guān)暫時(shí)不看 const expirationTime = root.finishedExpirationTime if (finishedWork === null) { return null } root.finishedWork = null root.finishedExpirationTime = NoWork root.callbackNode = null root.callbackExpirationTime = NoWork root.callbackPriority_old = NoPriority const remainingExpirationTimeBeforeCommit = getRemainingExpirationTime( finishedWork ) markRootFinishedAtTime( root, expirationTime, remainingExpirationTimeBeforeCommit ) if (rootsWithPendingDiscreteUpdates !== null) { const lastDiscreteTime = rootsWithPendingDiscreteUpdates.get(root) if ( lastDiscreteTime !== undefined && remainingExpirationTimeBeforeCommit < lastDiscreteTime ) { rootsWithPendingDiscreteUpdates.delete(root) } } if (root === workInProgressRoot) { workInProgressRoot = null workInProgress = null renderExpirationTime = NoWork } else { } // 將effectList賦值給firstEffect // 由于每個(gè)fiber的effectList只包含他的子孫節(jié)點(diǎn) // 所以根節(jié)點(diǎn)如果有effectTag則不會(huì)被包含進(jìn)來(lái) // 所以這里將有effectTag的根節(jié)點(diǎn)插入到effectList尾部 // 這樣才能保證有effect的fiber都在effectList中 let firstEffect if (finishedWork.effectTag > PerformedWork) { if (finishedWork.lastEffect !== null) { finishedWork.lastEffect.nextEffect = finishedWork firstEffect = finishedWork.firstEffect } else { firstEffect = finishedWork } } else { firstEffect = finishedWork.firstEffect }
準(zhǔn)備階段主要是確定 firstEffect
,我們的例子中就是 Name
這個(gè) fiber
。
before mutation 階段
const prevExecutionContext = executionContext executionContext |= CommitContext const prevInteractions = pushInteractions(root) // Reset this to null before calling lifecycles ReactCurrentOwner.current = null // The commit phase is broken into several sub-phases. We do a separate pass // of the effect list for each phase: all mutation effects come before all // layout effects, and so on. // The first phase a "before mutation" phase. We use this phase to read the // state of the host tree right before we mutate it. This is where // getSnapshotBeforeUpdate is called. focusedInstanceHandle = prepareForCommit(root.containerInfo) shouldFireAfterActiveInstanceBlur = false 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) // 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() }
before mutation
階段主要是調(diào)用了 commitBeforeMutationEffects
方法:
function commitBeforeMutationEffects() { while (nextEffect !== null) { if ( !shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null && isFiberHiddenOrDeletedAndContains(nextEffect, focusedInstanceHandle) ) { shouldFireAfterActiveInstanceBlur = true beforeActiveInstanceBlur() } const effectTag = nextEffect.effectTag if ((effectTag & Snapshot) !== NoEffect) { setCurrentDebugFiberInDEV(nextEffect) const current = nextEffect.alternate // 調(diào)用getSnapshotBeforeUpdate commitBeforeMutationEffectOnFiber(current, nextEffect) resetCurrentDebugFiberInDEV() } if ((effectTag & Passive) !== NoEffect) { // If there are passive effects, schedule a callback to flush at // the earliest opportunity. if (!rootDoesHavePassiveEffects) { rootDoesHavePassiveEffects = true scheduleCallback(NormalPriority, () => { flushPassiveEffects() return null }) } } nextEffect = nextEffect.nextEffect } }
因?yàn)?Name
中 effectTag
包括了 Passive
,所以這里會(huì)執(zhí)行:
scheduleCallback(NormalPriority, () => { flushPassiveEffects() return null })
這里主要是對(duì) useEffect
中的任務(wù)進(jìn)行異步調(diào)用,最終會(huì)在下個(gè)事件循環(huán)中執(zhí)行 commitPassiveHookEffects
:
export function commitPassiveHookEffects(finishedWork: Fiber): void { if ((finishedWork.effectTag & Passive) !== NoEffect) { switch (finishedWork.tag) { case FunctionComponent: case ForwardRef: case SimpleMemoComponent: case Block: { if ( enableProfilerTimer && enableProfilerCommitHooks && finishedWork.mode & ProfileMode ) { try { startPassiveEffectTimer(); commitHookEffectListUnmount( HookPassive | HookHasEffect, finishedWork, ); commitHookEffectListMount( HookPassive | HookHasEffect, finishedWork, ); } finally { recordPassiveEffectDuration(finishedWork); } } else { commitHookEffectListUnmount( HookPassive | HookHasEffect, finishedWork, ); commitHookEffectListMount(HookPassive | HookHasEffect, finishedWork); } break; } default: break; } } } function commitHookEffectListUnmount(tag: number, finishedWork: Fiber) { const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; if (lastEffect !== null) { const firstEffect = lastEffect.next; let effect = firstEffect; do { if ((effect.tag & tag) === tag) { // Unmount const destroy = effect.destroy; effect.destroy = undefined; if (destroy !== undefined) { destroy(); } } effect = effect.next; } while (effect !== firstEffect); } } function commitHookEffectListMount(tag: number, finishedWork: Fiber) { const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; if (lastEffect !== null) { const firstEffect = lastEffect.next; let effect = firstEffect; do { if ((effect.tag & tag) === tag) { // Mount const create = effect.create; effect.destroy = create(); ... } effect = effect.next; } while (effect !== firstEffect); } }
其中,commitHookEffectListUnmount
會(huì)執(zhí)行 useEffect
上次渲染返回的 destroy
方法,commitHookEffectListMount
會(huì)執(zhí)行 useEffect
本次渲染的 create
方法。具體到我們的例子:
因?yàn)槭鞘状武秩?,所?destroy
都是 undefined,所以只會(huì)打印 useEffect ayou
。
mutation 階段
mutation
階段主要是執(zhí)行了 commitMutationEffects
這個(gè)方法:
function commitMutationEffects(root: FiberRoot, renderPriorityLevel) { // TODO: Should probably move the bulk of this function to commitWork. while (nextEffect !== null) { setCurrentDebugFiberInDEV(nextEffect) const effectTag = nextEffect.effectTag ... // 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; } } } }
其中,Name
會(huì)走 Update
這個(gè)分支,執(zhí)行 commitWork
,最終會(huì)執(zhí)行到 commitHookEffectListUnmount
:
function commitHookEffectListUnmount(tag: number, finishedWork: Fiber) { const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; if (lastEffect !== null) { const firstEffect = lastEffect.next; let effect = firstEffect; do { if ((effect.tag & tag) === tag) { // Unmount const destroy = effect.destroy; effect.destroy = undefined; if (destroy !== undefined) { destroy(); } } effect = effect.next; } while (effect !== firstEffect); } }
這里會(huì)同步執(zhí)行 useLayoutEffect
上次渲染返回的 destroy
方法,我們的例子里是 undefined。
而 App
會(huì)走到 Placement
這個(gè)分支,執(zhí)行 commitPlacement
,這里的主要工作是把整棵 dom 樹(shù)插入到了 <div id='root'></div>
之中。
切換 Fiber Tree
mutation 階段完成后
,會(huì)執(zhí)行:
root.current = finishedWork
完成后, fiberRoot
會(huì)指向 current Fiber
樹(shù)。
layout 階段
對(duì)應(yīng)到我們的例子,layout 階段主要是同步執(zhí)行 useLayoutEffect
中的 create
函數(shù),所以這里會(huì)打印 useLayoutEffect ayou
。
題目解析
現(xiàn)在,我們來(lái)分析下文章開(kāi)始的兩個(gè)題目:
題目一:
渲染下面的組件,打印順序是什么?
import React from 'react' const channel = new MessageChannel() // onmessage 是一個(gè)宏任務(wù) channel.port1.onmessage = () => { console.log('1 message channel') } export default function App() { React.useEffect(() => { console.log('2 use effect') }, []) Promise.resolve().then(() => { console.log('3 promise') }) React.useLayoutEffect(() => { console.log('4 use layout effect') channel.port2.postMessage('') }, []) return <div>App</div> }
解析:
useLayoutEffect
中的任務(wù)會(huì)跟隨渲染過(guò)程同步執(zhí)行,所以先打印 4Promise
對(duì)象then
中的任務(wù)是一個(gè)微任務(wù),所以在 4 后面執(zhí)行,打印 3console.log('1 message channel')
和console.log('2 use effect')
都會(huì)在宏任務(wù)中執(zhí)行,執(zhí)行順序就看誰(shuí)先生成,這里 2 比 1 先,所以先打印 2,再打印 1。
題目二:
點(diǎn)擊 p
標(biāo)簽后,下面事件發(fā)生的順序
- 頁(yè)面顯示 xingzhi
- console.log('useLayoutEffect ayou')
- console.log('useLayoutEffect xingzhi')
- console.log('useEffect ayou')
- console.log('useEffect xingzhi')
import React from 'react' import {useState} from 'react' function Name({name}) { React.useEffect(() => { console.log(`useEffect ${name}`) return () => { console.log(`useEffect destroy ${name}`) } }, [name]) React.useLayoutEffect(() => { console.log(`useLayoutEffect ${name}`) return () => { console.log(`useLayoutEffect destroy ${name}`) } }, [name]) return <span>{name}</span> } // 點(diǎn)擊后,下面事件發(fā)生的順序 // 1. 頁(yè)面顯示 xingzhi // 2. console.log('useLayoutEffect destroy ayou') // 3. console.log(`useLayoutEffect xingzhi`) // 4. console.log('useEffect destroy ayou') // 5. console.log(`useEffect xingzhi`) export default function App() { const [name, setName] = useState('ayou') const onClick = React.useCallback(() => setName('xingzhi'), []) return ( <div> <Name name={name} /> <p onClick={onClick}>I am 18</p> </div> ) }
解析:
- span 這個(gè) Fiber 位于 effect 鏈表的首部,在 commitMutations 中會(huì)先處理,所以頁(yè)面先顯示 xingzhi。
- Name 這個(gè) Fiber 位于 span 之后,所以 useLayoutEffect 中上一次的 destroy 緊接著其執(zhí)行。打印 useLayoutEffect ayou。
- commitLayoutEffects 中執(zhí)行 useLayoutEffect 這一次的 create。打印 useLayoutEffect xingzhi。
- useEffect 在下一個(gè)宏任務(wù)中執(zhí)行,先執(zhí)行上一次的 destroy,再執(zhí)行這一次的 create。所以先打印 useEffect ayou,再打印 useEffect xingzhi。
總結(jié)
本文大部分內(nèi)容都參考自 React 技術(shù)揭秘,通過(guò)舉例及畫(huà)圖走讀了一遍首次渲染流程,加深了下自己的理解。
以上就是通過(guò)示例源碼解讀React首次渲染流程的詳細(xì)內(nèi)容,更多關(guān)于React首次渲染流程的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
react?app?rewrited替代品craco使用示例
這篇文章主要為大家介紹了react?app?rewrited替代品craco使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11react+axios實(shí)現(xiàn)github搜索用戶功能(示例代碼)
這篇文章主要介紹了react+axios實(shí)現(xiàn)搜索github用戶功能,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-09-09React Native可復(fù)用 UI分離布局組件和狀態(tài)組件技巧
這篇文章主要為大家介紹了React Native可復(fù)用 UI分離布局組件和狀態(tài)組件使用技巧,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09React+高德地圖實(shí)時(shí)獲取經(jīng)緯度,定位地址
思路其實(shí)沒(méi)有那么復(fù)雜,把地圖想成一個(gè)盒子容器,地圖中心點(diǎn)想成盒子中心點(diǎn);扎點(diǎn)在【地圖中心點(diǎn)】不會(huì)動(dòng),當(dāng)移動(dòng)地圖時(shí),去獲取【地圖中心點(diǎn)】經(jīng)緯度,設(shè)置某個(gè)位置的時(shí)候,將經(jīng)緯度設(shè)置為【地圖中心點(diǎn)】即可2021-06-06詳解使用WebPack搭建React開(kāi)發(fā)環(huán)境
這篇文章主要介紹了詳解使用WebPack搭建React開(kāi)發(fā)環(huán)境,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08使用react-dnd編寫(xiě)一個(gè)可拖拽排列的list
這篇文章主要為大家詳細(xì)介紹了如何使用react-dnd編寫(xiě)一個(gè)可拖拽排列的list,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-03-03