React的第一次渲染过程浅析

248次阅读

共计 25447 个字符,预计需要花费 64 分钟才能阅读完成。

React 的第一次渲染过程浅析

本篇文章暂时讨论 Sync 模式(同步), 源码为 16.9,部分源码内容不讨论(hooks classComponent 等等相关的代码)。

a demo

先看一段 react 的代码

function Counter(props) {
  return (
    <div>
      <div>{props.count}</div>
      <button
        onClick={() => {console.log('l am button');
        }}
      >
        add
      </button>
    </div>
  )
}
function App(props) {return <Counter count="12" key="12" />;}

ReactDOM.render(<App />, document.getElementById('app'));

jsx 语法可以通过 babel 对应的 jsx 插件需要转义成可执行的代码(try it out),上述代码<App />:

// 转义后的代码
function App(props) {
  return React.createElement(CounterButton, {key: "12"});
}

// 结果
{$$typeof: Symbol(react.element),
    key: null,
    props: {},
    ref: null,
    type: ƒ App(props),
}

创建 fiberRoot

传入 ReactDOM.render 函数的三个参数elementcontainercallback

container_reactRootContainer 属性在第一次创建是不存在的,先要创建它

// ReactDOM.js
let rootSibling;
while ((rootSibling = container.lastChild)) {container.removeChild(rootSibling);
}

先将 container 即我们传入 div#app 的所有子节点删除 得到的结果:

// root
{
  _internalRoot: {
    current: FiberNode,
    containerInfo: div#app,
    ...
  }
}

current 指向的是 root fiber 节点, containerInfo 执行 dom 元素 id 为 app 的 div

unbatchedUpdates

接着使用 unbatchedUpdates 调用 updateContainerunbatchedUpdates 来自调度系统ReactFiberWorkLoop

// ReactFiberWorkLoop.js
function unbatchedUpdates(fn, a) {
  const prevExecutionContext = executionContext;
  executionContext &= ~BatchedContext;
  executionContext |= LegacyUnbatchedContext;
  try {return fn(a);
  } finally {
    executionContext = prevExecutionContext;
    if (executionContext === NoContext) {flushSyncCallbackQueue();
    }
  }
}

全局变量 executionContext 代表当前的执行上下文, 初始化为 NoContent

// ReactFiberWorkLoop.js

const NoContext = /*                    */ 0b000000;
const BatchedContext = /*               */ 0b000001;
const EventContext = /*                 */ 0b000010;
const DiscreteEventContext = /*         */ 0b000100;
const LegacyUnbatchedContext = /*       */ 0b001000;
const RenderContext = /*                */ 0b010000;
const CommitContext = /*                */ 0b100000;

executionContext &= ~BatchedContext代表什么含义尼?

首先 & 操作当且当两个位上都为 1 的时候返回 1,| 只要有一位为1,返回1

executionContext则是这些 Context 组合的结果:
将当前上下文添加Render

executionContext |= RenderContext

判断当前是否处于 Render 阶段

executionContext &= RenderContext === NoContext

去除Render:

executionContext &= ~RenderContext

executionContext &= ~BatchedContext则代表把当前上下文的 BatchedContext 标志位置为 false,表示当前为非批量更新

在 react 源码中有很多类似的位运算,比如 effectTag,workTag。

reconciler(调和)

updateContainer

计算当前时间和当前的过期时间,因本文只讨论同步模式所以这里的 expirationTime

// ReactFiberExpirationTime.js
const Sync = MAX_SIGNED_31_BIT_INT;

// ReactFiberWorkLoop.js
function computeExpirationForFiber(
  currentTime,
  fiber,
  suspenseConfig,
) {
  const mode = fiber.mode
  if ((mode & BatchedMode) === NoMode) {return Sync}
}

expirationTime越大,代表优先级越高,所以同步模式拥有最高的优先级。

updateContainerAtExpirationTime 创建于 context 相关内容,后续有专门文章介绍context,这里先不讨论。

scheduleRootUpdate


// ReactFiberReconciler.js
function scheduleRootUpdate(
  current,
  element,
  expirationTime,
  suspenseConfig,
  callback,
) {const update = createUpdate(expirationTime, suspenseConfig);
  update.payload = {element};

  callback = callback === undefined ? null : callback;
  if (callback !== null) {update.callback = callback;}
  enqueueUpdate(current, update);
  scheduleWork(current, expirationTime);

  return expirationTime;
}

创建update,将 callback 添加到 update 上。

{
  callback: null
  expirationTime: 1073741823
  next: null
  nextEffect: null
  payload: {element: {$$typeof: Symbol(react.element)
    key: null
    props: {}
    ref: null
    type: ƒ App(props)
  }}
  priority: 97
  suspenseConfig: null
  tag: 0
}

