验证码: 看不清楚,换一张 查询 注册会员,免验证
  • {{ basic.site_slogan }}
  • 打开微信扫一扫,
    您还可以在这里找到我们哟

    关注我们

React首次渲染流程是什么

阅读:339 来源:乙速云 作者:代码code

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

      答案: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 {name}
      }
      // 点击后,下面事件发生的顺序
      // 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 (
          
                   I am 18

          
        ) }

      答案: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 {name}
      }
      function Gender() {
        return Male
      }
      export default function App() {
        const [name, setName] = useState('ayou')
        return (
          
                    setName('xingzhi')}>I am 18

                 
        ) } ... ReactDOM.render(, document.getElementById('root'))

      首先,我们看看 render,它是从 ReactDOMLegacy 中导出的,并最后调用了 legacyRenderSubtreeIntoContainer

      function legacyRenderSubtreeIntoContainer(
        parentComponent: ?React$Component,
        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(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,
        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 & (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<Interaction>))
        }
        executionContext = prevExecutionContext
        popDispatcher(prevDispatcher)
        if (workInProgress !== null) {
          // This is a sync render, so we should have finished the whole tree.
          invariant(
            false,
            'Cannot commit an incomplete root. This error is likely caused by a ' +
              'bug in React. Please file an issue.'
          )
        }
        // Set this to null to indicate there's no in-progress render.
        workInProgressRoot = null
        return workInProgressRootExitStatus
      }

      这里首先调用 prepareFreshStack(root, expirationTime),这一句主要是通过 root.current 来创建 workInProgress。调用后,数据结构成了这样:

      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 && (unitOfWork.mode & ProfileMode) !== NoMode) {
          startProfilerTimer(unitOfWork)
          next = beginWork(current, unitOfWork, renderExpirationTime)
          stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true)
        } else {
          next = beginWork(current, unitOfWork, renderExpirationTime)
        }
        resetCurrentDebugFiberInDEV()
        unitOfWork.memoizedProps = unitOfWork.pendingProps
        if (next === null) {
          // If this doesn't spawn new work, complete the current work.
          completeUnitOfWork(unitOfWork)
        } else {
          workInProgress = next
        }
        ReactCurrentOwner.current = null
      }

      这里又分为两个步骤:

      • beginWork,传入当前 Fiber 节点,创建子 Fiber 节点。

      • completeUnitOfWork,通过 Fiber 节点创建真实 DOM 节点。

      这两个步骤会交替的执行,其目标是:

      • 构建出新的 Fiber 树

      • 与旧 Fiber 比较得到 effect 链表(插入、更新、删除、useEffect 等都会产生 effect)

      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 < renderExpirationTime) {
            // 略
          } else {
            // An update was scheduled on this fiber, but there are no new props
            // nor legacy context. Set this to false. If an update queue or context
            // consumer produces a changed value, it will set this to true. Otherwise,
            // the component will assume the children have not changed and bail out.
            didReceiveUpdate = false
          }
        } else {
          didReceiveUpdate = false
        }
        // Before entering the begin phase, clear pending update priority.
        // TODO: This assumes that we're about to evaluate the component and process
        // the update queue. However, there's an exception: SimpleMemoComponent
        // sometimes bails out later in the begin phase. This indicates that we should
        // move this assignment out of the common path and into each branch.
        workInProgress.expirationTime = NoWork
        switch (workInProgress.tag) {
          case IndeterminateComponent:
          // ...省略
          case LazyComponent:
          // ...省略
          case FunctionComponent:
          // ...省略
          case ClassComponent:
          // ...省略
          case HostRoot:
            return updateHostRoot(current, workInProgress, renderExpirationTime)
          case HostComponent:
          // ...省略
          case HostText:
          // ...省略
          // ...省略其他类型
        }
      }

      这里因为是 rootFiber,所以会走到 updateHostRoot

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

      经过 updateHostRoot 后,会返回 workInProgress.child 作为下一个 workInProgress,最后的数据结构如下(这里先忽略 reconcileChildren 这个比较复杂的函数):

      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 树插入到了

      之中。

      切换 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 
      App
      }

      解析:

      • 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 {name}
      }
      // 点击后,下面事件发生的顺序
      // 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 (
          
                   I am 18

          
        ) }

      解析:

      • span 这个 Fiber 位于 effect 链表的首部,在 commitMutations 中会先处理,所以页面先显示 xingzhi。

      • Name 这个 Fiber 位于 span 之后,所以 useLayoutEffect 中上一次的 destroy 紧接着其执行。打印 useLayoutEffect ayou。

      • commitLayoutEffects 中执行 useLayoutEffect 这一次的 create。打印 useLayoutEffect xingzhi。

      • useEffect 在下一个宏任务中执行,先执行上一次的 destroy,再执行这一次的 create。所以先打印 useEffect ayou,再打印 useEffect xingzhi。

    分享到:
    *特别声明:以上内容来自于网络收集,著作权属原作者所有,如有侵权,请联系我们: hlamps#outlook.com (#换成@)。
    相关文章
    {{ v.title }}
    {{ v.description||(cleanHtml(v.content)).substr(0,100)+'···' }}
    你可能感兴趣
    推荐阅读 更多>