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

目录
  • 说明
  • 题目
  • 首次渲染流程
    • render
      • beginWork
      • completeUnitOfWork
    • commit
      • 准备阶段
      • before mutation 阶段
      • mutation 阶段
      • 切换 Fiber Tree
      • layout 阶段
  • 题目解析
  • 总结

说明

本文结论均基于 React 16.13.1 得出,若有出入请参考对应版本源码。参考了 React 技术揭秘

题目

在开始进行源码分析前,我们先来看几个题目:

题目一:

渲染下面的组件,打印顺序是什么?

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

答案:4 3 2 1

题目二:

点击 p 标签后,下面事件发生的顺序

  • 页面显示 xingzhi
  • console.log('useLayoutEffect ayou')
  • console.log('useLayoutEffect xingzhi')
  • console.log('useEffect ayou')
  • console.log('useEffect xingzhi')
import React from 'react'
import {useState} from 'react'
function Name({name}) {
  React.useEffect(() => {
    console.log(`useEffect ${name}`)
    return () => {
      console.log(`useEffect destroy ${name}`)
    }
  }, [name])
  React.useLayoutEffect(() => {
    console.log(`useLayoutEffect ${name}`)
    return () => {
      console.log(`useLayoutEffect destroy ${name}`)
    }
  }, [name])
  return <span>{name}</span>
}
// 点击后,下面事件发生的顺序
// 1. 页面显示 xingzhi
// 2. console.log('useLayoutEffect ayou')
// 3. console.log('useLayoutEffect xingzhi')
// 4. console.log('useEffect ayou')
// 5. console.log('useEffect xingzhi')
export default function App() {
  const [name, setName] = useState('ayou')
  const onClick = React.useCallback(() => setName('xingzhi'), [])
  return (
    <div>
      <Name name={name} />
      <p onClick={onClick}>I am 18</p>
    </div>
  )
}

答案:1 2 3 4 5

你是不是都答对了呢?

首次渲染流程

我们以下面这个例子来阐述下首次渲染的流程:

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

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

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

首次渲染时,经过下面这一系列的操作,会初始化一些东西:

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

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

然后,会来到:

unbatchedUpdates(() => {
  // 这里的 children 是 App 对应的这个 ReactElement
  updateContainer(children, fiberRoot, parentComponent, callback)
})

这里 unbatchedUpdates 会设置当前的 executionContext

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

然后执行 updateContainer

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

这里,会创建一个 update,然后入队,我们的数据结构会变成这样:

接下来就到了 scheduleUpdateOnFiber:

export function scheduleUpdateOnFiber(
  fiber: Fiber,
  expirationTime: ExpirationTime
) {
  checkForNestedUpdates()
  warnAboutRenderPhaseUpdatesInDEV(fiber)
  const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime)
  if (root === null) {
    warnAboutUpdateOnUnmountedFiberInDEV(fiber)
    return
  }
  // TODO: computeExpirationForFiber also reads the priority. Pass the
  // priority as an argument to that function and this one.
  const priorityLevel = getCurrentPriorityLevel()
  if (expirationTime === Sync) {
    if (
      // Check if we're inside unbatchedUpdates
      (executionContext & LegacyUnbatchedContext) !== NoContext &&
      // Check if we're not already rendering
      (executionContext & (RenderContext | CommitContext)) === NoContext
    ) {
      // Register pending interactions on the root to avoid losing traced interaction data.
      schedulePendingInteractions(root, expirationTime)
      // This is a legacy edge case. The initial mount of a ReactDOM.render-ed
      // root inside of batchedUpdates should be synchronous, but layout updates
      // should be deferred until the end of the batch.
      performSyncWorkOnRoot(root)
    } else {
      // 暂时不看
    }
  } else {
    // 暂时不看
  }
}

最后走到了 performSyncWorkOnRoot

function performSyncWorkOnRoot(root) {
  invariant(
    (executionContext &amp; (RenderContext | CommitContext)) === NoContext,
    'Should not already be working.'
  )
  flushPassiveEffects()
  const lastExpiredTime = root.lastExpiredTime
  let expirationTime
  if (lastExpiredTime !== NoWork) {
    ...
  } else {
    // There's no expired work. This must be a new, synchronous render.
    expirationTime = Sync
  }
  let exitStatus = renderRootSync(root, expirationTime)
  ...
  const finishedWork: Fiber = (root.current.alternate: any);
  root.finishedWork = finishedWork;
  root.finishedExpirationTime = expirationTime;
  root.nextKnownPendingLevel = getRemainingExpirationTime(finishedWork);
  commitRoot(root);
  return null
}

