我的编程空间,编程开发者的网络收藏夹
学习永远不晚

React首次渲染流程是什么

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

React首次渲染流程是什么

本篇内容介绍了“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.jsfunction legacyCreateRootFromDOMContainer(  container: Container,  forceHydrate: boolean): RootType {  ...  return createLegacyRoot(    container,    shouldHydrate      ? {          hydrate: true,        }      : undefined  )}ReactDOMRoot.jsfunction 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.jsfunction createContainer(  containerInfo: Container,  tag: RootTag,  hydrate: boolean,  hydrationCallbacks: null | SuspenseHydrationCallbacks,): OpaqueRoot {  return createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks);}ReactFiberRoot.old.jsfunction 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.siblingif (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 就是正在工作的 rootFiberconst finishedWork = root.// 优先级相关暂时不看const expirationTime = root.finishedExpirationTimeif (finishedWork === null) {  return null}root.finishedWork = nullroot.finishedExpirationTime = NoWorkroot.callbackNode = nullroot.callbackExpirationTime = NoWorkroot.callbackPriority_old = NoPriorityconst 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 firstEffectif (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 = executionContextexecutionContext |= CommitContextconst prevInteractions = pushInteractions(root)// Reset this to null before calling lifecyclesReactCurrentOwner.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 = falsenextEffect = firstEffectdo {  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 fiberfocusedInstanceHandle = nullif (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首次渲染流程是什么”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注编程网网站,小编将为大家输出更多高质量的实用文章!

    免责声明:

    ① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

    ② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

    React首次渲染流程是什么

    下载Word文档到电脑,方便收藏和打印~

    下载Word文档

    猜你喜欢

    React首次渲染流程是什么

    本篇内容介绍了“React首次渲染流程是什么”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!题目在开始进行源码分析前,我们先来看几个题目:题目
    2023-07-05

    react请求数据并渲染的方法是什么

    在React中,可以使用fetch或axios等库来发送请求并获取数据。一般情况下,在组件的生命周期方法中发送请求并在获取到数据后进行渲染。以下是一个使用fetch发送请求并渲染数据的例子:```jsximport React, { use
    2023-10-07

    编程热搜

    • Python 学习之路 - Python
      一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
      Python 学习之路 - Python
    • chatgpt的中文全称是什么
      chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
      chatgpt的中文全称是什么
    • C/C++中extern函数使用详解
    • C/C++可变参数的使用
      可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
      C/C++可变参数的使用
    • css样式文件该放在哪里
    • php中数组下标必须是连续的吗
    • Python 3 教程
      Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
      Python 3 教程
    • Python pip包管理
      一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
      Python pip包管理
    • ubuntu如何重新编译内核
    • 改善Java代码之慎用java动态编译

    目录