美文网首页IT技术篇前端让前端飞
探索React源码:Reconciler

探索React源码:Reconciler

作者: CBDxin | 来源:发表于2021-12-19 23:51 被阅读0次

    探索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);
    
      //...
    }
    
    未命名文件 (2).png

    这个遍历的过程实际上就是协调的整体过程,接下来我们来详细看看在新的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等需要持久化的数据;

    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中。
    1. 若 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

    相关文章

      网友评论

        本文标题:探索React源码:Reconciler

        本文链接:https://www.haomeiwen.com/subject/medaxrtx.html