likes
comments
collection
share

探索React源码:Reconciler

作者站长头像
站长
· 阅读数 24

探索React源码:初探React fiber一文我们提到:

React16之后,React的架构可以分成三层

  • Scheduler(调度)
  • Reconciler(协调)
  • Renderer(渲染)

其中Reconciler(协调器)的作用是收集变化的组件,最终让Renderer(渲染器)将变化的组件渲染的页面当中。这个收集变化的组件的过程我们称为render(协调)阶段。在此阶段,React会遍历current fiber tree并将fiber节点与对应的React element进行对比(也就是我们常说的diff),构造出新的fiber tree —— workInProgress fiber tree。今天我们就来了解一下render阶段的工作流程。

Reconciler起作用的阶段我们称为render阶段,Renderer起作用的阶段我们称为commit阶段

双缓存机制

双缓存机制是一种在内存中构建并直接替换的技术。协调的过程中就使用了这种技术。

在React中最多同时存在两棵fiber tree。一棵是当前在屏幕上显示的dom对应的fiber tree,称为current fiber tree,而另一棵是当触发新的更新任务时,React在内存中构建的fiber tree,称为workInProgress fiber tree

current fiber treeworkInProgress fiber tree中的fiber节点通过alternate属性进行连接。

currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;

React应用的根节点中也存在current属性,利用current属性在不同fiber tree的根节点之间进行切换的操作,就能够完成current fiber tree与workInProgress fiber tree之间的切换。

在协调阶段,React利用diff算法,将产生update的React elementcurrent fiber tree中对应的节点进行比较,并最终在内存中生成workInProgress fiber tree。随后Renderer会依据workInProgress fiber tree将update渲染到页面上。同时根节点的current属性会指向workInProgress fiber tree,此时workInProgress fiber tree就变为current fiber tree。

fiber tree的遍历流程

引入fiber后,fiber tree的遍历过程:(不需要完全看懂,只需要看懂遍历的流程就好)

// 执行协调的循环
function workLoopConcurrent() {
  // Perform work until Scheduler asks us to yield
  //shouldYield为Scheduler提供的函数, 通过 shouldYield 返回的结果判断当前是否还有可执行下一个工作单元的时间
  while (workInProgress !== null && !shouldYield()) {
    workInProgress = performUnitOfWork(workInProgress);
  }
}

function performUnitOfWork(unitOfWork: Fiber): void {
  //...

  let next;
  //...
  //对当前节点进行协调,如果存在子节点,则返回子节点的引用
  next = beginWork(current, unitOfWork, subtreeRenderLanes);

  //...

  //如果无子节点,则代表当前的child链表已经遍历完
  if (next === null) {
    // If this doesn't spawn new work, complete the current work.
    //此函数内部会帮我们找到下一个可执行的节点
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }

  //...
}

function completeUnitOfWork(unitOfWork: Fiber): void {
  let completedWork = unitOfWork;
  do {
    //...

    //查看当前节点是否存在兄弟节点
    const siblingFiber = completedWork.sibling;
    if (siblingFiber !== null) {
      // If there is more work to do in this returnFiber, do that next.
      //若存在,便把siblingFiber节点作为下一个工作单元,继续执行performUnitOfWork,执行当前节点并尝试遍历当前节点所在的child链表
      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);

  //...
}

探索React源码:Reconciler

这个遍历的过程实际上就是协调的整体过程,接下来我们来详细看看在新的fiber节点是如何被创建的以及新的fiber树是怎样构建出来的。

performSyncWorkOnRoot/performConcurrentWorkOnRoot

协调阶段的入口为performSyncWorkOnRoot(legacy模式)或performConcurrentWorkOnRoot(concurrent 模式)。

// performSyncWorkOnRoot会调用该方法
function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

// performConcurrentWorkOnRoot会调用该方法
function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

这两个方法会将生成workInProgress的下一级的fiber节点,并将workInProgress的第一个子fiber节点赋值给workInProgress。新的workInProgress会与已创建的fiber节点连接起来构成workInProgress fiber tree

他们俩唯一的区别就是在判断是否需要继续遍历时,performConcurrentWorkOnRoot会在判断是否存在下一工作单元workInProgress的基础上,还会通过Scheduler模块提供的shouldYield方法来询问当前浏览器是否有充足的时间来执行下一工作单元。

三种链表的遍历

引入fiber前,React遍历节点的方式是 n 叉树的深度优先遍历,而引入fiber后,从fiber tree的遍历过程我们能够知道,React将遍历的方法从原来的 n 叉树的深度优先遍历改变为对多种单向链表的遍历:

  • 由 fiber.child 连接的父 -> 子链表的遍历
  • 由 fiber.return 连接的子 -> 父链表的遍历
  • 由 fiber.sibling 连接的兄 -> 弟链表的遍历

这三种链表的遍历主要通过beginWorkcompleteWork两个方法进行,我们来重点分析一下这两个方法。

beginWork

beginWork的执行路径是workInProgress fiber tree中所有的父 -> 子链表。beginWork会根据传入的fiber节点创建出当前workInProgress fiber节点的所有次级workInProgress fiber节点(这些次级节点会通过fiber.sibling进行连接),并将当前workInProgress fiber节点于次级的第一个workInProgress fiber通过fiber.child属性连接起来。

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  // ...
}