这里,可以分为两个大的步骤:

  • render
  • commit

render

首先看看 renderRootSync

function renderRootSync(root, expirationTime) {
  const prevExecutionContext = executionContext
  executionContext |= RenderContext
  const prevDispatcher = pushDispatcher(root)
  // If the root or expiration time have changed, throw out the existing stack
  // and prepare a fresh one. Otherwise we'll continue where we left off.
  if (root !== workInProgressRoot || expirationTime !== renderExpirationTime) {
    // 主要是给 workInProgress 赋值
    prepareFreshStack(root, expirationTime)
    startWorkOnPendingInteractions(root, expirationTime)
  }
  const prevInteractions = pushInteractions(root)
  do {
    try {
      workLoopSync()
      break
    } catch (thrownValue) {
      handleError(root, thrownValue)
    }
  } while (true)
  resetContextDependencies()
  if (enableSchedulerTracing) {
    popInteractions(((prevInteractions: any): Set&lt;Interaction&gt;))
  }
  executionContext = prevExecutionContext
  popDispatcher(prevDispatcher)
  if (workInProgress !== null) {
    // This is a sync render, so we should have finished the whole tree.
    invariant(
      false,
      'Cannot commit an incomplete root. This error is likely caused by a ' +
        'bug in React. Please file an issue.'
    )
  }
  // Set this to null to indicate there's no in-progress render.
  workInProgressRoot = null
  return workInProgressRootExitStatus
}

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

跳过中间的一些语句,我们来到 workLoopSync

function workLoopSync() {
  // Already timed out, so perform work without checking if we need to yield.
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress)
  }
}
function performUnitOfWork(unitOfWork: Fiber): void {
  // The current, flushed, state of this fiber is the alternate. Ideally
  // nothing should rely on this, but relying on it here means that we don't
  // need an additional field on the work in progress.
  const current = unitOfWork.alternate
  setCurrentDebugFiberInDEV(unitOfWork)
  let next
  if (enableProfilerTimer &amp;&amp; (unitOfWork.mode &amp; ProfileMode) !== NoMode) {
    startProfilerTimer(unitOfWork)
    next = beginWork(current, unitOfWork, renderExpirationTime)
    stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true)
  } else {
    next = beginWork(current, unitOfWork, renderExpirationTime)
  }
  resetCurrentDebugFiberInDEV()
  unitOfWork.memoizedProps = unitOfWork.pendingProps
  if (next === null) {
    // If this doesn't spawn new work, complete the current work.
    completeUnitOfWork(unitOfWork)
  } else {
    workInProgress = next
  }
  ReactCurrentOwner.current = null
}

这里又分为两个步骤:

  • beginWork,传入当前 Fiber 节点,创建子 Fiber 节点。
  • completeUnitOfWork,通过 Fiber 节点创建真实 DOM 节点。

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

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

beginWork

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderExpirationTime: ExpirationTime
): Fiber | null {
  const updateExpirationTime = workInProgress.expirationTime
  if (current !== null) {
    const oldProps = current.memoizedProps
    const newProps = workInProgress.pendingProps
    if (
      oldProps !== newProps ||
      hasLegacyContextChanged() ||
      // Force a re-render if the implementation changed due to hot reload:
      (__DEV__ ? workInProgress.type !== current.type : false)
    ) {
      // 略
    } else if (updateExpirationTime &lt; renderExpirationTime) {
      // 略
    } else {
      // An update was scheduled on this fiber, but there are no new props
      // nor legacy context. Set this to false. If an update queue or context
      // consumer produces a changed value, it will set this to true. Otherwise,
      // the component will assume the children have not changed and bail out.
      didReceiveUpdate = false
    }
  } else {
    didReceiveUpdate = false
  }
  // Before entering the begin phase, clear pending update priority.
  // TODO: This assumes that we're about to evaluate the component and process
  // the update queue. However, there's an exception: SimpleMemoComponent
  // sometimes bails out later in the begin phase. This indicates that we should
  // move this assignment out of the common path and into each branch.
  workInProgress.expirationTime = NoWork
  switch (workInProgress.tag) {
    case IndeterminateComponent:
    // ...省略
    case LazyComponent:
    // ...省略
    case FunctionComponent:
    // ...省略
    case ClassComponent:
    // ...省略
    case HostRoot:
      return updateHostRoot(current, workInProgress, renderExpirationTime)
    case HostComponent:
    // ...省略
    case HostText:
    // ...省略
    // ...省略其他类型
  }
}

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

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

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

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