再更新添加到 root fiber 的更新队列上,指的一提的是这里的更新队列 updateQueue 也采用了双缓冲技术,两条 updateQueue 通过 alternate 属性
相互引用。这个链表大致为:

{
  baseState: null
  firstCapturedEffect: null
  firstCapturedUpdate: null
  firstEffect: null
  firstUpdate: update
  lastCapturedEffect: null
  lastCapturedUpdate: null
  lastEffect: null
  lastUpdate: update
}

调用 scheduleWork 进入到调度阶段。

scheduleWork(调度阶段)

// ReactFiberWorkLoop.js
function scheduleUpdateOnFiber(fiber, expirationTime) {const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime);

  if (expirationTime === Sync) {
    if ((executionContext & LegacyUnbatchedContext) !== NoContext &&
      (executionContext & (RenderContext | CommitContext)) === NoContext
    ) {let callback = renderRoot(root, Sync, true);
      while (callback !== null) {callback = callback(true);
      }
    }
  }
}

进入调度阶段,首先调用 markUpdateTimeFromFiberToRoot 将 fiber 上的更新时间,此时的 fiber 树只有一个 root fiber 光杆司令。

// ReactFiberWorkLoop.js
function markUpdateTimeFromFiberToRoot() {if (fiber.expirationTime < expirationTime) {fiber.expirationTime = expirationTime;}
  ...
  let alternate = fiber.alternate;

  let node = fiber.return;
  let root = null;
  if (node === null && fiber.tag === HostRoot) {root = fiber.stateNode;} else {...}
  return root
}

这里返回的 root 是个 fiberRoot 类型的节点。

继续往下,条件 expirationTime === Sync 符合

executionContext & LegacyUnbatchedContext) !== NoContext &&
executionContext & (RenderContext | CommitContext)) === NoContext

这里的两个位运算,在 unbatchedUpdates 方法内将初始化的上下文 NoContext 添加了 LegacyUnbatchedContext 上下文,所以这里得到的结果是真。

renderRoot

renderRoot 阶段只要进行两部分工作:一个是 workLoop 循环,即 render 阶段 另一个为 commitRoot,commit 阶段

// ReactFiberExpirationTime.js
const NoWork = 0

// ReactFiberWorkLoop.js
let workInProgressRoot = null
let renderExpirationTime = NoWork

function renderRoot(root, expirationTime) {
  ...
  if (root !== workInProgressRoot || expirationTime !== renderExpirationTime) {prepareFreshStack(root, expirationTime);
  } 
  ...

  /* renderRoot-code-branch-01 */
}

此时的 workInProgressRootrenderExpirationTime 都处于初始状态。

function prepareFreshStack(root, expirationTime) {
  root.finishedWork = null;
  root.finishedExpirationTime = NoWork;
  ...
  workInProgressRoot = root;
  workInProgress = createWorkInProgress(root.current, null, expirationTime);
  renderExpirationTime = expirationTime;
  ...
}

prepareFreshStack顾名思义,准备一个新生的堆栈环境。
首先将 finishedWork 相关的变量初始化。
root 赋给全局变量 workInProgressRootexpirationTime 赋给 renderExpirationTime
为 root.current 即 root fiber 节点创建一个 workInProgress 节点,并将该节点赋给全局变量 workInProgressfiber 节点也是应用了双缓冲,两个 fiber 节点通过 alternate 属性保存了对方的引用 在更新的过程中操作的是 workInProgress 节点。调度结束时 workInProgress fiber会替代current fiber

/* renderRoot-code-branch-01 */
if (workInProgress !== null) {
  const prevExecutionContext = executionContext;
  executionContext |= RenderContext;

  /* hooks-related ** start */
  let prevDispatcher = ReactCurrentDispatcher.current;
  if (prevDispatcher === null) {prevDispatcher = ContextOnlyDispatcher;}
  ReactCurrentDispatcher.current = ContextOnlyDispatcher;
  /* hooks-related ** end */

  /* workLoop */
}

此时的 workInProgress 为刚创建的那个节点。接着为当前的上下文添加 RenderContext,标志着进入 render 阶段。
hooks-related 这部分代码是与 hooks 先关的代码,在这过程中用户调用 hooks 相关的 API 都不是在 FunctionComponent 的内部,所以都会报错。

render 阶段

function workLoopSync() {while (workInProgress !== null) {workInProgress = performUnitOfWork(workInProgress);
  }
}