beginWork接收三个参数:

  • current:当前组件在current fiber tree中对应的fiber节点,即workInProgress.alternate
  • workInProgress:当前组件在workInProgerss fiber tree中对应的fiber节点,即current.alternate
  • renderLanes:此次render的优先级;

我们知道,current fiber treeworkInProgress fiber tree中的fiber节点通过alternate属性进行连接的。

组件在mount时,由于是首次渲染,workInProgress fiber tree中除了根节点fiberRootNode之外,其余节点都不存在上一次更新时的fiber节点,也就是说,在mount时,workInProgress fiber tree中除了根节点之外,所有节点的alternate都为空。所以在mount时,除了根节点fiberRootNode之外,其余节点调用beginWork时参数current等于null

而update时,workInProgress fiber tree所有节点都存在上一次更新时的fiber节点,所以current !== null。

beginWork在mount和update时会分别执行不同分支的工作。我们可以通过 current === null 作为条件,判断组件是处于mount还是update。随后会根据当前的workInProgress.tag的不同,进入到不同的分支执行创建子Fiber节点的操作。

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  //...

  if (current !== null) {
    //update时
    //...
  } else {
    //mount时
    didReceiveUpdate = false;
  }

  //...

  //根据tag不同,创建不同的子Fiber节点
  switch (workInProgress.tag) {
    case IndeterminateComponent: 
      // ...省略
    case LazyComponent: 
      // ...省略
    case FunctionComponent: 
      // ...省略
    case ClassComponent: 
      // ...省略
    case HostRoot:
      // ...省略
    case HostComponent:
      // ...省略
    case HostText:
      // ...省略
    // ...省略其他类型
  }
}

update时的beginWork

此时workInProgress存在对应的current节点,当currentworkInProgress满足一定条件时,我们可以复用current节点的子节点的作为workInProgress的子节点,反之则需要进入对比(diff)的流程,根据比对的结果创建workInProgress的子节点。

beginWork在创建fiber节点的过程中中会依赖一个didReceiveUpdate变量来标识当前的current是否有更新。

在满足下面的几种情况时,didReceiveUpdate === false:

  1. 未使用forceUpdate,且oldProps === newProps && workInProgress.type === current.type && !hasLegacyContextChanged() ,即props、fiber.type和context都未发生变化

  2. 未使用forceUpdate,且!includesSomeLane(renderLanes, updateLanes),即当前fiber节点优先级低于当前更新的优先级

const updateLanes = workInProgress.lanes;
if (current !== null) {
  //update时
  const oldProps = current.memoizedProps;
  const newProps = workInProgress.pendingProps;
  if (
    oldProps !== newProps ||
    hasLegacyContextChanged() ||
    (__DEV__ ? workInProgress.type !== current.type : false)
  ) {
    didReceiveUpdate = true;
  } else if (!includesSomeLane(renderLanes, updateLanes)) {
    // 本次的渲染优先级renderLanes不包含fiber.lanes, 表明当前fiber节点优先级低于本次的渲染优先级,不需渲染
    didReceiveUpdate = false;
    //...
    // 虽然当前节点不需要更新,但需要使用bailoutOnAlreadyFinishedWork循环检测子节点是否需要更新
    return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
  } else {
    if ((current.effectTag & ForceUpdateForLegacySuspense) !== NoEffect) {
      // forceUpdate产生的更新,需要强制渲染
      didReceiveUpdate = true;
    } else {
      didReceiveUpdate = false;
    }
  }
} else {
  //mount时
  //...
}

