您好,登錄后才能下訂單哦!
本篇內(nèi)容介紹了“React首次渲染流程是什么”的有關(guān)知識(shí),在實(shí)際案例的操作過程中,不少人都會(huì)遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!
在開始進(jìn)行源碼分析前,我們先來看幾個(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ā)生的順序
頁面顯示 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. 頁面顯示 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è)例子來闡述下首次渲染的流程:
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)過下面這一系列的操作,會(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)過這一系列的操作以后,會(huì)形成如下的數(shù)據(jù)結(jié)構(gòu):
然后,會(huì)來到:
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ì)變成這樣:
接下來就到了 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
首先看看 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)
,這一句主要是通過 root.current
來創(chuàng)建 workInProgress
。調(diào)用后,數(shù)據(jù)結(jié)構(gòu)成了這樣:
跳過中間的一些語句,我們來到 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
,通過 Fiber
節(jié)點(diǎn)創(chuàng)建真實(shí) DOM 節(jié)點(diǎn)。
這兩個(gè)步驟會(huì)交替的執(zhí)行,其目標(biāo)是:
構(gòu)建出新的 Fiber 樹
與舊 Fiber 比較得到 effect 鏈表(插入、更新、刪除、useEffect 等都會(huì)產(chǎn)生 effect)
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)過 updateHostRoot
后,會(huì)返回 workInProgress.child
作為下一個(gè) workInProgress
,最后的數(shù)據(jù)結(jié)構(gòu)如下(這里先忽略 reconcileChildren
這個(gè)比較復(fù)雜的函數(shù)):
接著會(huì)繼續(xù)進(jìn)行 beginWork
,這次會(huì)來到 mountIndeterminateComponent
(暫時(shí)忽略)??傊?,經(jīng)過不斷的 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 { ... }
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。如果沒有 sibling 了,就返回去完成父 fiber
這里一路走下去最后會(huì)來到 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)如下所示(我們用綠色的圓來表示真實(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
過程,是因?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ì)來到 rootFiber
。不同的是,該節(jié)點(diǎn)的 alternate
并不為空,且該節(jié)點(diǎn) tag
為 HootRoot
,所以 completeWork
時(shí)會(huì)來到這里:
case HostRoot: { ... updateHostContainer(workInProgress); return null; }
updateHostContainer = function (workInProgress: Fiber) { // Noop }
看來幾乎沒有做什么事情,到這我們的 render
階段就結(jié)束了,最后的結(jié)構(gòu)如下所示:
其中藍(lán)色表示是有 effect 的 Fiber 節(jié)點(diǎn),他們組成了一個(gè)鏈表,方便 commit 過程進(jìn)行遍歷。
可以查看 render
過程動(dòng)畫。
commit
大致可分為以下過程:
準(zhǔn)備階段
before mutation 階段(執(zhí)行 DOM 操作前)
mutation 階段(執(zhí)行 DOM 操作)
切換 Fiber Tree
layout 階段(執(zhí)行 DOM 操作后)
收尾階段
do { // 觸發(fā)useEffect回調(diào)與其他同步任務(wù)。由于這些任務(wù)可能觸發(fā)新的渲染,所以這里要一直遍歷執(zhí)行直到?jīng)]有任務(wù) flushPassiveEffects() // 暫時(shí)沒有復(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)來 // 所以這里將有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
。
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
階段主要是執(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 樹插入到了 <div id='root'></div>
之中。
mutation 階段完成后
,會(huì)執(zhí)行:
root.current = finishedWork
完成后, fiberRoot
會(huì)指向 current Fiber
樹。
對(duì)應(yīng)到我們的例子,layout 階段主要是同步執(zhí)行 useLayoutEffect
中的 create
函數(shù),所以這里會(huì)打印 useLayoutEffect ayou
。
現(xiàn)在,我們來分析下文章開始的兩個(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ì)跟隨渲染過程同步執(zhí)行,所以先打印 4
Promise
對(duì)象 then
中的任務(wù)是一個(gè)微任務(wù),所以在 4 后面執(zhí)行,打印 3
console.log('1 message channel')
和 console.log('2 use effect')
都會(huì)在宏任務(wù)中執(zhí)行,執(zhí)行順序就看誰先生成,這里 2 比 1 先,所以先打印 2,再打印 1。
題目二:
點(diǎn)擊 p
標(biāo)簽后,下面事件發(fā)生的順序
頁面顯示 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. 頁面顯示 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ì)先處理,所以頁面先顯示 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。
“React首次渲染流程是什么”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識(shí)可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實(shí)用文章!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。