/* workLoop */
do {
  try {if (isSync) {workLoopSync()
    }
  } catch (error) {// ...}
  break
} while (true)

workLoop 过程是一个递归的过程 从 root 阶段向下遍历到叶子节点,再从叶子节点执行一些遍历的逻辑最后返回到 root 节点,这次过程执行 beginWorkcompleteWork 等操作,
在此过程中创建 fiber 节点组装 fiber 树,创建对应的 dom 节点等等。

文章开始的代码 workLoop 过程大致如下:

一个简单的线上 demo,根据代码模拟 workLoop 执行过程地址(放在 githubpage 上的打开速度可能慢一些)

让我们开启 workLoop 之旅吧!

function performUnitOfWork(unitOfWork) {
  const current = unitOfWork.alternate
  ...
  let next = beginWork(current, unitOfWork, renderExpirationTime)
  unitOfWork.memoizedProps = unitOfWork.pendingProps

  if (next === null) {next = completeUnitOfWork(unitOfWork)
  }

  return next
}

在这个循环过程 beginWork 顺着 element 树的向下深度遍历 当遍历到叶子节点时,即 next 为 null 时,completeUnitOfWork 则会定位 next 的值:

  1. 当前节点 是否有兄弟节点,有,返回进行下一次 beginWork;无则转到 2
  2. 当前节点置为 父节点,父节点是否存在 存在,转到 1;否则返回 null

当然这两个过程所得工作不仅仅就是这样。

beginWork

// ReactFiberBeginWork.js
let didReceiveUpdate = false


function beginWork(current, workInProgress, renderExpirationTime) {if (current !== null) {
    const oldProps = current.memoizedProps
    const newProps = workInProgress.pendingProps
    if (oldProps !== newProps || hasLegacyContextChanged()) {didReceiveUpdate = true;} else if (updateExpirationTime < renderExpirationTime) {...}
  } else {didReceiveUpdate = true}

  workInProgress.expirationTime = NoWork;

  switch (workInProgress.tag) {
    case HostRoot: {return updateHostRoot(current, workInProgress, renderExpirationTime);
    }
    case 
  }
}

root fiber 是存在 current fiber 的,但此时的 oldPropsnewProps都为 null。虽然这里不讨论context,但是从

if (oldProps !== newProps || hasLegacyContextChanged()) {didReceiveUpdate = true;}

我们可以看出旧的context API 的低效。

在进入到 beginWork 之前先将 expirationTime 置为NoWork

beginWork HostRoot
root fiber 对应的更新为HostRoot

// ReactFiberBeginWork.js
function updateHostRoot(current, workInProgress, renderExpirationTime) {
  const updateQueue = workInProgress.updateQueue;
  const nextProps = workInProgress.pendingProps;
  const prevState = workInProgress.memoizedState;
  const prevChildren = prevState !== null ? prevState.element : null;
  processUpdateQueue(
    workInProgress,
    updateQueue,
    nextProps,
    null,
    renderExpirationTime,
  );

  const nextState = workInProgress.memoizedState;
  const nextChildren = nextState.element;
  
  if (nextChildren === prevChildren) {...}
  const root = workInProgress.stateNode
  if ((current === null || current.child === null) && root.hydrate) {...} else {
    reconcileChildren(
      current,
      workInProgress,
      nextChildren,
      renderExpirationTime,
    );
  }
  return workInProgress.child;
}

scheduleRootUpdate 创建的更新队列我们创建了一个更新队列,里面有一条更新。

processUpdateQueue对于所做的将队列清空 将 updatepayload合并到 updateQueuebaseState属性 同时添加到 workInProgress 节点的 memoizedState
所以 nextChildren 就是 memoizedStateelement属性了。也就是

{$$typeof: Symbol(react.element)
  key: null
  props: {}
  ref: null
  type: ƒ App(props)
}

接着 root.hydrate 这个判断是服务端渲染相关的代码,这里不涉及,所以走另一个分支

// ReactFiberBeginWork.js
function reconcileChildren(current, workInProgress, nextChildren, renderExpirationTime) {if (current === null) {
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderExpirationTime,
    );
  } else {
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderExpirationTime,
    );
  }
}

根据 current 是否存在 走不同的分支,mountChildFibersmountChildFibers 不同在于一个参数传递的问题。此时 current.childnull

// ReactChildFiber.js
const reconcileChildFibers = ChildReconciler(true);
const mountChildFibers = ChildReconciler(false);

ChildReconciler

ChildReconciler是一个高阶函数,内部许多子方法,依次看来

// ReactChildFiber.js
function ChildReconciler(shouldTrackSideEffects) {
  function reconcileChildFibers(
    returnFiber,
    currentFirstChild,
    newChild,
    expirationTime
  ) {
    // Fragment 相关内容 先跳过
    const isUnkeyedTopLevelFragment = false
    const isObject = typeof newChild === 'object' && newChild !== null;

    if (isObject) {switch (newChild.$$typeof) {
        case REACT_ELEMENT_TYPE:
          return placeSingleChild(
            reconcileSingleElement(
              returnFiber,
              currentFirstChild,
              newChild,
              expirationTime,
            ),
          );
      }
    }

    /**  **/
  }
}