mount时的beginWork

由于在mount时,直接将didReceiveUpdate赋值为false。

const updateLanes = workInProgress.lanes;
if (current !== null) {
  //update时
  //...
} else {
  //mount时
  didReceiveUpdate = false;
}

此处mount和update的不同主要体现在在didReceiveUpdate的赋值逻辑的不同, 后续进入diff阶段后,针对mount和update,diff的逻辑也会有所差别。

updateXXX

beginWork会根据当前的workInProgress.tag的不同,进入到不同的分支执行创建子Fiber节点的操作。

switch (workInProgress.tag) {
  case IndeterminateComponent: 
    // ...
  case LazyComponent: 
    // ...
  case FunctionComponent: 
    // ...
  case ClassComponent: 
    // ...
  case HostRoot:
    // ...
  case HostComponent:
    // ...
  case HostText:
    // ...
  // ...
}

各个分支中的updateXXX函数的逻辑大致相同,主要经历了下面的几个步骤:

  1. 计算当前workInProgressfiber.memoizedStatefiber.memoizedProps fiber.stateNode等需要持久化的数据;

  2. 获取下级ReactElement对象,根据实际情况, 设置fiber.effectTag

  3. 根据ReactElement对象, 调用reconcilerChildren生成下级fiber子节点,并将第一个子fiber节点赋值给workInProgress.child。同时,根据实际情况, 设置fiber.effectTag

我们以updateHostComponent为例进行分析。HostComponent代表原生的 DOM 元素节点(如div,span,p等节点),这些节点的更新会进入updateHostComponent

function updateHostComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
) {
  //...

  //1. 状态计算, 由于HostComponent是无状态组件, 所以只需要收集 nextProps即可, 它没有 memoizedState
  const type = workInProgress.type;
  const nextProps = workInProgress.pendingProps;
  const prevProps = current !== null ? current.memoizedProps : null;
  // 2. 获取下级`ReactElement`对象
  let nextChildren = nextProps.children;
  const isDirectTextChild = shouldSetTextContent(type, nextProps);

  if (isDirectTextChild) {
    // 如果子节点只有一个文本节点, 不用再创建一个HostText类型的fiber
    nextChildren = null;
  } else if (prevProps !== null && shouldSetTextContent(type, prevProps)) {
  // 特殊操作需要设置fiber.effectTag 
    workInProgress.effectTag |= ContentReset;
  }
  // 特殊操作需要设置fiber.effectTag 
  markRef(current, workInProgress);
  // 3. 根据`ReactElement`对象, 调用`reconcilerChildren`生成`fiber`子节点,并将第一个子fiber节点赋值给workInProgress.child。
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  return workInProgress.child;
}

在各个updateXXX函数中,会判断当前节点是否需要更新,如果不需要更新则会进入bailoutOnAlreadyFinishedWork,并使用bailoutOnAlreadyFinishedWork的结果作为beginWork的返回值,提前beginWork,而不需要进入diff阶段。

常见的不需要更新的情况

  1. updateClassComponent时若!shouldUpdate && !didCaptureError
  2. updateFunctionComponent时若current !== null && !didReceiveUpdate
  3. updateMemoComponent时若compare(prevProps, nextProps) && current.ref === workInProgress.ref
  4. updateHostRoot时若nextChildren === prevChildren

bailoutOnAlreadyFinishedWork

bailoutOnAlreadyFinishedWork内部先会判断!includesSomeLane(renderLanes, workInProgress.childLanes)是否成立。