此时 next 为空,我们会走到:

if (next === null) {
  // If this doesn't spawn new work, complete the current work.
  completeUnitOfWork(unitOfWork)
} else {
  ...
}

completeUnitOfWork

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

此时这里的 unitOfWorkspan 对应的 fiber。从函数头部的注释我们可以大致知道该函数的功能:

// Attempt to complete the current unit of work, then move to the next
// sibling. If there are no more siblings, return to the parent fiber.
// 尝试去完成当前的工作单元,然后处理下一个 sibling。如果没有 sibling 了,就返回去完成父 fiber

这里一路走下去最后会来到 completeWork 这里 :

case HostComponent:
  ...
  // 会调用 ReactDOMComponent.js 中的 createELement 方法创建 span 标签
  const instance = createInstance(
    type,
    newProps,
    rootContainerInstance,
    currentHostContext,
    workInProgress
  )
  // 将子元素 append 到 instance 中
  appendAllChildren(instance, workInProgress, false, false)
  workInProgress.stateNode = instance;

执行完后,我们的结构如下所示(我们用绿色的圆来表示真实 dom):

此时 next 将会是 null,我们需要往上找到下一个 completedWork,即 Name,因为 Name 是一个 FunctionComponent,所以在 completeWork 中直接返回了 null。又因为它有 sibling,所以会将它的 sibling 赋值给 workInProgress,并返回对其进行 beginWork

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

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

之所以要额外的分析 divcomplete 过程,是因为这个例子方便我们分析 appendAllChildren

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

由于 workInProgress 指向 div 这个 fiber,他的 childName,会进入 else if (node.child !== null) 这个条件分支。然后继续下一个循环,此时 nodespan 这个 fiber,会进入第一个分支,将 span 对应的 dom 元素插入到 parent 之中。

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

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

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

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

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

可以查看 render 过程动画。

commit

commit 大致可分为以下过程:

  • 准备阶段
  • before mutation 阶段(执行 DOM 操作前)
  • mutation 阶段(执行 DOM 操作)
  • 切换 Fiber Tree
  • layout 阶段(执行 DOM 操作后)
  • 收尾阶段

准备阶段

do {
  // 触发useEffect回调与其他同步任务。由于这些任务可能触发新的渲染,所以这里要一直遍历执行直到没有任务
  flushPassiveEffects()
  // 暂时没有复现出 rootWithPendingPassiveEffects !== null 的情景
  // 首次渲染 rootWithPendingPassiveEffects 为 null
} while (rootWithPendingPassiveEffects !== null)
// finishedWork 就是正在工作的 rootFiber
const finishedWork = root.
// 优先级相关暂时不看
const expirationTime = root.finishedExpirationTime
if (finishedWork === null) {
  return null
}
root.finishedWork = null
root.finishedExpirationTime = NoWork
root.callbackNode = null
root.callbackExpirationTime = NoWork
root.callbackPriority_old = NoPriority
const remainingExpirationTimeBeforeCommit = getRemainingExpirationTime(
  finishedWork
)
markRootFinishedAtTime(
  root,
  expirationTime,
  remainingExpirationTimeBeforeCommit
)
if (rootsWithPendingDiscreteUpdates !== null) {
  const lastDiscreteTime = rootsWithPendingDiscreteUpdates.get(root)
  if (
    lastDiscreteTime !== undefined &&
    remainingExpirationTimeBeforeCommit < lastDiscreteTime
  ) {
    rootsWithPendingDiscreteUpdates.delete(root)
  }
}
if (root === workInProgressRoot) {
  workInProgressRoot = null
  workInProgress = null
  renderExpirationTime = NoWork
} else {
}
// 将effectList赋值给firstEffect
// 由于每个fiber的effectList只包含他的子孙节点
// 所以根节点如果有effectTag则不会被包含进来
// 所以这里将有effectTag的根节点插入到effectList尾部
// 这样才能保证有effect的fiber都在effectList中
let firstEffect
if (finishedWork.effectTag > PerformedWork) {
  if (finishedWork.lastEffect !== null) {
    finishedWork.lastEffect.nextEffect = finishedWork
    firstEffect = finishedWork.firstEffect
  } else {
    firstEffect = finishedWork
  }
} else {
  firstEffect = finishedWork.firstEffect
}