这里暂不讨论 Fragment 相关内容 直接将标志位 isUnkeyedTopLevelFragment 置为假。这里的 newChild 对应着 App 组件,isObject为真,且newChild.$$typeof === REACT_ELEMENT_TYPE

reconcileSingleElement placeSingleChild

// ReactChildFiber.js
function reconcileSingleElement(
  returnFiber,
  currentFirstChild,
  element,
  expirationTime
) {
  const key = element.key
  let child = currentFirstChild
  while(child !== null) {...}
  if (element.type === REACT_FRAGMENT_TYPE) {...} else {
    const created = createFiberFromElement(
      element,
      returnFiber.mode,
      expirationTime,
    );
    // to do
    // created.ref = coerceRef(returnFiber, currentFirstChild, element);
    created.return = returnFiber;
    return created;
  }
}

function placeSingleChild(newFiber) {if (shouldTrackSideEffects && newFiber.alternate === null) {newFiber.effectTag = Placement;}
  return newFiber
}

App 组件对应的 fiber 节点在之前并不存在,所以这里创建 fiber 节点 并将 fiber 的父节点设为 root fiber 节点。之后在 placeSingleChild 为 fiber 的 effectTag 打上 Placement
返回到 beginWorkupdateHostRoot,接着返回 workInProgress.child,返回到completeUnitOfWork 函数内,

next = beginWork()
if (next === null) {...}
return next

返回的为新创建的 App 对应的 fiber,所以 beginWork 继续执行。

回到刚才的 beginWork
创建的 Function Component 组件 fiber 默认的 tag 为 IndeterminateComponent,class Component 会被指定为 ClassComponent

let fiber;
let fiberTag = IndeterminateComponent;
let resolvedType = type;
if (typeof type === 'function') {if (shouldConstruct(type)) {
    fiberTag = ClassComponent;
    ...
  } else {...}
} else if (typeof type === 'string') {fiberTag = HostComponent;}

回顾一下 beginWork

let didReceiveUpdate = false

function beginWork() {
  ...
  if (current !== null) {...} else {didReceiveUpdate = false}

  switch (workInProgress.tag) {
    case IndeterminateComponent: {
      return mountIndeterminateComponent(
        current,
        workInProgress,
        workInProgress.type,
        renderExpirationTime,
      );
    }
  }
}

mountIndeterminateComponent 大致代码:

function mountIndeterminateComponent(
  _current,
  workInProgress,
  Component,
  renderExpirationTime
) {if (_current !== null) {...}

  const props = workInProgress.pendingProps
  
  ...
  let value = renderWithHooks(
    null,
    workInProgress,
    Component,
    props,
    context,
    renderExpirationTime,
  );

  if (typeof value === 'object' && value !== null && typeof value.render === 'function') {...} else {
    workInProgress.tag = FunctionComponent;
    reconcileChildren(null, workInProgress, value, renderExpirationTime);
  }

  return workInProgress.child;
}

这里的 renderWithHooks 先简单看成 Component(props),后面部分介绍 hooks 相关代码。

返回的 value 为:

React.createElement(Counter, {
  count: "12",
  key: "12"
})

// value
{$$typeof: Symbol(react.element)
  key: "12"
  props: {}
  ref: null
  type: ƒ CounterButton(props)
}

reconcileChildren –> mountChildFibersCounter 组件创建 fiber 与创建 App 的 fiber 逻辑基本相同。所不同的是 effectTag 没有被标记。

beginWork Counter,renderWithHooks 返回的是 div,接着创建下一次 beginWork 的 fiber。

{$$typeof: Symbol(react.element)
  key: null
  props: {children: Array(2)}
  ref: null
  type: "div"
}

beginWork: HostComponent

case HostComponent:
  return updateHostComponent(current, workInProgress, renderExpirationTime);
// ReactDOMHostConfig.js
function shouldSetTextContent(type: string, props: Props): boolean {
  return (
    type === 'textarea' ||
    type === 'option' ||
    type === 'noscript' ||
    typeof props.children === 'string' ||
    typeof props.children === 'number' ||
    (typeof props.dangerouslySetInnerHTML === 'object' &&
      props.dangerouslySetInnerHTML !== null &&
      props.dangerouslySetInnerHTML.__html != null)
  );
}

// ReactFiberBeginWork.js
function updateHostComponent(
  current,
  workInProgress,
  renderExpirationTime,
) {
  const type = workInProgress.type
  const nextProps = workInProgress.pendingProps
  const prevProps = current !== null ? current.memoizedProps : null

  let nextChildren = nextProps.children
  const isDirectTextChild = shouldSetTextContent(type, nextProps)
  if (isDirectTextChild) {nextChildren = null} else if (...) {...}

  reconcileChildren(
    current,
    workInProgress,
    nextChildren,
    renderExpirationTime,
  );
  return workInProgress.child;
}