若!includesSomeLane(renderLanes, workInProgress.childLanes)成立,则所有的子节点都不需要更新,或更新的优先级都低于当前更新的渲染优先级。此时以此节点为头节点的整颗子树都可以直接复用。此时会跳过整颗子树,并使用null作为beginWork的返回值(进入回溯的逻辑);

若不成立,则表示虽然当前节点不需要更新,但当前节点存在某些fiber子节点需要在此次渲染中进行更新,则复用current fiber生成workInProgress的次级节点;

function bailoutOnAlreadyFinishedWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  //...

  if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
    // renderLanes 不包含 workInProgress.childLanes
    // 所有的子节点都不需要在本次更新进行更新操作,直接跳过,进行回溯
    return null;
  } 

  //...

  // 虽然此节点不需要更新,此节点的某些子节点需要更新,需要继续进行协调
  cloneChildFibers(current, workInProgress);
  return workInProgress.child;
}

effectTag

上面我们介绍到在updateXXX的主要逻辑中,在获取下级ReactElement以及根据ReactElement对象, 调用reconcilerChildren生成fiber子节点时,都会根据实际情况,进行effectTag的设置。那么effrctTag的作用到底是什么呢?

我们知道,Reconciler的目的之一就是负责找出变化的组件,随后通知Renderer需要执行的DOM操作,effectTag正是用于保存要执行DOM操作的具体类型的。 effectTag通过二进制表示:

//...
// 意味着该Fiber节点对应的DOM节点需要插入到页面中。
export const Placement = /*                    */ 0b000000000000010;
//意味着该Fiber节点需要更新。
export const Update = /*                       */ 0b000000000000100;
export const PlacementAndUpdate = /*           */ 0b000000000000110;
//意味着该Fiber节点对应的DOM节点需要从页面中删除。
export const Deletion = /*                     */ 0b000000000001000;
//...

通过这种方式保存effectTag可以方便的使用位操作为fiber赋值多个effect以及判断当前fiber是否存在某种effect。

React 的优先级 lane 模型中同样使用了二进制的方式来表示优先级。

reconcileChildren

在各个updateXXX函数中,会根据获取到的下级ReactElement对象, 调用reconcilerChildren生成当前workInProgress fiber节点的下级fiber子节点。

在双缓冲机制中我们介绍到:

在协调阶段,React利用diff算法,将产生update的ReactElementcurrent fiber tree中对应的节点进行比较,并最终在内存中生成workInProgress fiber tree。随后Renderer会依据workInProgress fiber tree将update渲染到页面上。同时根节点的current属性会指向workInProgress fiber tree,此时workInProgress fiber tree就变为current fiber tree。

diff的过程就是在reconcileChildren中发生的。

本文的重点是Reconciler进行协调的过程,我们只需要了解reconcileChildren函数的目的,不会对reconcileChildren中的diff算法的实现做更深入的了解,对React的diff算法感兴趣的同学可阅读探索React源码:React Diff

reconcileChildren也会通过current === null 区分mount与update,再分别执行不同的工作:

export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes
) {
  if (current === null) {
    // 对于mount的组件
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes,
    );
  } else {
    // 对于update的组件
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes,
    );
  }
}

mountChildFibersreconcileChildFibers的都是通过ChildReconciler生成的。他们的不同点在于shouldTrackSideEffects参数的不同,当shouldTrackSideEffects为true时会为生成的fiber节点收集effectTag属性,反之不会进行收集effectTag属性。

这样做的目的是提升commit阶段的效率。如果mountChildFibers也会赋值effectTag,由于mountChildFibers的节点都是首次渲染的,所以他们的effectTag都会收集到Placement effectTag。那么commit阶段在执行DOM操作时,会导致每个fiber节点都需要进行插入操作。为了解决这个问题,在mount时只有根节点会进行effectTag的收集,在commit阶段只会执行一次插入操作。

export const reconcileChildFibers = ChildReconciler(true);
export const mountChildFibers = ChildReconciler(false);

function ChildReconciler(shouldTrackSideEffects) {
  //...

  function reconcileChildrenArray(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChildren: Array<*>,
    lanes: Lanes,
  ): Fiber | null { 
    //... 
  }

  //...

  function reconcileSingleElement(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    element: ReactElement,
    lanes: Lanes,
  ): Fiber {
    //... 
  }

  //...

  function reconcileChildFibers(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChild: any,
    lanes: Lanes,
  ): Fiber | null {
    //... 
  }

  return reconcileChildFibers;
}