准备阶段主要是确定 firstEffect,我们的例子中就是 Name 这个 fiber

before mutation 阶段

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

before mutation 阶段主要是调用了 commitBeforeMutationEffects 方法:

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

因为 NameeffectTag 包括了 Passive,所以这里会执行:

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

这里主要是对 useEffect 中的任务进行异步调用,最终会在下个事件循环中执行 commitPassiveHookEffects

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

其中,commitHookEffectListUnmount 会执行 useEffect 上次渲染返回的 destroy 方法,commitHookEffectListMount 会执行 useEffect 本次渲染的 create 方法。具体到我们的例子:

因为是首次渲染,所以 destroy 都是 undefined,所以只会打印 useEffect ayou

mutation 阶段

mutation 阶段主要是执行了 commitMutationEffects 这个方法:

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

其中,Name 会走 Update 这个分支,执行 commitWork,最终会执行到 commitHookEffectListUnmount

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

这里会同步执行 useLayoutEffect 上次渲染返回的 destroy 方法,我们的例子里是 undefined。

App 会走到 Placement 这个分支,执行 commitPlacement,这里的主要工作是把整棵 dom 树插入到了 <div id='root'></div> 之中。

切换 Fiber Tree

mutation 阶段完成后,会执行:

root.current = finishedWork

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

layout 阶段

对应到我们的例子,layout 阶段主要是同步执行 useLayoutEffect 中的 create 函数,所以这里会打印 useLayoutEffect ayou

题目解析

现在,我们来分析下文章开始的两个题目:

题目一:

渲染下面的组件,打印顺序是什么?

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

解析:

  • useLayoutEffect 中的任务会跟随渲染过程同步执行,所以先打印 4
  • Promise 对象 then 中的任务是一个微任务,所以在 4 后面执行,打印 3
  • console.log('1 message channel')console.log('2 use effect') 都会在宏任务中执行,执行顺序就看谁先生成,这里 2 比 1 先,所以先打印 2,再打印 1。

题目二:

点击 p 标签后,下面事件发生的顺序

  • 页面显示 xingzhi
  • console.log('useLayoutEffect ayou')
  • console.log('useLayoutEffect xingzhi')
  • console.log('useEffect ayou')
  • console.log('useEffect xingzhi')
import React from 'react'
import {useState} from 'react'
function Name({name}) {
  React.useEffect(() => {
    console.log(`useEffect ${name}`)
    return () => {
      console.log(`useEffect destroy ${name}`)
    }
  }, [name])
  React.useLayoutEffect(() => {
    console.log(`useLayoutEffect ${name}`)
    return () => {
      console.log(`useLayoutEffect destroy ${name}`)
    }
  }, [name])
  return <span>{name}</span>
}
// 点击后,下面事件发生的顺序
// 1. 页面显示 xingzhi
// 2. console.log('useLayoutEffect destroy ayou')
// 3. console.log(`useLayoutEffect xingzhi`)
// 4. console.log('useEffect destroy ayou')
// 5. console.log(`useEffect xingzhi`)
export default function App() {
  const [name, setName] = useState('ayou')
  const onClick = React.useCallback(() => setName('xingzhi'), [])
  return (
    <div>
      <Name name={name} />
      <p onClick={onClick}>I am 18</p>
    </div>
  )
}

解析:

  • span 这个 Fiber 位于 effect 链表的首部,在 commitMutations 中会先处理,所以页面先显示 xingzhi。
  • Name 这个 Fiber 位于 span 之后,所以 useLayoutEffect 中上一次的 destroy 紧接着其执行。打印 useLayoutEffect ayou。
  • commitLayoutEffects 中执行 useLayoutEffect 这一次的 create。打印 useLayoutEffect xingzhi。
  • useEffect 在下一个宏任务中执行,先执行上一次的 destroy,再执行这一次的 create。所以先打印 useEffect ayou,再打印 useEffect xingzhi。