这里的 pendingProps,就是 div 的 props 为 span button 的数组。
shouldSetTextContent 则判断当前元素可不可以拥有子元素,或者 children 可以作为一个 text 节点 之后继续调用 reconcileChildren –> mountChildFibers

此时 nextChildren 是一个数组结构 在 ReactFiberChildreconcileChildFibers相应的代码:

if (isArray(newChild)) {
  return reconcileChildrenArray(
    returnFiber,
    currentFirstChild,
    newChild,
    expirationTime,
  );
}

function reconcileChildrenArray(
  returnFiber,
  currentFirstChild,
  newChildren,
  expirationTime,
) {
  let resultingFirstChild: Fiber | null = null;
  let previousNewFiber: Fiber | null = null;

  let oldFiber = currentFirstChild;
  let lastPlacedIndex = 0;
  let newIdx = 0;
  let nextOldFiber = null;

  for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {...}

  if (newIdx === newChildren.length) {...}

  if (oldFiber === null) {for (; newIdx < newChildren.length; newIdx++) {
      const newFiber = createChild(
        returnFiber,
        newChildren[newIdx],
        expirationTime,
      );
      if (newFiber === null) {continue;}
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      if (previousNewFiber === null) {
        // TODO: Move out of the loop. This only happens for the first run.
        resultingFirstChild = newFiber;
      } else {previousNewFiber.sibling = newFiber;}
      previousNewFiber = newFiber;
    }
    return resultingFirstChild;
  }
}

由于第一次创建 此时的 currentFirstChild 为 null,reconcileChildrenArray代码很多,但是第一次用到的不多,主要遍历 children 为它们创建 fiber,并添加到 fiber 树上。
最后返回第一个 child 的 fiber 也就是 span 对应的 fiber。

接着对 span 进行 beginWork,此时的isDirectTextChild 标志位为 true。nextChildren 则为 null。reconcileChildFibers结果返回 null。

此时回到 workLoop 的performUnitOfWork,因为 next 为 null,则进行下一步 completeUnitOfWork

completeUnitOfWork

function completeUnitOfWork(unitOfWork) {
  workInProgress = unitOfWork
  do {
    const current = workInProgress.alternate
    const returnFiber = workInProgress.return

    if ((workInProgress.effectTag & Incomplete) === NoEffect) {let next = completeWork(current, workInProgress, renderExpirationTime);

      if (next !== null) {return null}
      ...
      /* completeUnitOfWork-code-01 */
    } else {...}
    /* completeUnitOfWork-code-02 */
    const siblingFiber = workInProgress.sibling;
    if (siblingFiber !== null) {return siblingFiber;}
    workInProgress = returnFiber;
    /* completeUnitOfWork-code-02 */
  } while (workProgress !== null)
}

此时传入的 unitOfWork 为 span 对应的 fiber。将全局变量 workInProgress 赋值为unitWork

(workInProgress.effectTag & Incomplete) === NoEffect显然为 true。调用 completeWork 返回下一次的工作内容

completeWork

function completeWork(
  current,
  workInProgress,
  renderExpirationTime
) {
  const newProps = workInProgress.pendingProps
  switch (workInProgress.tag) {
    ...
    case HostComponent: {const rootContainerInfo = getRootHostContainer();
      const type = workInProgress.type;
      if (current !== null && workInProgress.stateNode != null) {...} else {const currentHostContext = getHostContext();
        let instance = createInstance(
          type,
          newProps,
          rootContainerInstance,
          currentHostContext,
          workInProgress,
        );

        appendAllChildren(instance, workInProgress, false, false);

        if (
            finalizeInitialChildren(
              instance,
              type,
              newProps,
              rootContainerInstance,
              currentHostContext,
            )
          ) {markUpdate(workInProgress);
          }
          workInProgress.stateNode = instance;
      }
    }
  }
  return null;
}

此处的 rootContainerInfo 先把他认为是div#app,继续忽略currentHostContext。创建过程可以理解为三步:

  1. createInstance:创建 dom 等
  2. appendAllChildren:将 children 的 host Component 添加到刚创建的 dom 上 组成 dom 树。
  3. finalizeInitialChildren:给 dom 设置属性。

先详细看一下 createInstance 实现

// ReactDOMComponentTree.js
export function updateFiberProps(node, props) {node[internalEventHandlersKey] = props;
}

export function precacheFiberNode(hostInst, node) {node[internalInstanceKey] = hostInst;
}

// ReactDOMHostConfig
function createInstance(
  type,
  props,
  rootContainerInstance,
  hostContext,
  internalInstanceHandle
) {
  const domElement: Instance = createElement(
    type,
    props,
    rootContainerInstance,
    parentNamespace,
  );
  precacheFiberNode(internalInstanceHandle, domElement);
  updateFiberProps(domElement, props);
  return domElement;
}