ChildReconciler内部定义了许多用于操作fiber节点的函数,并最终会使用一个名为 reconcileChildFibers 的函数作为返回值。这个函数的主要目的是生成当前workInProgress fiber节点的下级fiber节点,并将第一个子fiber节点作为本次beginWork返回值。

reconcileChildFibers的执行过程中除了向下生成子节点之外,还会进行下列的操作:

  1. 把即将要在commit阶段中要对dom节点进行的操作(如新增,移动: Placement, 删除: Deletion)收集到effectTag中;
  2. 对于被删除的fiber节点, 除了节点自身的effectTag需要收集Deletion之外, 还要将其添加到父节点的effectList中(正常effectList的收集是在completeWork中进行的, 但是被删除的节点会脱离fiber树, 无法进入completeWork的流程, 所以在beginWork阶段提前加入父节点的effectList)。

在遍历的流程中我们可以看到,beginWork返回值不为空时,会把该值赋值给workInProgress,作为下一次的工作单元,即完成了父 -> 子链表中的一个节点的遍历。beginWork返回值为空时我们将进入completeWork

completeUnitOfWork

beginWork返回值为空时,代表在遍历父->子链表的过程中发现当前链表已经无下一个节点了(也就是已遍历完当前父->子链表),此时会进入到completeUnitOfWork函数。

completeUnitOfWork主要做了以下几件事情:

  1. 调用completeWork

  2. 用于进行父节点的effectList的收集:

    • 把当前 fiber 节点的 effectList 合并到父节点的effectList中。
    • 若当前 fiber 节点存在存在副作用(增,删,改), 则将其加入到父节点的effectList中。
  3. 沿着此节点所在的兄 -> 弟链表查看其是否拥有兄弟fiber节点(即fiber.sibling !== null),如果存在,则进入其兄弟fiber父 -> 子链表的遍历(即进入其兄弟节点的beginWork阶段)。如果不存在兄弟fiber,会通过子 -> 父链表回溯到父节点上,直到回溯到根节点,也即完成本次协调。

function completeUnitOfWork(unitOfWork: Fiber): void {
  let completedWork = unitOfWork;
  // 此循环控制fiber节点向父节点回溯
  do {
    const current = completedWork.alternate;
    const returnFiber = completedWork.return;
    if ((completedWork.flags & Incomplete) === NoFlags) {
      let next;
      //  使用completeWork处理Fiber节点,后面再详细分析completeWork
      next = completeWork(current, completedWork, subtreeRenderLanes); // 处理单个节点
      if (next !== null) {
        // Suspense类型的组件可能回派生出其他节点, 此时回到`beginWork`阶段进行处理此节点
        workInProgress = next;
        return;
      }
      // 重置子节点的优先级
      resetChildLanes(completedWork);
      if (
        returnFiber !== null &&
        (returnFiber.flags & Incomplete) === NoFlags
      ) {
        // 将此节点的effectList合并到到父节点的effectList中
        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;
        }
        // 若当前 fiber 节点存在存在副作用(增,删,改), 则将其加入到父节点的`effectList`中。
        const flags = completedWork.flags;
        if (flags > PerformedWork) {
          if (returnFiber.lastEffect !== null) {
            returnFiber.lastEffect.nextEffect = completedWork;
          } else {
            returnFiber.firstEffect = completedWork;
          }
          returnFiber.lastEffect = completedWork;
        }
      }
    } else {
      // 异常处理
      //...
    }

    const siblingFiber = completedWork.sibling;
    if (siblingFiber !== null) {
      // 如果有兄弟节点, 则将兄弟节点作为下一个工作单元,进入到兄弟节点的beginWork阶段
      workInProgress = siblingFiber;
      return;
    }
    // 若不存在兄弟节点,则回溯到父节点
    completedWork = returnFiber;
    workInProgress = completedWork;
  } while (completedWork !== null);
  // 已回溯到根节点, 设置workInProgressRootExitStatus = RootCompleted
  if (workInProgressRootExitStatus === RootIncomplete) {
    workInProgressRootExitStatus = RootCompleted;
  }
}

