本篇内容介绍了“React首次渲染流程是什么”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!
在开始进行源码分析前,我们先来看几个题目:
题目一:
渲染下面的组件,打印顺序是什么?
import React from 'react' const channel = new MessageChannel() // onmessage 是一个宏任务 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
题目二:
点击 p
标签后,下面事件发生的顺序
页面显示 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> } // 点击后,下面事件发生的顺序 // 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
你是不是都答对了呢?
我们以下面这个例子来阐述下首次渲染的流程:
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
中导出的,并最后调用了 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) }
首次渲染时,经过下面这一系列的操作,会初始化一些东西:
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 }
经过这一系列的操作以后,会形成如下的数据结构:
然后,会来到:
unbatchedUpdates(() => { // 这里的 children 是 App 对应的这个 ReactElement updateContainer(children, fiberRoot, parentComponent, callback) })
这里 unbatchedUpdates
会设置当前的 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() } } }
然后执行 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 }
这里,会创建一个 update
,然后入队,我们的数据结构会变成这样:
接下来就到了 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 { // 暂时不看 } } else { // 暂时不看 } }
最后走到了 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 }
这里,可以分为两个大的步骤:
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 }
这里首先调用 prepareFreshStack(root, expirationTime)
,这一句主要是通过 root.current
来创建 workInProgress
。调用后,数据结构成了这样:
跳过中间的一些语句,我们来到 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 }
这里又分为两个步骤:
beginWork
,传入当前 Fiber
节点,创建子 Fiber
节点。
completeUnitOfWork
,通过 Fiber
节点创建真实 DOM 节点。
这两个步骤会交替的执行,其目标是:
构建出新的 Fiber 树
与旧 Fiber 比较得到 effect 链表(插入、更新、删除、useEffect 等都会产生 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: // ...省略 // ...省略其他类型 } }
这里因为是 rootFiber
,所以会走到 updateHostRoot
:
function updateHostRoot(current, workInProgress, renderExpirationTime) { // 暂时不看 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 }
经过 updateHostRoot
后,会返回 workInProgress.child
作为下一个 workInProgress
,最后的数据结构如下(这里先忽略 reconcileChildren
这个比较复杂的函数):
接着会继续进行 beginWork
,这次会来到 mountIndeterminateComponent
(暂时忽略)。总之,经过不断的 beginWork
后,我们会得到如下的一个结构:
此时 next
为空,我们会走到:
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 } }
此时这里的 unitOfWork
是 span
对应的 fiber
。从函数头部的注释我们可以大致知道该函数的功能:
// 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. // 尝试去完成当前的工作单元,然后处理下一个 sibling。如果没有 sibling 了,就返回去完成父 fiber
这里一路走下去最后会来到 completeWork
这里 :
case HostComponent: ... // 会调用 ReactDOMComponent.js 中的 createELement 方法创建 span 标签 const instance = createInstance( type, newProps, rootContainerInstance, currentHostContext, workInProgress ) // 将子元素 append 到 instance 中 appendAllChildren(instance, workInProgress, false, false) workInProgress.stateNode = instance;
执行完后,我们的结构如下所示(我们用绿色的圆来表示真实 dom):
此时 next
将会是 null
,我们需要往上找到下一个 completedWork
,即 Name
,因为 Name
是一个 FunctionComponent
,所以在 completeWork
中直接返回了 null
。又因为它有 sibling
,所以会将它的 sibling
赋值给 workInProgress
,并返回对其进行 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
不断交替的执行,当我们执行到 div
的时候,我们的结构如下所示:
之所以要额外的分析 div
的 complete
过程,是因为这个例子方便我们分析 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
这个 fiber
,他的 child
是 Name
,会进入 else if (node.child !== null)
这个条件分支。然后继续下一个循环,此时 node
为 span
这个 fiber
,会进入第一个分支,将 span
对应的 dom
元素插入到 parent
之中。
这样不停的循环,最后会执行到 if (node === workInProgress)
退出,此时所有的子元素都 append 到了 parent
之中:
然后继续 beginWork
和 completeWork
,最后会来到 rootFiber
。不同的是,该节点的 alternate
并不为空,且该节点 tag
为 HootRoot
,所以 completeWork
时会来到这里:
case HostRoot: { ... updateHostContainer(workInProgress); return null; }
updateHostContainer = function (workInProgress: Fiber) { // Noop }
看来几乎没有做什么事情,到这我们的 render
阶段就结束了,最后的结构如下所示:
其中蓝色表示是有 effect 的 Fiber 节点,他们组成了一个链表,方便 commit 过程进行遍历。
可以查看 render
过程动画。
commit
大致可分为以下过程:
准备阶段
before mutation 阶段(执行 DOM 操作前)
mutation 阶段(执行 DOM 操作)
切换 Fiber Tree
layout 阶段(执行 DOM 操作后)
收尾阶段
do { // 触发useEffect回调与其他同步任务。由于这些任务可能触发新的渲染,所以这里要一直遍历执行直到没有任务 flushPassiveEffects() // 暂时没有复现出 rootWithPendingPassiveEffects !== null 的情景 // 首次渲染 rootWithPendingPassiveEffects 为 null } while (rootWithPendingPassiveEffects !== null) // finishedWork 就是正在工作的 rootFiber const finishedWork = root. // 优先级相关暂时不看 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 // 由于每个fiber的effectList只包含他的子孙节点 // 所以根节点如果有effectTag则不会被包含进来 // 所以这里将有effectTag的根节点插入到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 }
准备阶段主要是确定 firstEffect
,我们的例子中就是 Name
这个 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
阶段主要是调用了 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 // 调用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 } }
因为 Name
中 effectTag
包括了 Passive
,所以这里会执行:
scheduleCallback(NormalPriority, () => { flushPassiveEffects() return null })
这里主要是对 useEffect
中的任务进行异步调用,最终会在下个事件循环中执行 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
会执行 useEffect
上次渲染返回的 destroy
方法,commitHookEffectListMount
会执行 useEffect
本次渲染的 create
方法。具体到我们的例子:
因为是首次渲染,所以 destroy
都是 undefined,所以只会打印 useEffect ayou
。
mutation
阶段主要是执行了 commitMutationEffects
这个方法:
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
会走 Update
这个分支,执行 commitWork
,最终会执行到 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); } }
这里会同步执行 useLayoutEffect
上次渲染返回的 destroy
方法,我们的例子里是 undefined。
而 App
会走到 Placement
这个分支,执行 commitPlacement
,这里的主要工作是把整棵 dom 树插入到了 <div id='root'></div>
之中。
mutation 阶段完成后
,会执行:
root.current = finishedWork
完成后, fiberRoot
会指向 current Fiber
树。
对应到我们的例子,layout 阶段主要是同步执行 useLayoutEffect
中的 create
函数,所以这里会打印 useLayoutEffect ayou
。
现在,我们来分析下文章开始的两个题目:
题目一:
渲染下面的组件,打印顺序是什么?
import React from 'react' const channel = new MessageChannel() // onmessage 是一个宏任务 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
中的任务会跟随渲染过程同步执行,所以先打印 4
Promise
对象 then
中的任务是一个微任务,所以在 4 后面执行,打印 3
console.log('1 message channel')
和 console.log('2 use effect')
都会在宏任务中执行,执行顺序就看谁先生成,这里 2 比 1 先,所以先打印 2,再打印 1。
题目二:
点击 p
标签后,下面事件发生的顺序
页面显示 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> } // 点击后,下面事件发生的顺序 // 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 这个 Fiber 位于 effect 链表的首部,在 commitMutations 中会先处理,所以页面先显示 xingzhi。
Name 这个 Fiber 位于 span 之后,所以 useLayoutEffect 中上一次的 destroy 紧接着其执行。打印 useLayoutEffect ayou。
commitLayoutEffects 中执行 useLayoutEffect 这一次的 create。打印 useLayoutEffect xingzhi。
useEffect 在下一个宏任务中执行,先执行上一次的 destroy,再执行这一次的 create。所以先打印 useEffect ayou,再打印 useEffect xingzhi。
“React首次渲染流程是什么”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注亿速云网站,小编将为大家输出更多高质量的实用文章!
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。