溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

React首次渲染流程是什么

發(fā)布時(shí)間:2023-03-27 13:54:50 來源:億速云 閱讀:157 作者:iii 欄目:開發(fā)技術(shù)

本篇內(nèi)容介紹了“React首次渲染流程是什么”的有關(guān)知識(shí),在實(shí)際案例的操作過程中,不少人都會(huì)遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!

    題目

    在開始進(jìn)行源碼分析前,我們先來看幾個(gè)題目:

    題目一:

    渲染下面的組件,打印順序是什么?

    import React from 'react'
    const channel = new MessageChannel()
    // onmessage 是一個(gè)宏任務(wù)
    channel.port1.onmessage = () => {
      console.log('1 message channel')
    }
    export default function App() {
      React.useEffect(() => {
        console.log('2 use effect')
      }, [])
      Promise.resolve().then(() => {
        console.log('3 promise')
      })
      React.useLayoutEffect(() => {
        console.log('4 use layout effect')
        channel.port2.postMessage('')
      }, [])
      return <div>App</div>
    }

    答案:4 3 2 1

    題目二:

    點(diǎn)擊 p 標(biāo)簽后,下面事件發(fā)生的順序

    • 頁面顯示 xingzhi

    • console.log('useLayoutEffect ayou')

    • console.log('useLayoutEffect xingzhi')

    • console.log('useEffect ayou')

    • console.log('useEffect xingzhi')

    import React from 'react'
    import {useState} from 'react'
    function Name({name}) {
      React.useEffect(() => {
        console.log(`useEffect ${name}`)
        return () => {
          console.log(`useEffect destroy ${name}`)
        }
      }, [name])
      React.useLayoutEffect(() => {
        console.log(`useLayoutEffect ${name}`)
        return () => {
          console.log(`useLayoutEffect destroy ${name}`)
        }
      }, [name])
      return <span>{name}</span>
    }
    // 點(diǎn)擊后,下面事件發(fā)生的順序
    // 1. 頁面顯示 xingzhi
    // 2. console.log('useLayoutEffect ayou')
    // 3. console.log('useLayoutEffect xingzhi')
    // 4. console.log('useEffect ayou')
    // 5. console.log('useEffect xingzhi')
    export default function App() {
      const [name, setName] = useState('ayou')
      const onClick = React.useCallback(() => setName('xingzhi'), [])
      return (
        <div>
          <Name name={name} />
          <p onClick={onClick}>I am 18</p>
        </div>
      )
    }

    答案:1 2 3 4 5

    你是不是都答對(duì)了呢?

    首次渲染流程

    我們以下面這個(gè)例子來闡述下首次渲染的流程:

    function Name({name}) {
      React.useEffect(() => {
        console.log(`useEffect ${name}`)
        return () => {
          console.log('useEffect destroy')
        }
      }, [name])
      React.useLayoutEffect(() => {
        console.log(`useLayoutEffect ${name}`)
        return () => {
          console.log('useLayoutEffect destroy')
        }
      }, [name])
      return <span>{name}</span>
    }
    function Gender() {
      return <i>Male</i>
    }
    export default function App() {
      const [name, setName] = useState('ayou')
      return (
        <div>
          <Name name={name} />
          <p onClick={() => setName('xingzhi')}>I am 18</p>
          <Gender />
        </div>
      )
    }
    ...
    ReactDOM.render(<App />, document.getElementById('root'))

    首先,我們看看 render,它是從 ReactDOMLegacy 中導(dǎo)出的,并最后調(diào)用了 legacyRenderSubtreeIntoContainer

    function legacyRenderSubtreeIntoContainer(
      parentComponent: ?React$Component<any, any>,
      children: ReactNodeList,
      container: Container,
      forceHydrate: boolean,
      callback: ?Function
    ) {
      // TODO: Without `any` type, Flow says "Property cannot be accessed on any
      // member of intersection type." Whyyyyyy.
      let root: RootType = (container._reactRootContainer: any)
      let fiberRoot
      if (!root) {
        // 首次渲染
        root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
          container,
          forceHydrate
        )
        fiberRoot = root._internalRoot
        if (typeof callback === 'function') {
          const originalCallback = callback
          callback = function () {
            const instance = getPublicRootInstance(fiberRoot)
            originalCallback.call(instance)
          }
        }
        // Initial mount should not be batched.
        unbatchedUpdates(() => {
          updateContainer(children, fiberRoot, parentComponent, callback)
        })
      } else {
        // 更新
        fiberRoot = root._internalRoot
        if (typeof callback === 'function') {
          const originalCallback = callback
          callback = function () {
            const instance = getPublicRootInstance(fiberRoot)
            originalCallback.call(instance)
          }
        }
        updateContainer(children, fiberRoot, parentComponent, callback)
      }
      return getPublicRootInstance(fiberRoot)
    }

    首次渲染時(shí),經(jīng)過下面這一系列的操作,會(huì)初始化一些東西:

    ReactDOMLegacy.js
    function legacyCreateRootFromDOMContainer(
      container: Container,
      forceHydrate: boolean
    ): RootType {
      ...
      return createLegacyRoot(
        container,
        shouldHydrate
          ? {
              hydrate: true,
            }
          : undefined
      )
    }
    ReactDOMRoot.js
    function createLegacyRoot(
      container: Container,
      options?: RootOptions,
    ): RootType {
      return new ReactDOMBlockingRoot(container, LegacyRoot, options);
    }
    function ReactDOMBlockingRoot(
      container: Container,
      tag: RootTag,
      options: void | RootOptions,
    ) {
      this._internalRoot = createRootImpl(container, tag, options);
    }
    function createRootImpl(
      container: Container,
      tag: RootTag,
      options: void | RootOptions,
    ) {
      ...
      const root = createContainer(container, tag, hydrate, hydrationCallbacks)
      ...
    }
    ReactFiberReconciler.old.js
    function createContainer(
      containerInfo: Container,
      tag: RootTag,
      hydrate: boolean,
      hydrationCallbacks: null | SuspenseHydrationCallbacks,
    ): OpaqueRoot {
      return createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks);
    }
    ReactFiberRoot.old.js
    function createFiberRoot(
      containerInfo: any,
      tag: RootTag,
      hydrate: boolean,
      hydrationCallbacks: null | SuspenseHydrationCallbacks,
    ): FiberRoot {
      ...
      const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any)
      const uninitializedFiber = createHostRootFiber(tag)
      root.current = uninitializedFiber
      uninitializedFiber.stateNode = root
      initializeUpdateQueue(uninitializedFiber)
      return root
    }

    經(jīng)過這一系列的操作以后,會(huì)形成如下的數(shù)據(jù)結(jié)構(gòu):

    React首次渲染流程是什么

    然后,會(huì)來到:

    unbatchedUpdates(() => {
      // 這里的 children 是 App 對(duì)應(yīng)的這個(gè) ReactElement
      updateContainer(children, fiberRoot, parentComponent, callback)
    })

    這里 unbatchedUpdates 會(huì)設(shè)置當(dāng)前的 executionContext

    export function unbatchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
      const prevExecutionContext = executionContext
      // 去掉 BatchedContext
      executionContext &= ~BatchedContext
      // 加上 LegacyUnbatchedContext
      executionContext |= LegacyUnbatchedContext
      try {
        return fn(a)
      } finally {
        executionContext = prevExecutionContext
        if (executionContext === NoContext) {
          // Flush the immediate callbacks that were scheduled during this batch
          flushSyncCallbackQueue()
        }
      }
    }

    然后執(zhí)行 updateContainer

    export function updateContainer(
      element: ReactNodeList,
      container: OpaqueRoot,
      parentComponent: ?React$Component<any, any>,
      callback: ?Function
    ): ExpirationTime {
      const current = container.current
      const currentTime = requestCurrentTimeForUpdate()
      const suspenseConfig = requestCurrentSuspenseConfig()
      const expirationTime = computeExpirationForFiber(
        currentTime,
        current,
        suspenseConfig
      )
      const context = getContextForSubtree(parentComponent)
      if (container.context === null) {
        container.context = context
      } else {
        container.pendingContext = context
      }
      const update = createUpdate(expirationTime, suspenseConfig)
      // Caution: React DevTools currently depends on this property
      // being called "element".
      update.payload = {element}
      callback = callback === undefined ? null : callback
      if (callback !== null) {
        update.callback = callback
      }
      enqueueUpdate(current, update)
      scheduleUpdateOnFiber(current, expirationTime)
      return expirationTime
    }

    這里,會(huì)創(chuàng)建一個(gè) update,然后入隊(duì),我們的數(shù)據(jù)結(jié)構(gòu)會(huì)變成這樣:

    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 {
          // 暫時(shí)不看
        }
      } else {
        // 暫時(shí)不看
      }
    }

    最后走到了 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
    }

    這里,可以分為兩個(gè)大的步驟:

    • render

    • commit

    render

    首先看看 renderRootSync

    function renderRootSync(root, expirationTime) {
      const prevExecutionContext = executionContext
      executionContext |= RenderContext
      const prevDispatcher = pushDispatcher(root)
      // If the root or expiration time have changed, throw out the existing stack
      // and prepare a fresh one. Otherwise we'll continue where we left off.
      if (root !== workInProgressRoot || expirationTime !== renderExpirationTime) {
        // 主要是給 workInProgress 賦值
        prepareFreshStack(root, expirationTime)
        startWorkOnPendingInteractions(root, expirationTime)
      }
      const prevInteractions = pushInteractions(root)
      do {
        try {
          workLoopSync()
          break
        } catch (thrownValue) {
          handleError(root, thrownValue)
        }
      } while (true)
      resetContextDependencies()
      if (enableSchedulerTracing) {
        popInteractions(((prevInteractions: any): Set&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
    }

    這里首先調(diào)用 prepareFreshStack(root, expirationTime),這一句主要是通過 root.current 來創(chuàng)建 workInProgress。調(diào)用后,數(shù)據(jù)結(jié)構(gòu)成了這樣:

    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
    }

    這里又分為兩個(gè)步驟:

    • beginWork,傳入當(dāng)前 Fiber 節(jié)點(diǎn),創(chuàng)建子 Fiber 節(jié)點(diǎn)。

    • completeUnitOfWork,通過 Fiber 節(jié)點(diǎn)創(chuàng)建真實(shí) DOM 節(jié)點(diǎn)。

    這兩個(gè)步驟會(huì)交替的執(zhí)行,其目標(biāo)是:

    • 構(gòu)建出新的 Fiber 樹

    • 與舊 Fiber 比較得到 effect 鏈表(插入、更新、刪除、useEffect 等都會(huì)產(chǎn)生 effect)

    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:
        // ...省略
        // ...省略其他類型
      }
    }

    這里因?yàn)槭?rootFiber,所以會(huì)走到 updateHostRoot

    function updateHostRoot(current, workInProgress, renderExpirationTime) {
      // 暫時(shí)不看
      pushHostRootContext(workInProgress)
      const updateQueue = workInProgress.updateQueue
      const nextProps = workInProgress.pendingProps
      const prevState = workInProgress.memoizedState
      const prevChildren = prevState !== null ? prevState.element : null
      cloneUpdateQueue(current, workInProgress)
      processUpdateQueue(workInProgress, nextProps, null, renderExpirationTime)
      const nextState = workInProgress.memoizedState
      // Caution: React DevTools currently depends on this property
      // being called "element".
      const nextChildren = nextState.element
      if (nextChildren === prevChildren) {
        // 省略
      }
      const root: FiberRoot = workInProgress.stateNode
      if (root.hydrate &amp;&amp; enterHydrationState(workInProgress)) {
        // 省略
      } else {
        // 給 rootFiber 生成子 fiber
        reconcileChildren(
          current,
          workInProgress,
          nextChildren,
          renderExpirationTime
        )
        resetHydrationState()
      }
      return workInProgress.child
    }

    經(jīng)過 updateHostRoot 后,會(huì)返回 workInProgress.child 作為下一個(gè) workInProgress,最后的數(shù)據(jù)結(jié)構(gòu)如下(這里先忽略 reconcileChildren 這個(gè)比較復(fù)雜的函數(shù)):

    React首次渲染流程是什么

    接著會(huì)繼續(xù)進(jìn)行 beginWork,這次會(huì)來到 mountIndeterminateComponent (暫時(shí)忽略)??傊?,經(jīng)過不斷的 beginWork 后,我們會(huì)得到如下的一個(gè)結(jié)構(gòu):

    React首次渲染流程是什么

    此時(shí) next 為空,我們會(huì)走到:

    if (next === null) {
      // If this doesn't spawn new work, complete the current work.
      completeUnitOfWork(unitOfWork)
    } else {
      ...
    }
    completeUnitOfWork
    function completeUnitOfWork(unitOfWork: Fiber): void {
      // Attempt to complete the current unit of work, then move to the next
      // sibling. If there are no more siblings, return to the parent fiber.
      let completedWork = unitOfWork
      do {
        // The current, flushed, state of this fiber is the alternate. Ideally
        // nothing should rely on this, but relying on it here means that we don't
        // need an additional field on the work in progress.
        const current = completedWork.alternate
        const returnFiber = completedWork.return
        // Check if the work completed or if something threw.
        if ((completedWork.effectTag & Incomplete) === NoEffect) {
          setCurrentDebugFiberInDEV(completedWork)
          let next
          if (
            !enableProfilerTimer ||
            (completedWork.mode & ProfileMode) === NoMode
          ) {
            next = completeWork(current, completedWork, renderExpirationTime)
          } else {
            startProfilerTimer(completedWork)
            next = completeWork(current, completedWork, renderExpirationTime)
            // Update render duration assuming we didn't error.
            stopProfilerTimerIfRunningAndRecordDelta(completedWork, false)
          }
          resetCurrentDebugFiberInDEV()
          resetChildExpirationTime(completedWork)
          if (next !== null) {
            // Completing this fiber spawned new work. Work on that next.
            workInProgress = next
            return
          }
          if (
            returnFiber !== null &&
            // Do not append effects to parents if a sibling failed to complete
            (returnFiber.effectTag & Incomplete) === NoEffect
          ) {
            // Append all the effects of the subtree and this fiber onto the effect
            // list of the parent. The completion order of the children affects the
            // side-effect order.
            if (returnFiber.firstEffect === null) {
              returnFiber.firstEffect = completedWork.firstEffect
            }
            if (completedWork.lastEffect !== null) {
              if (returnFiber.lastEffect !== null) {
                returnFiber.lastEffect.nextEffect = completedWork.firstEffect
              }
              returnFiber.lastEffect = completedWork.lastEffect
            }
            // If this fiber had side-effects, we append it AFTER the children's
            // side-effects. We can perform certain side-effects earlier if needed,
            // by doing multiple passes over the effect list. We don't want to
            // schedule our own side-effect on our own list because if end up
            // reusing children we'll schedule this effect onto itself since we're
            // at the end.
            const effectTag = completedWork.effectTag
            // Skip both NoWork and PerformedWork tags when creating the effect
            // list. PerformedWork effect is read by React DevTools but shouldn't be
            // committed.
            if (effectTag > PerformedWork) {
              if (returnFiber.lastEffect !== null) {
                returnFiber.lastEffect.nextEffect = completedWork
              } else {
                returnFiber.firstEffect = completedWork
              }
              returnFiber.lastEffect = completedWork
            }
          }
        } else {
          // This fiber did not complete because something threw. Pop values off
          // the stack without entering the complete phase. If this is a boundary,
          // capture values if possible.
          const next = unwindWork(completedWork, renderExpirationTime)
          // Because this fiber did not complete, don't reset its expiration time.
          if (
            enableProfilerTimer &&
            (completedWork.mode & ProfileMode) !== NoMode
          ) {
            // Record the render duration for the fiber that errored.
            stopProfilerTimerIfRunningAndRecordDelta(completedWork, false)
            // Include the time spent working on failed children before continuing.
            let actualDuration = completedWork.actualDuration
            let child = completedWork.child
            while (child !== null) {
              actualDuration += child.actualDuration
              child = child.sibling
            }
            completedWork.actualDuration = actualDuration
          }
          if (next !== null) {
            // If completing this work spawned new work, do that next. We'll come
            // back here again.
            // Since we're restarting, remove anything that is not a host effect
            // from the effect tag.
            next.effectTag &= HostEffectMask
            workInProgress = next
            return
          }
          if (returnFiber !== null) {
            // Mark the parent fiber as incomplete and clear its effect list.
            returnFiber.firstEffect = returnFiber.lastEffect = null
            returnFiber.effectTag |= Incomplete
          }
        }
        const siblingFiber = completedWork.sibling
        if (siblingFiber !== null) {
          // If there is more work to do in this returnFiber, do that next.
          workInProgress = siblingFiber
          return
        }
        // Otherwise, return to the parent
        completedWork = returnFiber
        // Update the next thing we're working on in case something throws.
        workInProgress = completedWork
      } while (completedWork !== null)
      // We've reached the root.
      if (workInProgressRootExitStatus === RootIncomplete) {
        workInProgressRootExitStatus = RootCompleted
      }
    }

    此時(shí)這里的 unitOfWorkspan 對(duì)應(yīng)的 fiber。從函數(shù)頭部的注釋我們可以大致知道該函數(shù)的功能:

    // Attempt to complete the current unit of work, then move to the next
    // sibling. If there are no more siblings, return to the parent fiber.
    // 嘗試去完成當(dāng)前的工作單元,然后處理下一個(gè) sibling。如果沒有 sibling 了,就返回去完成父 fiber

    這里一路走下去最后會(huì)來到 completeWork 這里 :

    case HostComponent:
      ...
      // 會(huì)調(diào)用 ReactDOMComponent.js 中的 createELement 方法創(chuàng)建 span 標(biāo)簽
      const instance = createInstance(
        type,
        newProps,
        rootContainerInstance,
        currentHostContext,
        workInProgress
      )
      // 將子元素 append 到 instance 中
      appendAllChildren(instance, workInProgress, false, false)
      workInProgress.stateNode = instance;

    執(zhí)行完后,我們的結(jié)構(gòu)如下所示(我們用綠色的圓來表示真實(shí) dom):

    React首次渲染流程是什么

    此時(shí) next 將會(huì)是 null,我們需要往上找到下一個(gè) completedWork,即 Name,因?yàn)?Name 是一個(gè) FunctionComponent,所以在 completeWork 中直接返回了 null。又因?yàn)樗?sibling,所以會(huì)將它的 sibling 賦值給 workInProgress,并返回對(duì)其進(jìn)行 beginWork。

    const siblingFiber = completedWork.sibling
    if (siblingFiber !== null) {
      // If there is more work to do in this returnFiber, do that next.
      // workInProgress 更新為 sibling
      workInProgress = siblingFiber
      // 直接返回,回到了 performUnitOfWork
      return
    }
    function performUnitOfWork(unitOfWork: Fiber): void {
      ...
      if (next === null) {
        // If this doesn't spawn new work, complete the current work.
        // 上面的代碼回到了這里
        completeUnitOfWork(unitOfWork)
      } else {
        workInProgress = next
      }
      ReactCurrentOwner.current = null
    }

    這樣 beginWorkcompleteWork 不斷交替的執(zhí)行,當(dāng)我們執(zhí)行到 div 的時(shí)候,我們的結(jié)構(gòu)如下所示:

    React首次渲染流程是什么

    之所以要額外的分析 divcomplete 過程,是因?yàn)檫@個(gè)例子方便我們分析 appendAllChildren

    appendAllChildren = function (
      parent: Instance,
      workInProgress: Fiber,
      needsVisibilityToggle: boolean,
      isHidden: boolean
    ) {
      // We only have the top Fiber that was created but we need recurse down its
      // children to find all the terminal nodes.
      let node = workInProgress.child
      while (node !== null) {
        if (node.tag === HostComponent || node.tag === HostText) {
          appendInitialChild(parent, node.stateNode)
        } else if (enableFundamentalAPI && node.tag === FundamentalComponent) {
          appendInitialChild(parent, node.stateNode.instance)
        } else if (node.tag === HostPortal) {
          // If we have a portal child, then we don't want to traverse
          // down its children. Instead, we'll get insertions from each child in
          // the portal directly.
        } else if (node.child !== null) {
          node.child.return = node
          node = node.child
          continue
        }
        if (node === workInProgress) {
          return
        }
        while (node.sibling === null) {
          if (node.return === null || node.return === workInProgress) {
            return
          }
          node = node.return
        }
        node.sibling.return = node.return
        node = node.sibling
      }
    }

    由于 workInProgress 指向 div 這個(gè) fiber,他的 childName,會(huì)進(jìn)入 else if (node.child !== null) 這個(gè)條件分支。然后繼續(xù)下一個(gè)循環(huán),此時(shí) nodespan 這個(gè) fiber,會(huì)進(jìn)入第一個(gè)分支,將 span 對(duì)應(yīng)的 dom 元素插入到 parent 之中。

    這樣不停的循環(huán),最后會(huì)執(zhí)行到 if (node === workInProgress) 退出,此時(shí)所有的子元素都 append 到了 parent 之中:

    React首次渲染流程是什么

    然后繼續(xù) beginWorkcompleteWork,最后會(huì)來到 rootFiber。不同的是,該節(jié)點(diǎn)的 alternate 并不為空,且該節(jié)點(diǎn) tagHootRoot,所以 completeWork 時(shí)會(huì)來到這里:

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

    看來幾乎沒有做什么事情,到這我們的 render 階段就結(jié)束了,最后的結(jié)構(gòu)如下所示:

    React首次渲染流程是什么

    其中藍(lán)色表示是有 effect 的 Fiber 節(jié)點(diǎn),他們組成了一個(gè)鏈表,方便 commit 過程進(jìn)行遍歷。

    可以查看 render 過程動(dòng)畫。

    commit

    commit 大致可分為以下過程:

    • 準(zhǔn)備階段

    • before mutation 階段(執(zhí)行 DOM 操作前)

    • mutation 階段(執(zhí)行 DOM 操作)

    • 切換 Fiber Tree

    • layout 階段(執(zhí)行 DOM 操作后)

    • 收尾階段

    準(zhǔn)備階段
    do {
      // 觸發(fā)useEffect回調(diào)與其他同步任務(wù)。由于這些任務(wù)可能觸發(fā)新的渲染,所以這里要一直遍歷執(zhí)行直到?jīng)]有任務(wù)
      flushPassiveEffects()
      // 暫時(shí)沒有復(fù)現(xiàn)出 rootWithPendingPassiveEffects !== null 的情景
      // 首次渲染 rootWithPendingPassiveEffects 為 null
    } while (rootWithPendingPassiveEffects !== null)
    // finishedWork 就是正在工作的 rootFiber
    const finishedWork = root.
    // 優(yōu)先級(jí)相關(guān)暫時(shí)不看
    const expirationTime = root.finishedExpirationTime
    if (finishedWork === null) {
      return null
    }
    root.finishedWork = null
    root.finishedExpirationTime = NoWork
    root.callbackNode = null
    root.callbackExpirationTime = NoWork
    root.callbackPriority_old = NoPriority
    const remainingExpirationTimeBeforeCommit = getRemainingExpirationTime(
      finishedWork
    )
    markRootFinishedAtTime(
      root,
      expirationTime,
      remainingExpirationTimeBeforeCommit
    )
    if (rootsWithPendingDiscreteUpdates !== null) {
      const lastDiscreteTime = rootsWithPendingDiscreteUpdates.get(root)
      if (
        lastDiscreteTime !== undefined &&
        remainingExpirationTimeBeforeCommit < lastDiscreteTime
      ) {
        rootsWithPendingDiscreteUpdates.delete(root)
      }
    }
    if (root === workInProgressRoot) {
      workInProgressRoot = null
      workInProgress = null
      renderExpirationTime = NoWork
    } else {
    }
    // 將effectList賦值給firstEffect
    // 由于每個(gè)fiber的effectList只包含他的子孫節(jié)點(diǎn)
    // 所以根節(jié)點(diǎn)如果有effectTag則不會(huì)被包含進(jìn)來
    // 所以這里將有effectTag的根節(jié)點(diǎn)插入到effectList尾部
    // 這樣才能保證有effect的fiber都在effectList中
    let firstEffect
    if (finishedWork.effectTag > PerformedWork) {
      if (finishedWork.lastEffect !== null) {
        finishedWork.lastEffect.nextEffect = finishedWork
        firstEffect = finishedWork.firstEffect
      } else {
        firstEffect = finishedWork
      }
    } else {
      firstEffect = finishedWork.firstEffect
    }

    準(zhǔn)備階段主要是確定 firstEffect,我們的例子中就是 Name 這個(gè) fiber。

    before mutation 階段
    const prevExecutionContext = executionContext
    executionContext |= CommitContext
    const prevInteractions = pushInteractions(root)
    // Reset this to null before calling lifecycles
    ReactCurrentOwner.current = null
    // The commit phase is broken into several sub-phases. We do a separate pass
    // of the effect list for each phase: all mutation effects come before all
    // layout effects, and so on.
    // The first phase a "before mutation" phase. We use this phase to read the
    // state of the host tree right before we mutate it. This is where
    // getSnapshotBeforeUpdate is called.
    focusedInstanceHandle = prepareForCommit(root.containerInfo)
    shouldFireAfterActiveInstanceBlur = false
    nextEffect = firstEffect
    do {
      if (__DEV__) {
        ...
      } else {
        try {
          commitBeforeMutationEffects()
        } catch (error) {
          invariant(nextEffect !== null, 'Should be working on an effect.')
          captureCommitPhaseError(nextEffect, error)
          nextEffect = nextEffect.nextEffect
        }
      }
    } while (nextEffect !== null)
    // We no longer need to track the active instance fiber
    focusedInstanceHandle = null
    if (enableProfilerTimer) {
      // Mark the current commit time to be shared by all Profilers in this
      // batch. This enables them to be grouped later.
      recordCommitTime()
    }

    before mutation 階段主要是調(diào)用了 commitBeforeMutationEffects 方法:

    function commitBeforeMutationEffects() {
      while (nextEffect !== null) {
        if (
          !shouldFireAfterActiveInstanceBlur &&
          focusedInstanceHandle !== null &&
          isFiberHiddenOrDeletedAndContains(nextEffect, focusedInstanceHandle)
        ) {
          shouldFireAfterActiveInstanceBlur = true
          beforeActiveInstanceBlur()
        }
        const effectTag = nextEffect.effectTag
        if ((effectTag & Snapshot) !== NoEffect) {
          setCurrentDebugFiberInDEV(nextEffect)
          const current = nextEffect.alternate
          // 調(diào)用getSnapshotBeforeUpdate
          commitBeforeMutationEffectOnFiber(current, nextEffect)
          resetCurrentDebugFiberInDEV()
        }
        if ((effectTag & Passive) !== NoEffect) {
          // If there are passive effects, schedule a callback to flush at
          // the earliest opportunity.
          if (!rootDoesHavePassiveEffects) {
            rootDoesHavePassiveEffects = true
            scheduleCallback(NormalPriority, () => {
              flushPassiveEffects()
              return null
            })
          }
        }
        nextEffect = nextEffect.nextEffect
      }
    }

    因?yàn)?NameeffectTag 包括了 Passive,所以這里會(huì)執(zhí)行:

    scheduleCallback(NormalPriority, () => {
      flushPassiveEffects()
      return null
    })

    這里主要是對(duì) useEffect 中的任務(wù)進(jìn)行異步調(diào)用,最終會(huì)在下個(gè)事件循環(huán)中執(zhí)行 commitPassiveHookEffects

    export function commitPassiveHookEffects(finishedWork: Fiber): void {
      if ((finishedWork.effectTag & Passive) !== NoEffect) {
        switch (finishedWork.tag) {
          case FunctionComponent:
          case ForwardRef:
          case SimpleMemoComponent:
          case Block: {
            if (
              enableProfilerTimer &&
              enableProfilerCommitHooks &&
              finishedWork.mode & ProfileMode
            ) {
              try {
                startPassiveEffectTimer();
                commitHookEffectListUnmount(
                  HookPassive | HookHasEffect,
                  finishedWork,
                );
                commitHookEffectListMount(
                  HookPassive | HookHasEffect,
                  finishedWork,
                );
              } finally {
                recordPassiveEffectDuration(finishedWork);
              }
            } else {
              commitHookEffectListUnmount(
                HookPassive | HookHasEffect,
                finishedWork,
              );
              commitHookEffectListMount(HookPassive | HookHasEffect, finishedWork);
            }
            break;
          }
          default:
            break;
        }
      }
    }
    function commitHookEffectListUnmount(tag: number, finishedWork: Fiber) {
      const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
      const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
      if (lastEffect !== null) {
        const firstEffect = lastEffect.next;
        let effect = firstEffect;
        do {
          if ((effect.tag & tag) === tag) {
            // Unmount
            const destroy = effect.destroy;
            effect.destroy = undefined;
            if (destroy !== undefined) {
              destroy();
            }
          }
          effect = effect.next;
        } while (effect !== firstEffect);
      }
    }
    function commitHookEffectListMount(tag: number, finishedWork: Fiber) {
      const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
      const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
      if (lastEffect !== null) {
        const firstEffect = lastEffect.next;
        let effect = firstEffect;
        do {
          if ((effect.tag & tag) === tag) {
            // Mount
            const create = effect.create;
            effect.destroy = create();
            ...
          }
          effect = effect.next;
        } while (effect !== firstEffect);
      }
    }

    其中,commitHookEffectListUnmount 會(huì)執(zhí)行 useEffect 上次渲染返回的 destroy 方法,commitHookEffectListMount 會(huì)執(zhí)行 useEffect 本次渲染的 create 方法。具體到我們的例子:

    React首次渲染流程是什么

    因?yàn)槭鞘状武秩?,所?destroy 都是 undefined,所以只會(huì)打印 useEffect ayou。

    mutation 階段

    mutation 階段主要是執(zhí)行了 commitMutationEffects 這個(gè)方法:

    function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
      // TODO: Should probably move the bulk of this function to commitWork.
      while (nextEffect !== null) {
        setCurrentDebugFiberInDEV(nextEffect)
        const effectTag = nextEffect.effectTag
        ...
        // The following switch statement is only concerned about placement,
        // updates, and deletions. To avoid needing to add a case for every possible
        // bitmap value, we remove the secondary effects from the effect tag and
        // switch on that value.
        const primaryEffectTag =
          effectTag & (Placement | Update | Deletion | Hydrating)
        switch (primaryEffectTag) {
         case Placement: {
            commitPlacement(nextEffect);
            // Clear the "placement" from effect tag so that we know that this is
            // inserted, before any life-cycles like componentDidMount gets called.
            // TODO: findDOMNode doesn't rely on this any more but isMounted does
            // and isMounted is deprecated anyway so we should be able to kill this.
            nextEffect.effectTag &= ~Placement;
            break;
          }
          case PlacementAndUpdate: {
            // Placement
            commitPlacement(nextEffect);
            // Clear the "placement" from effect tag so that we know that this is
            // inserted, before any life-cycles like componentDidMount gets called.
            nextEffect.effectTag &= ~Placement;
            // Update
            const current = nextEffect.alternate;
            commitWork(current, nextEffect);
            break;
          }
          case Hydrating: {
            nextEffect.effectTag &= ~Hydrating;
            break;
          }
          case HydratingAndUpdate: {
            nextEffect.effectTag &= ~Hydrating;
            // Update
            const current = nextEffect.alternate;
            commitWork(current, nextEffect);
            break;
          }
          case Update: {
            const current = nextEffect.alternate;
            commitWork(current, nextEffect);
            break;
          }
          case Deletion: {
            commitDeletion(root, nextEffect, renderPriorityLevel);
            break;
          }
        }
      }
    }

    其中,Name 會(huì)走 Update 這個(gè)分支,執(zhí)行 commitWork,最終會(huì)執(zhí)行到 commitHookEffectListUnmount

    function commitHookEffectListUnmount(tag: number, finishedWork: Fiber) {
      const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
      const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
      if (lastEffect !== null) {
        const firstEffect = lastEffect.next;
        let effect = firstEffect;
        do {
          if ((effect.tag & tag) === tag) {
            // Unmount
            const destroy = effect.destroy;
            effect.destroy = undefined;
            if (destroy !== undefined) {
              destroy();
            }
          }
          effect = effect.next;
        } while (effect !== firstEffect);
      }
    }

    這里會(huì)同步執(zhí)行 useLayoutEffect 上次渲染返回的 destroy 方法,我們的例子里是 undefined。

    App 會(huì)走到 Placement 這個(gè)分支,執(zhí)行 commitPlacement,這里的主要工作是把整棵 dom 樹插入到了 <div id='root'></div> 之中。

    切換 Fiber Tree

    mutation 階段完成后,會(huì)執(zhí)行:

    root.current = finishedWork

    完成后, fiberRoot 會(huì)指向 current Fiber 樹。

    React首次渲染流程是什么

    layout 階段

    對(duì)應(yīng)到我們的例子,layout 階段主要是同步執(zhí)行 useLayoutEffect 中的 create 函數(shù),所以這里會(huì)打印 useLayoutEffect ayou。

    題目解析

    現(xiàn)在,我們來分析下文章開始的兩個(gè)題目:

    題目一:

    渲染下面的組件,打印順序是什么?

    import React from 'react'
    const channel = new MessageChannel()
    // onmessage 是一個(gè)宏任務(wù)
    channel.port1.onmessage = () => {
      console.log('1 message channel')
    }
    export default function App() {
      React.useEffect(() => {
        console.log('2 use effect')
      }, [])
      Promise.resolve().then(() => {
        console.log('3 promise')
      })
      React.useLayoutEffect(() => {
        console.log('4 use layout effect')
        channel.port2.postMessage('')
      }, [])
      return <div>App</div>
    }

    解析:

    • useLayoutEffect 中的任務(wù)會(huì)跟隨渲染過程同步執(zhí)行,所以先打印 4

    • Promise 對(duì)象 then 中的任務(wù)是一個(gè)微任務(wù),所以在 4 后面執(zhí)行,打印 3

    • console.log('1 message channel')console.log('2 use effect') 都會(huì)在宏任務(wù)中執(zhí)行,執(zhí)行順序就看誰先生成,這里 2 比 1 先,所以先打印 2,再打印 1。

    題目二:

    點(diǎn)擊 p 標(biāo)簽后,下面事件發(fā)生的順序

    • 頁面顯示 xingzhi

    • console.log('useLayoutEffect ayou')

    • console.log('useLayoutEffect xingzhi')

    • console.log('useEffect ayou')

    • console.log('useEffect xingzhi')

    import React from 'react'
    import {useState} from 'react'
    function Name({name}) {
      React.useEffect(() => {
        console.log(`useEffect ${name}`)
        return () => {
          console.log(`useEffect destroy ${name}`)
        }
      }, [name])
      React.useLayoutEffect(() => {
        console.log(`useLayoutEffect ${name}`)
        return () => {
          console.log(`useLayoutEffect destroy ${name}`)
        }
      }, [name])
      return <span>{name}</span>
    }
    // 點(diǎn)擊后,下面事件發(fā)生的順序
    // 1. 頁面顯示 xingzhi
    // 2. console.log('useLayoutEffect destroy ayou')
    // 3. console.log(`useLayoutEffect xingzhi`)
    // 4. console.log('useEffect destroy ayou')
    // 5. console.log(`useEffect xingzhi`)
    export default function App() {
      const [name, setName] = useState('ayou')
      const onClick = React.useCallback(() => setName('xingzhi'), [])
      return (
        <div>
          <Name name={name} />
          <p onClick={onClick}>I am 18</p>
        </div>
      )
    }

    解析:

    • span 這個(gè) Fiber 位于 effect 鏈表的首部,在 commitMutations 中會(huì)先處理,所以頁面先顯示 xingzhi。

    • Name 這個(gè) Fiber 位于 span 之后,所以 useLayoutEffect 中上一次的 destroy 緊接著其執(zhí)行。打印 useLayoutEffect ayou。

    • commitLayoutEffects 中執(zhí)行 useLayoutEffect 這一次的 create。打印 useLayoutEffect xingzhi。

    • useEffect 在下一個(gè)宏任務(wù)中執(zhí)行,先執(zhí)行上一次的 destroy,再執(zhí)行這一次的 create。所以先打印 useEffect ayou,再打印 useEffect xingzhi。

    “React首次渲染流程是什么”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識(shí)可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實(shí)用文章!

    向AI問一下細(xì)節(jié)

    免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

    AI