completeWork

completeWork的作用包括:

  1. 为新增的 fiber 节点生成对应的DOM节点。

  2. 更新DOM节点的属性。

  3. 进行事件绑定。

  4. 收集effectTag。

beginWork类似,completeWork针对不同fiber.tag也会进入到不同的逻辑处理分支。

function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  const newProps = workInProgress.pendingProps;

  switch (workInProgress.tag) {
    case IndeterminateComponent:
    case LazyComponent:
    case SimpleMemoComponent:
    case FunctionComponent:
    case ForwardRef:
    case Fragment:
    case Mode:
    case Profiler:
    case ContextConsumer:
    case MemoComponent:
      return null;
    case ClassComponent: {
      // ...
      return null;
    }
    case HostRoot: {
      // ...
      return null;
    }
    case HostComponent: {
      // ...
      return null;
    }
  // ...
}

我们继续以HostComponent类型的节点为例,进行分析。

在处理HostComponent时,我们同样需要区分当前节点是需要进行新建操作还是更新操作。但与beginWork阶段判断mount还是update不同的是,判断节点是否需要更新时,除了要满足 current !== null 之外,我们还需要考虑workInProgress.stateNode节点是否为null,只有当current !== null && workInProgress.stateNode != null时,我们才会进行更新操作。

个人猜测,待验证:beginWork阶段mount的节点的stateNode属性为空,并且进入到了completeWork阶段才会被赋值。若在该节点进入到beginWork阶段之后,进入到completeWork阶段前的这段时间内,出现了更高优先级的更新中断了此次更新的情况,就有可能出现current !== null,但workInProgress.stateNode == null的情况,此时需要进行新建操作。

更新时

进入更新逻辑的fiber节点的stateNode属性不为空,即已经存在对应的DOM节点。这时候我们只需要更新DOM节点的属性并进行相关effectTag的收集。

if (current !== null && workInProgress.stateNode != null) {
  updateHostComponent(
    current,
    workInProgress,
    type,
    newProps,
    rootContainerInstance,
  );

  // ref更新时,收集Ref effectTag
  if (current.ref !== workInProgress.ref) {
    markRef(workInProgress);
  }
}

updateHostComponent

updateHostComponent用于更新DOM节点的属性并在当前节点存在更新属性,收集Update effectTag。

updateHostComponent = function(
  current: Fiber,
  workInProgress: Fiber,
  type: Type,
  newProps: Props,
  rootContainerInstance: Container,
) {
  // props没有变化,跳过对当前节点的处理
  const oldProps = current.memoizedProps;
  if (oldProps === newProps) {
    return;
  }

  const instance: Instance = workInProgress.stateNode;
  const currentHostContext = getHostContext();

  // 计算需要变化的DOM节点属性,并存储到updatePayload 中,updatePayload 为一个偶数索引的值为变化的prop key,奇数索引的值为变化的prop value的数组。
  const updatePayload = prepareUpdate(
    instance,
    type,
    oldProps,
    newProps,
    rootContainerInstance,
    currentHostContext,
  );

  // 将updatePayload挂载到workInProgress.updateQueue上,供后续commit阶段使用
  workInProgress.updateQueue = (updatePayload: any);
 
  // 若updatePayload不为空,即当前节点存在更新属性,收集Update effectTag
  if (updatePayload) {
    markUpdate(workInProgress);
  }
};

我们可以看到,需要变化的prop会被存储到updatePayload 中,updatePayload 为一个偶数索引的值为变化的prop key,奇数索引的值为变化的prop value的数组。并最终挂载到挂载到workInProgress.updateQueue上,供后续commit阶段使用。

prepareUpdate

prepareUpdate内部会调用diff方法用于计算updatePayload。

export function prepareUpdate(
  instance: Instance,
  type: string,
  oldProps: Props,
  newProps: Props,
  rootContainerInstance: Container,
  hostContext: HostContext,
): null | Object {
  const viewConfig = instance.canonical.viewConfig;
  const updatePayload = diff(oldProps, newProps, viewConfig.validAttributes);

  instance.canonical.currentProps = newProps;
  return updatePayload;
}