总结

本文大部分内容都参考自 React 技术揭秘,通过举例及画图走读了一遍首次渲染流程,加深了下自己的理解。

以上就是通过示例源码解读React首次渲染流程的详细内容,更多关于React首次渲染流程的资料请关注我们其它相关文章!

(0)

相关推荐

  • go开源Hugo站点渲染之模板词法解析

    目录 正文 了然于胸 - newTemplateExec时序图 词法解析 - parse 正文 Deps在准备好NewPathSpec,NewSpec,NewContentSpec,NewSourceSpec后,调用onCreate正式创建HugoSites,并在最后一步,加载模板执行器. 模板执行器只是提前将模板信息转换成了模板执行器,如何使用并没有涉及到. 为了让我们对模板生命周期有更全面的了解,让我们以robots.txt为例,来看看是如何通过模板执行器生成最终文件的. 了然于胸 - ne

  • electron渲染进程主进程相互传值示例解析

    目录 在electron中分为渲染进程和主进程 浏览器传值给主进程 浏览器环境 主进程 主进程传值给渲染进程 主进程 渲染进程 注意 在electron中分为渲染进程和主进程 渲染进程就是浏览器环境,主进程就是node环境 既然他们是不同的环境,那么为我们怎么让他们相互关联起来呢?或者说怎么传递值? 毕竟在开发中可能会遇到我想要的值只能在node环境中才能获取,然后node中可能也会需要浏览器环境的值:这个时候就需要两个环境联通起来相互传值 浏览器传值给主进程 浏览器环境 引入: import

  • go开源Hugo站点构建三步曲之集结渲染

    目录 Assemble PageState 动手实践 - Show Me the Code of Create a PageState Render 动手实践 - Show Me the Code of Publish Assemble Assemble所做的事情很纯粹,那就是创建站点页面实例 - pageState. 因为支持多站点,contentMaps有多个. 所以Assemble不仅要创建pageState,还需要管理好所有的pages,这就用到了PageMaps. type pageM

  • React中immutable的UI组件渲染性能详解

    目录 引言 UI组件渲染性能 方案一:shallow compare 方案二:直接对前后的对象进行deepCompare 总结: 引言 react 一直遵循UI = fn(state) 的原则,有时候我们的state却和UI不同步 有时候组件本身在业务上不需要渲染,却又会再一次re-render.之前在项目中遇到的一些问题,这里做一个简单的分析,大家可以一起交流一下 UI组件渲染性能 react每次触发页面的更新可大致分成两步: render(): 主要是计算v-dom的diff commit阶

  • 详解无界微前端是如何渲染子应用的demo解析

    目录 正文 无界渲染子应用的步骤 创建子应用 iframe 解析入口 HTML 处理 CSS 并重新嵌入 HTML 创建 webComponent 并挂载 HTML JS 的执行细节 简单的实现 将 UI 渲染到 shadowRoot 挟持 document 的属性/方法 副作用的处理 DOM 相关的副作用处理 修正相对 URl 修正 shadowRoot head.body iframe 的副作用处理 History API window/document 属性/事件 location 对象

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

    目录 说明 题目 首次渲染流程 render beginWork completeUnitOfWork commit 准备阶段 before mutation 阶段 mutation 阶段 切换 Fiber Tree layout 阶段 题目解析 总结 说明 本文结论均基于 React 16.13.1 得出,若有出入请参考对应版本源码.参考了 React 技术揭秘. 题目 在开始进行源码分析前,我们先来看几个题目: 题目一: 渲染下面的组件,打印顺序是什么? import React from

  • Vue3 源码解读之 Teleport 组件使用示例

    目录 Teleport 组件解决的问题 Teleport 组件的基本结构 Teleport 组件 process 函数 Teleport 组件的挂载 Teleport 组件的更新 moveTeleport 移动Teleport 组件 hydrateTeleport 服务端渲染 Teleport 组件 总结 Teleport 组件解决的问题 版本:3.2.31 如果要实现一个 “蒙层” 的功能,并且该 “蒙层” 可以遮挡页面上的所有元素,通常情况下我们会选择直接在 标签下渲染 “蒙层” 内容.如果

  • React前端开发createElement源码解读

    目录 React 与 Babel 元素标签转译 组件转译 子元素转译 createElement 源码 函数入参 第一段代码 __self 和 __source 第二段代码 props 对象 第三段代码 children 第四段代码 defaultProps 第五段代码 owner ReactElement 源码 REACT_ELEMENT_TYPE 回顾 React 与 Babel 元素标签转译 用过 React 的同学都知道,当我们这样写时: <div id="foo">

  • React之echarts-for-react源码解读

    目录 前言 从与原生初始化对比开始 陷阱-默认值height为300px 主逻辑源码剖析 挂载渲染过程 更新渲染过程 卸载过程 项目依赖 后续 前言 在当前工业4.0和智能制造的产业升级浪潮当中,智慧大屏无疑是展示企业IT成果的最有效方式之一.然而其背后怎么能缺少ECharts的身影呢?对于React应用而言,直接使用ECharts并不是最高效且优雅的方式,而echarts-for-react则是针对React应用对ECharts进行轻量封装和增强的工具库. echarts-for-react的

  • vue3 源码解读之 time slicing的使用方法

    今天给大家带来一篇源码解析的文章,emm 是关于 vue3 的,vue3 源码放出后,已经有很多文章来分析它的源码,我觉得很快又要烂大街了,哈哈 不过今天我要解析的部分是已经被废除的 time slicing 部分,这部分源码曾经出现在 vue conf 2018 的视频中,但是源码已经被移除掉了,之后可能也不会有人关注,所以应该不会烂大街 打包 阅读源码之前,需要先进行打包,打包出一份干净可调试的文件很重要 vue3 使用的 rollup 进行打包,我们需要先对它进行改造 import cle

  • 深入理解Java线程池从设计思想到源码解读

    线程池:从设计思想到源码解析 前言初识线程池线程池优势线程池设计思路 深入线程池构造方法任务队列拒绝策略线程池状态初始化&容量调整&关闭 使用线程池ThreadPoolExecutorExecutors封装线程池 解读线程池execute()addWorker()Worker类runWorker()processWorkerExit() 前言 各位小伙伴儿,春节已经结束了,在此献上一篇肝了一个春节假期的迟来的拜年之作,希望读者朋友们都能有收获. 根据穆氏哲学,投入越多,收获越大.我作此文时

  • Vue 2源码解读$mount函数原理

    目录 1. $mount 函数来源 2. runtime 运行时的 $mount 函数 2.1 mountComponent 函数 2.2 _update 函数(首次渲染) 3. runtime-with-compiler 的 $mount 函数 4. runtime 对 Vue 构造函数的其他修改 1. $mount 函数来源 上一节虽然直接从 core 目录下找到了 Vue 的构造函数定义,但是缺少 $mount 方法.所以直接从开发过程中使用的 vue.esm.js 找到对应的源码入口.

  • Css-In-Js实现classNames库源码解读

    目录 引言 使用 源码阅读 兼容性 CommonJS AMD window 浏览器环境 实现 多个参数处理 参数类型处理 数组处理 对象处理 测试用例 Css-in-JS 示例 总结 引言 classNames是一个简单的且实用的JavaScript应用程序,可以有条件的将多个类名组合在一起.它是一个非常有用的工具,可以用来动态的添加或者删除类名. 仓库地址:classNames 使用 根据classNames的README,可以发现库的作者对这个库非常认真,文档和测试用例都非常齐全,同时还有有

  • Ajax::prototype 源码解读

    AJAX之旅(1):由prototype_1.3.1进入javascript殿堂-类的初探  还是决定冠上ajax的头衔,毕竟很多人会用这个关键词搜索.虽然我认为这只是个炒作的概念,不过不得不承认ajax叫起来要方便多了.所以ajax的意思我就不详细解释了. 写这个教程的起因很简单:经过一段时间的ajax学习,有一些体会,并且越发认识到ajax技术的强大,所以决定记录下来,顺便也是对自己思路的整理.有关这个教程的后续,请关注http://www.x2design.net 前几年,javascri

  • Bootstrap源码解读排版(1)

    源码解读Bootstrap排版 粗体 可以使用<b>和<strong>标签让文本直接加粗. 例如: <p>我在学习<strong>Bootstrap</strong></p> 源码 b, strong { font-weight: bold; } 斜体 使用标签<em>或<i>来实现. 例如: <p>我在学<i>Bootstrap</i>.</p> 强调相关的类

随机推荐