通过示例源码解读React首次渲染流程

说明

本文结论均基于 React 16.13.1 得出,若有出入请参考对应版本源码。参考了 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 &amp; (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

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&lt;Interaction&gt;))
 }
 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 &amp;&amp; (unitOfWork.mode &amp; 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)

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 &lt; 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 &amp;&amp; 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 {
 ...
}

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
 }
}

此时这里的 unitOfWorkspan 对应的 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
}

这样 beginWorkcompleteWork 不断交替的执行,当我们执行到 div 的时候,我们的结构如下所示:

之所以要额外的分析 divcomplete 过程,是因为这个例子方便我们分析 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,他的 childName,会进入 else if (node.child !== null) 这个条件分支。然后继续下一个循环,此时 nodespan 这个 fiber,会进入第一个分支,将 span 对应的 dom 元素插入到 parent 之中。

这样不停的循环,最后会执行到 if (node === workInProgress) 退出,此时所有的子元素都 append 到了 parent 之中:

然后继续 beginWorkcompleteWork,最后会来到 rootFiber。不同的是,该节点的 alternate 并不为空,且该节点 tagHootRoot,所以 completeWork 时会来到这里:

case HostRoot: {
 ...
 updateHostContainer(workInProgress);
 return null;
}
updateHostContainer = function (workInProgress: Fiber) {
 // Noop
}

看来几乎没有做什么事情,到这我们的 render 阶段就结束了,最后的结构如下所示:

其中蓝色表示是有 effect 的 Fiber 节点,他们组成了一个链表,方便 commit 过程进行遍历。

可以查看 render 过程动画。

commit

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

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 阶段主要是调用了 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
 }
}

因为 NameeffectTag 包括了 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 阶段

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> 之中。

切换 Fiber Tree

mutation 阶段完成后,会执行:

root.current = finishedWork

完成后, fiberRoot 会指向 current Fiber 树。

layout 阶段

对应到我们的例子,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 技术揭秘,通过举例及画图走读了一遍首次渲染流程,加深了下自己的理解。

作者:Aaaaaaaaaaayou

%s 个评论

要回复文章请先登录注册