createElement先暂时理解为 document.createElement
precacheFiberNode则是 将 fiber 实例添加到 dom 上。
updateFiberProps 将 fiber 实例添加到 dom 上

虽然是一样将 fiber 添加到 dom 上 通过 key 的命名可以发现用途不同,updateFiberProps是为事件系统做准备的。internalInstanceKey估计就是为了保持引用,取值判断等用途

appendAllChildren 这里先跳过,到 complete div 的时候具体分析一下。

由于是第一次渲染也就不存在 diff props 的过程,这里的 finalizeInitialChildren 的职责也相对简单些,设置 dom 元素的一些初始值。在设置初始值的时候对应不同的 dom 元素有特殊的处理,这些部分我们也先跳过

export function finalizeInitialChildren(
  domElement: Instance,
  type: string,
  props: Props,
  rootContainerInstance: Container,
  hostContext: HostContext,
): boolean {setInitialProperties(domElement, type, props, rootContainerInstance);
  // return shouldAutoFocusHostComponent(type, props);
  ...
}

function setInitialProperties(
  domElement,
  tag,
  rawProps,
  rootContainerElement,
) {
  ...
  const isCustomComponentTag = true
  switch (tag) {...}
  setInitialDOMProperties(
    tag,
    domElement,
    rootContainerElement,
    props,
    isCustomComponentTag,
  );
}

function setInitialDOMProperties(
  tag,
  domElement,
  rootContainerElement,
  nextProps,
) {for (const propKey in nextProps) {if (!nextProps.hasOwnProperty(propKey)) {continue;}
    const nextProp = nextProps[propKey];
    if (propKey === STYLE) {...} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {...} else if (propKey === CHILDREN) {if (typeof nextProp === 'string') {
        const canSetTextContent = tag !== 'textarea' || nextProp !== '';
        if (canSetTextContent) {setTextContent(domElement, nextProp);
        }
      } else if (typeof nextProp === 'number') {setTextContent(domElement, '' + nextProp);
      }
    } else if (registrationNameModules.hasOwnProperty(propKey)) {...} else if (nextProp != null) {setValueForProperty(domElement, propKey, nextProp, isCustomComponentTag);
    }
  }
}

在设置 dom 属性的时候,有几个注意点 一个是 style 属性的设置 最终的 style 属性是字符串,而我们写的则是属性名是驼峰命名的对象。感兴趣的可自行查看 setValueForStyles。

span 的 children 属性是被当做文字节点设置

// setTextContent.js
function(node, text) {if (text) {
    let firstChild = node.firstChild;
    if (
      firstChild &&
      firstChild === node.lastChild &&
      firstChild.nodeType === TEXT_NODE
    ) {
      firstChild.nodeValue = text;
      return;
    }
  }
  node.textContent = text;
}

回到 completeWork,最后将创建的 dom 添加到 fiber 的stateNode 属性上,返回 null 结束 completeWork 调用

返回到 completeUnitOfWork/* completeUnitOfWork-code-01 */

/* completeUnitOfWork-code-01 */
if (
  returnFiber !== null
  && (returnFiber.effectTag & Incomplete) === NoEffect
) {if (returnFiber.effect === null) {returnFiber.firstEffect = workInProgress.firstEffect}

  if (workInProgress.lastEffect !== null) {if (returnFiber.lastEffect !== null) {returnFiber.lastEffect.nextEffect = workInProgress.firstEffect;}
    returnFiber.lastEffect = workInProgress.lastEffect;
  }

  const effectTag = workInProgress.effectTag;

  if (effectTag > PerformedWork) {if (returnFiber.lastEffect !== null) {returnFiber.lastEffect.nextEffect = workInProgress;} else {returnFiber.firstEffect = workInProgress;}
    returnFiber.lastEffect = workInProgress;
  }
}

将 span 节点的 effectList 归并到父组件上(但此时 span fiber 上并没有 effect), 此时子组件没有任何 effect,且 effectTag 为 0。

/* completeUnitOfWork-code-02 */
const siblingFiber = workInProgress.sibling;
if (siblingFiber !== null) {return siblingFiber;}
workInProgress = returnFiber;
/* completeUnitOfWork-code-02 */

/* completeUnitOfWork-code-02 */,如果当前节点有兄弟节点,则返回,没有则返回父节点继续 completeWork。
此时 span 有一个创建了 fiber 但是没有进行 beginWork 的兄弟节点button

button 节点经历过 beginWork, completeWork,又回到了/* completeUnitOfWork-code-02 */ 处。button 节点没有兄弟节点,workInProgress 被置为了 div 节点,进行
div 的 completeWork

div 的 completeWork 与 span 和 button 不同之处在于appendAllChildren,之前跳过的部分现在分析一下