diff方法内部实际是通过diffProperties方法实现的,diffProperties会对lastPropsnextProps进行对比:

  1. 对 input/option/select/textarea 的 lastProps & nextProps 做特殊处理,此处和React受控组件的相关,不做展开。

  2. 遍历 lastProps:

    • 当遍历到的prop属性在 nextProps 中也存在时,那么跳出本次循环(continue)。若遍历到的prop属性在 nextProps 中不存在,则进入下一步。
    • 特殊处理style,判断当前prop是否为 style prop ,若不是,进入下一步,若是,则将 style prop 整理到styleUpdates中,其中styleUpdates为以style prop的key值为key,''(空字符串)为value的对象,用于清空style属性。
    • 由于进入到此步骤的prop在 nextProps 中不存在,将此类型的prop整理进updatePayload,并赋值为null,表示删除此属性。
  3. 遍历 nextProps:

    • 当遍历到的prop属性 与 lastProp 相等,即更新前后没有发生变化,跳过。
    • 特殊处理style,判断当前prop是否为 style prop ,若不是,进入下一步,若是,整理到 styleUpdates 变量中,其中styleUpdates为以style prop的key值为key,tyle prop的 value 为value的对象,用于更新style属性。
    • 特殊处理 DANGEROUSLY_SET_INNER_HTML
    • 特殊处理 children
    • 若以上场景都没命中,直接把 prop 的 key 和值都整理到updatePayload中。
  4. 若 styleUpdates 不为空,则将styleUpdates作为style prop 的值整理到updatePayload中。

新建时

进入新建逻辑的fiber节点的stateNode属性为空,不存在对应的DOM节点。相比于更新操作,我们需要做更多的事情:

  1. 为 fiber 节点生成对应的 DOM 节点,并赋值给stateNode属性。

  2. 将子孙DOM节点插入刚生成的DOM节点中。

  3. 处理 DOM 节点的所有属性以及事件回调。

  4. 收集effectTag。

if (current !== null && workInProgress.stateNode != null) {
  // 更新操作
  // ...
} else {
    // 新建操作
    // 创建DOM节点
    const instance = createInstance(
      type,
      newProps,
      rootContainerInstance,
      currentHostContext,
      workInProgress,
    );

    // 将子孙DOM节点插入刚生成的DOM节点中
    appendAllChildren(instance, workInProgress, false, false);

    // 将DOM节点赋值给stateNode属性
    workInProgress.stateNode = instance;

    // 处理 DOM 节点的所有属性以及事件回调
    if (
      finalizeInitialChildren(
        instance,
        type,
        newProps,
        rootContainerInstance,
        currentHostContext,
      )
    ) {
      markUpdate(workInProgress);
    }
}

createInstance

createInstance负责给fiber节点生成对应的DOM节点。

export function createInstance(
  type: string,
  props: Props,
  rootContainerInstance: Container,
  hostContext: HostContext,
  internalInstanceHandle: Object,
): Instance {
  let parentNamespace: string;
  // ...

  // 创建 DOM 元素
  const domElement: Instance = createElement(
    type,
    props,
    rootContainerInstance,
    parentNamespace,
  );

  // 在DOM节点中挂载一个指向 fiber 节点对象的指针
  precacheFiberNode(internalInstanceHandle, domElement);
  // 在 DOM节点中挂载一个指向 props 的指针
  updateFiberProps(domElement, props);
  return domElement;
}

appendAllChildren

