温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

React首次渲染流程是什么

发布时间:2023-03-27 13:54:50 来源:亿速云 阅读:148 作者:iii 栏目:开发技术

本篇内容介绍了“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
    }

    经过这一系列的操作以后,会形成如下的数据结构:

    React首次渲染流程是什么

    然后,会来到:

    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,然后入队,我们的数据结构会变成这样:

    React首次渲染流程是什么

    接下来就到了 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。调用后,数据结构成了这样:

    React首次渲染流程是什么

    跳过中间的一些语句,我们来到 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 这个比较复杂的函数):

    React首次渲染流程是什么

    接着会继续进行 beginWork,这次会来到 mountIndeterminateComponent (暂时忽略)。总之,经过不断的 beginWork 后,我们会得到如下的一个结构:

    React首次渲染流程是什么

    此时 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):

    React首次渲染流程是什么

    此时 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 的时候,我们的结构如下所示:

    React首次渲染流程是什么

    之所以要额外的分析 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 之中:

    React首次渲染流程是什么

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

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

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

    React首次渲染流程是什么

    其中蓝色表示是有 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 方法。具体到我们的例子:

    React首次渲染流程是什么

    因为是首次渲染,所以 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 树。

    React首次渲染流程是什么

    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首次渲染流程是什么”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注亿速云网站,小编将为大家输出更多高质量的实用文章!

    向AI问一下细节

    免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

    AI