function appendAllChildren(
  parent,
  workInProgress,
) {
  let node = workInProgress.child;
  while (node !== null) {if (node.tag === HostComponent || node.tag === HostText) {
      // condition 01
      appendInitialChild(parent, node.stateNode.instance);
    } else if (...*2) {} else if (node.child !== null) {
      // condition 03
      node.child.return = node;
      node = node.child;
      continue;
    }

    if (node === workInProgress) {return null}

    // condition 04
    while (node.sibling === null) {if (node.return === null || node.return === workInProgress) {return;}
      node = node.return;
    }
    node.sibling.return = node.return;
    node = node.sibling;
  }
}

div 的 child 为 span 且满足 condition 01,将 span 添加到 div 上,轮到 button fiber 同样将 button 添加到 div 上。
condition 04处 是当前的返回出口:找到最后一个 sibling,在向上查找到 div 节点 返回。

我们实际应用中,上述的 div>span-button 算是最简单操作。有很多想 div 与 span、button 又隔了一层 Function/Class Component。此时就需要利用到
condition 03 继续向 child 查找,查找各个分叉向下距离workInProgress 最近的 host 节点,将他们添加到 workInProgress 对应的 dom 上,这样 dom 树才能完整构成。

这样 divcompleteWork就完成了,继续到 Counter 组件:

Component组件的 completeWork 是直接被break,所以这里只需要将 effectList 归并到父节点。

/* completeUnitOfWork-code-02 */ 节点到 Counter 的 returnFiberApp 节点,App 节点与其他节点不同的地方在于其 effectTag 为 3。这是怎么来的尼?还记得我们的 root fiber 节点在 beginWork 时与其他节点不同的地方在于:它是有 current节点的,所以作为 children 的 App,在 placeSingleChild 的时候 effectTag 被添加了 Placement,在beginWorkmountIndeterminateComponent时,Component组件的 effectTag 被添加了PerformedWork

回归一下 /* completeUnitOfWork-code-01 */ 处代码,只有到 App 满足 effectTag > PerformedWork,在之前出现的 host 节点的effectTag 都为 0,Function 节点都为 1(PerformedWork),都不符合添加 effect 的要求。所以到此时才有一个effect,它被添加到了 root Fiber 上。

root fiber 的 completeWork,它的tagHostRoot

// ReactFiberCompleteWork.js