appendAllChildren负责将子孙DOM节点插入刚生成的DOM节点中。

  appendAllChildren = function(
    parent: Instance,
    workInProgress: Fiber,
    needsVisibilityToggle: boolean,
    isHidden: boolean,
  ) {
    // 获取workInProgress的子fiber节点
    let node = workInProgress.child;

    // 当存在子节点时,去往下遍历
    while (node !== null) {
      if (node.tag === HostComponent || node.tag === HostText) {
        // 当node节点为HostComponent后HostText时,直接插入到子DOM节点列表的尾部
        appendInitialChild(parent, node.stateNode);
      } else if (enableFundamentalAPI && node.tag === FundamentalComponent) {
        appendInitialChild(parent, node.stateNode.instance);
      } else if (node.tag === HostPortal) {
        // 当node节点为HostPortal类型的节点,什么都不做
      } else if (node.child !== null) {
        // 上面分支都没有命中,说明node节点不存在对应DOM,向下查找拥有stateNode属性的子节点
        node.child.return = node;
        node = node.child;
        continue;
      }
      if (node === workInProgress) {
        // 回溯到workInProgress时,以添加完所有子节点
        return;
      }

      // 当node节点不存在兄弟节点时,向上回溯
      while (node.sibling === null) {
        // 回溯到workInProgress时,以添加完所有子节点
        if (node.return === null || node.return === workInProgress) {
          return;
        }
        node = node.return;
      }
      
      // 此时workInProgress的第一个子DOM节点已经插入到进入workInProgress对应的DOM节点了,开始进入node节点的兄弟节点的插入操作
      node.sibling.return = node.return;
      node = node.sibling;
    }
  };

  function appendInitialChild(parentInstance: Instance, child: Instance | TextInstance): void {
    parentInstance.appendChild(child);
  }

我们在介绍beginWork时介绍过,在mount时,为了避免每个fiber节点都需要进行插入操作,在mount时,只有根节点会收集effectTag,其余节点不会进行effectTag的收集。由于每次执行appendAllChildren后,我们都能得到一棵以当前workInProgress为根节点的DOM树。因此在commit阶段我们只需要对mount的根节点进行一次插入操作就可以了。

finalizeInitialChildren

function finalizeInitialChildren(
  domElement: Instance,
  type: string,
  props: Props,
  rootContainerInstance: Container,
  hostContext: HostContext,
): boolean {
  // 此方法会将 DOM 属性挂载到 DOM 节点上,并进行事件绑定
  setInitialProperties(domElement, type, props, rootContainerInstance);
  // 返回 props.autoFocus 的值
  return shouldAutoFocusHostComponent(type, props);
}

effectList

我们在介绍completeUnitOfWork函数的时候提到,他的其中一个作用是用于进行父节点的effectList的收集: - 把当前 fiber 节点的 effectList 合并到父节点的effectList中。 - 若当前 fiber 节点存在存在副作用(增,删,改), 则将其加入到父节点的effectList中。

  // 将此节点的effectList合并到到父节点的effectList中
  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;
  }
  // 若当前 fiber 节点存在存在副作用(增,删,改), 则将其加入到父节点的`effectList`中。
  const flags = completedWork.flags;
  if (flags > PerformedWork) {
    if (returnFiber.lastEffect !== null) {
      returnFiber.lastEffect.nextEffect = completedWork;
    } else {
      returnFiber.firstEffect = completedWork;
    }
    returnFiber.lastEffect = completedWork;
  }

effectList是一条用于收集存在effectTag的fiber节点的单向链表。React使用fiber.firstEffect表示挂载到此fiber节点的effectList的第一个fiber节点,使用fiber.lastEffect表示挂载到此fiber节点的effectList的最后一个fiber节点。

effectList存在的目的是为了提升commit阶段的工作效率。在commit阶段,我们需要找出所有存在effectTag的fiber节点并依次执行effectTag对应操作。为了避免在commit阶段再去做遍历操作去寻找effectTag不为空的fiber节点,React在completeUnitOfWork函数调用的过程中提前把所有存在effectTag的节点收集到effectList中,在commit阶段,只需要遍历effectList,并执行各个节点的effectTag的对应操作就好。

render阶段结束

completeUnitOfWork的回溯过程中,如果completedWork === null,说明workInProgress fiber tree中的所有节点都已完成了completeWorkworkInProgress fiber tree已经构建完成,至此,render阶段全部工作完成。

后续我们将回到协调阶段的入口函数performSyncWorkOnRoot(legacy模式)或performConcurrentWorkOnRoot(concurrent 模式)中,调用commitRoot(root)(其中root为fiberRootNode)来开启commit阶段的工作流程。

探索React源码系列文章

探索React源码:初探React fiber

探索React源码:React Diff

探索React源码:Reconciler