updateHostContainer = function (workInProgress) {// Noop};

case HostRoot: {
  ...
  if (current === null || current.child === null) {workInProgress.effectTag &= ~Placement;}
  // updateHostContainer(workInProgress)
}

这里 current.child 为 null,因为我们之前 beginWork 时,改变的是 workInProgress 节点,这里将 Placement effectTag 取消。结束 completeWork。

这时我们已经到达了 root 节点,做一些收尾工作

// ReactWorkLoop.js
function completeUnitOfWork(unitOfWork) {
  workInProgress = unitOfWork
  do {} while (workInProgress !== null)

  if (workInProgressRootExitStatus === RootIncomplete) {workInProgressRootExitStatus = RootCompleted;}
  return null;
}

workLoopSync结束之后,将执行上下文由 RenderContext 重置为上次的执行环境

root.finishedWork = root.current.alternate;
root.finishedExpirationTime = expirationTime;

之后将 workLoop 所做的工作添加到 root 的 finishedWork

workLoopSync部分,也可以成为 render 阶段到此结束。回顾一下在此期间所做的主要工作。

  • 创建各个节点对应的 workInProgress fiber 节点
  • 创建 dom 节点,设置属性,连接构成 dom 树(并未 append 到 container 上)
  • 为节点打上 effectTag,构建完整的 effectList 链表,从叶子节点归并到 root fiber 节点上。

commit 阶段

继续回来renderRoot

function commitRoot() {
  ...
  workInProgressRoot = null

  switch (workInProgressRootExitStatus) {
    case RootComplete: {
      ...
      return commitRoot.bind(null, root);
    }
  }
}

workInProgressRoot 置为 null,在 completeWork 时将 workInProgressRootExitStatus 置为了RootCompleted,之后进入 commitRoot 阶段。

暂不讨论优先级调度相关的代码, 完整代码戳我 这里看成:

function commitRoot(root) {commitRootImpl.bind(null, root, renderPriorityLevel)
  if (rootWithPendingPassiveEffects !== null) {flushPassiveEffects();
  }
  return null;
}
  • commitBeforeMutationEffects
  • commitMutationEffects
  • commitLayoutEffects

commitRoot 源码主要内容是以上遍历 effectList 的三个循环,看看他们做了什么吧


let nextEffect = null

function commitRootImpl(root, renderPriorityLevel) {
    const finishWork = root.finishWork
    const expirationTime = root.finishedExpirationTime
    ...

    root.finishedWork = null;
    root.finishedExpirationTime = NoWork;
    
    let firstEffect
    if (finishedWork.effectTag > PerformedWork) {
        // 将自身 effect 添加到 effect list 上
        ...
    }

    if (firstEffect !== null) {
        const prevExecutionContext = executionContext;
        executionContext |= CommitContext;
        
        do {
            try {commitBeforeMutationEffects();
            } catch (error) {..}
        } while (nextEffect !== null)

        ...

        ...
        nextEffect = null;
        executionContext = prevExecutionContext;
    }

}

先获取 effectList,在 render 阶段生成的 effect list 并不包含自身的 effect,这里先添加(但此时 finishedWork.effectTag 其实为 0),获取完整的 effectList。
之后把当前的执行上下文置为CommitContext, 正式进入 commit 阶段。

此时 effectList 其实就是 App 节点的 workInProgress fiber。这里有一个全局变量nextEffect 表示当前正在处理的 effect

commitBeforeMutationEffects

function commitBeforeMutationEffects() {while (nextEffect !== null) {if ((nextEffect.effectTag & Snapshot) !== NoEffect) {
            ...
            const current = nextEffect.alternate;
            commitBeforeMutationEffectOnFiber(current, nextEffect);
            ...
        }
        nextEffect = nextEffect.nextEffect;
  }
}

这个 App fiber 上的 effectTag 为 3(Placement | Update), 这个循环直接跳过了

function commitMutationEffects() {while (nextEffect !== null) {
        const effectTag = nextEffect.effectTag
        ...

        let primaryEffectTag = effectTag & (Placement | Update | Deletion)

        switch (primaryEffectTag) {
            ...
            case PlacementAndUpdate: {commitPlacement(nextEffect)
                nextEffect.effectTag &= ~Placement;

        // Update
        const current = nextEffect.alternate;
        commitWork(current, nextEffect);
            }
        }

        nextEffect = nextEffect.nextEffect;
    }
}

commitPlacement

commitPlacement主要是把 dom 元素添加到对应的父节点上,对于第一次渲染其实也只是将 div 添加到 div#app 上。并将当前的 effectTag update 去掉。

commitWork

// ReactFiberCommitWork.js
function commitWork(current, finishedWork) {switch (finishedWork.tag) {
        case FunctionComponent:
    case ForwardRef:
    case MemoComponent:
    case SimpleMemoComponent: {
      // Note: We currently never use MountMutation, but useLayout uses
      // UnmountMutation.
      commitHookEffectList(UnmountMutation, MountMutation, finishedWork);
            return;
        
        case HostComponent: {...}
    }
}

这里 commitWork 有涉及到 hook 组件的部分,这里暂时跳过。
对于 host 组件其实是有前后 props diff 的部分,这里是第一次渲染,所以也就不存在,所以这里也没有多少第一渲染需要做的工作。

commitLayoutEffects

// ReactFiberWorkLoop.js

import {commitLifeCycles as commitLayoutEffectOnFiber} from 'ReactFiberCommitWork'

function commitLayoutEffects() {while (nextEffect !== null) {
        const effectTag = nextEffect.effectTag;
        if (effectTag & (Update | Callback)) {recordEffect();
      const current = nextEffect.alternate;
      commitLayoutEffectOnFiber(
        root,
        current,
        nextEffect,
        committedExpirationTime,
      );
        }
        ...
        nextEffect = nextEffect.nextEffect
    }
    ...
}

App fiber 上的 effectTag 现在剩下 1(PerformedWork),并不符合所以当当循环也跳出。顺便一提,如果我们的 ReactDOM.render 有 callback 的话 将会在这里执行。

三个循环结束之后将 nextEffect 置为 null;执行上下文变更成之前的执行上下文。
function commitRootImpl() {
    ...
    if ((executionContext & LegacyUnbatchedContext) !== NoContext) {return null;}
}

现在我们的执行上下文还剩下在 upbatchedUpdate 添加的LegacyUnbatchedContext,所以这里直接返回。到这里我们第一渲染过程到这也就基本结束了。

总结一下 commit 工作:

  1. 处理 beginWork 产出 finishedWork 的 effectList
  2. 将 dom 添加到屏幕上(div#app container)
  3. callback 调用
  4. hooks 相关逻辑(未涉及)
  5. classComponent 的生命周期逻辑(未涉及)
  6. 其他

本文在走源码的时候也有有许多部分没有涵盖 或者直接跳过的地方:

  • 更新过程 hooks 组件更新 classComponent setState 更新
  • Hooks
  • ClassComponent、SimpleMemoComponent、HostPortal、SuspenseComponent、SuspenseListComponent 等
  • 事件相关
  • context ref 等
  • scheduler 模块
  • 其他

尾声

本文是笔者跟着源码 debugger 写出来的文章,对于缺失的部分,计划慢慢会有对应的介绍部分。另外本文属于流水账类型的文章,分析部分非常少,忘大家多多包涵、提提意见,你的参与就是我的动力。

正文完
 0