一文读懂 react Fiber

React Fiber 发生的原因

要知道React Fiber发生的原因是什么,首先咱们得知道 React哲学,借用官网的话 React 是用 JavaScript 构建快速呼应的大型 Web 运用程序的首选办法。快速呼应是要害。那么制约网页快速呼应的因素有哪些呢? 一般来说影响网页快速呼应的有以下两类场景:

  • 发送网络请求后,由于需求等候数据回来才干进一步操作导致不能快速呼应。
  • 当遇到大核算量的操作或许设备功能不足使页面掉帧,导致卡顿。

这两类场景能够归纳为:

  • CPU的瓶颈
  • IO的瓶颈

React是怎么处理这两个瓶颈的呢?

CPU瓶颈

要处理 CPU 瓶颈,首先要理解什么是 “掉帧” 。咱们知道主流浏览器的改写频率为60HZ(1000ms/60HZ),即每16.6ms浏览器改写一次。咱们知道 JS 能够操作 DOM,GUI 烘托进程JS 线程是互拆的,所以JS脚本履行浏览器布局、制作不能一起履行。所以当一帧内js脚本履行占用过长时刻(超越16.6ms),就没有时刻去履行款式布局款式制作了,也就导致了所谓的掉帧。理解了这个原理,咱们就知道了处理问题的办法,那便是在浏览器每一帧的时刻中,预留一些时刻给JS线程,然后把控制权交还给烘托进程,让浏览器有剩余时刻去履行款式布局和制作。其间 React 预留的初始时刻为5ms源码。当预留的时刻不够用时,React将线程控制权交还给浏览器使其有时刻烘托UI,React则等候下一帧时刻到来持续被中止的作业。

这种将长使命拆分到每一帧中去履行每一段微小使命的操作被称为时刻切片(time slice)

因而要处理 CPU 瓶颈要害是要完结时刻切片时刻切片的要害是将同步更新变为可中止的异步更新

IO瓶颈

IO的瓶颈首要来源于网络延迟,但许多状况下前端开发者是无法处理的,如何在开发者无法处理的前提下减少网络延迟对用户的感知。 React 给出的答案是 将人机交互研究的成果整合到实在的 UI 中。为此 React 完结了 Suspense功能及配套的hook- useDeferredValue。为了完结这些特性,同样需求将同步更新变为可中止的异步更新

React 15及以下版别中,React 运用了一种被称为 Stack Reconciler 的调度算法,当组件的更新使命被调度后,它会一直履行到更新使命完结,期间不允许其他的使命搅扰。这种办法的优点是简单粗暴,可是也有显着的缺点,由于这会导致 UI 界面被卡死,失去了流畅性和呼应性。因而这种更新办法现已不能满足需求,此刻Fiber就在16版别中应运而生了。

什么是 React Fiber

React Fiber能够理解为一个履行单元(work unit) ,也能够说是一种新的数据结构,里面保存了保存了组件的tagkeytypestateNode等相关信息,用于表明组件树上的每个节点以及他们的关系。与传统的递归算法不同,在V16版中 Reconciler是根据 Fiber 节点完结的,被称为Fiber Reconciler,支撑可中止异步更新,使命支撑时刻切片。咱们知道React在数据更新时会有diff的操作,此刻diff的进程是被分成一小段一小段的,Fiber节点保存了每一阶段使命的作业进度,js会比较一小部分虚拟dom,然后让出主线程,交给浏览器去做其他操作,然后持续比较,如此循环往复,直至完结diff,然后一次性更新到视图上。

一文读懂 react Fiber

React Fiber 数据结构

从源码中咱们能够看到Fiber节点的界说Fiber节点界说如下:

  function FiberNode(this: $FlowFixMe,tag: WorkTag,pendingProps: mixed,key:null | string,mode: TypeOfMode,) {
      /**
      Instance 静态数据结构特点
      **/
      this.tag = tag; // Fiber组件类型 Function/Class....
      this.key = key; // key 特点,diff时需求
      this.elementType = null; // 大部分状况同 type,某些状况不同,比方 FunctionComponent 运用 React.memo 包裹
      this.type = null; // 关于 FunctionComponent,指函数本身,关于 ClassComponent,指 class,关于 HostComponent,指 DOM 节点的 tagName
      this.stateNode = null; // Fiber 对应的实在 DOM 节点
      /**
      链接Fiber节点构成Fiber树所需特点
      **/
      this.return = null; // 指向父级Fbier节点
      this.child = null; // 指向子Fiber节点
      this.sibling = null; // 指向兄弟Fiber节点
      this.index = 0; // 下标
      this.ref = null;
      this.refCleanup = null;
      /**
      Fiber 动态作业单元,保存本次更新相关信息
      **/ 
      this.pendingProps = pendingProps; // 当时组件特点
      this.memoizedProps = null; // 指向最近一次运用props
      this.updateQueue = null; // 更新行列
      this.memoizedState = null; // 组件状况
      this.dependencies = null; // 依靠组件数据
      // 运转形式。React Fiber 支撑两种形式,别离是 "concurrent"(并发形式)和 "legacy"(留传形式)。
      // 在并发形式下,React Fiber 会采取一系列优化战略,使组件的更新能够在多线程环境中异步地履行,然后提高运用的功能和用户体会。
      // 而在留传形式下,React Fiber 会选用与 React 16 及之前版别相同的同步更新办法,即组件更新会阻塞浏览器主线程的运转,直到更新完结后才干持续其他操作。
      this.mode = mode;
      /**
      Effects 副作用相关
      **/ 
      this.flags = NoFlags; // 当时组件符号位,表明组件的操作类型和更新战略
      this.subtreeFlags = NoFlags; // 当时组件子树的符号位
      this.deletions = null; // 保存需求删去的fiber对象链表
      /**
      调度优先级
      ***/
      this.lanes = NoLanes; // 当时组件的优先级
      this.childLanes = NoLanes; // 子组件的优先级
      this.alternate = null; // 指向该fiber在另一次更新时对应的fiber
}

fiber 作业原理

经过以上咱们知道:

  • 当组件树层级很深时,React 会一次性遍历整颗组件树履行更新操作,导致功能瓶颈。
  • 当时的调度战略是不可中止的,也便是说React履行中心无法打断。在大型运用中,假如履行使命时刻太长,会导致页面呈现卡顿现象,并影响用户体会。

React Fiber 经过分片(slicing)和优先级调度(priority scheduling)来处理上述问题,然后完结了高效的组件更新和异步烘托。React Fiber 的作业原理能够归纳为以下几个步骤:

  1. 构建 Fiber 树

    React Fiber 会创立一棵 Fiber 树,用于表明React组件树的结构和状况。Fiber 树是一个轻量级的树形结构,与 React 组件树一一对应。与传统的递归遍历不同,React Fiber 选用链表结构对树进行分片拆分,完结递增烘托的作用。

  2. 确认调度优先级

    Fiber 树构建完结后,React Fiber 会依据组件的更新状况和优先级,确认需求优先更新的组件,即“调度”更新。React Fiber 支撑多个优先级,组件的优先级由组件的更新状况和所处的位置决定。比方页面的顶部和底部能够具有不同的优先级,用户的交互行为比自动更新的优先级更高,等等。

  3. 履行调度更新

    当确认了需求调度更新的组件后,React Fiber 会将这些组件符号为“脏”(dirty),并将它们放入更新行列中,待后续处理。需求留意的是,React Fiber 并未立即履行更新操作,而是等候时刻片到来时才开端履行,这样能够让 React Fiber 在履行更新时具有更高的优先级,提高了运用的呼应性和功能。

  4. 中止和康复

    在履行更新时,假如需求中止当时使命,React Fiber 能够依据当时使命的优先级、履行时刻和剩余时刻等因素,自动中止当时使命,并将现场保存到堆栈中。当下次处理到该使命的时分,React Fiber 能够经过康复堆栈中保存的现场信息,持续履行使命,然后完结中止和康复的作用。

  5. 烘托和提交

    React Fiber 会将更新成果烘托到页面中,并设置下一次更新的时刻和优先级。React Fiber 利用 WebGL canvas 等浏览器原生的制作 API,完结了GPU加快,然后提高了烘托效率和功能。

此外,React 运用了一种叫做双缓存的技能,何谓是双缓存呢?双缓存技能跟动画领域有关系,在核算机上的动画跟实践的动画不一样,实践的动画都是先画好了,播放的时分直接拿出来显现就行。核算机动画则是画一张,就拿出来一张,再画下一张,再拿出来。假如所需求制作的图形很简单,那么这样也没什么问题。但一旦图形比较复杂,制作需求的时刻较长,问题就会变得突出。

举个例子,当咱们用canvas制作动画,每一帧制作前都会调用ctx.clearRect清除上一帧的画面。 假如当时帧画面核算量比较大,导致清除上一帧画面到制作当时帧画面之间有较长间隙,就会呈现白屏。 为了处理这个问题,咱们能够在内存中制作当时帧动画,制作完毕后直接用当时帧替换上一帧画面,由于省去了两帧替换间的核算时刻,不会呈现从白屏到呈现画面的闪耀状况。这种在内存中构建并直接替换的技能叫做双缓存技能React运用“双缓存”来完结Fiber树的构建与替换——对应着DOM树的创立与更新。

React中最多一起存在两颗Fiber树,当时显现的Fiber树称为current Fiber 树,内存中构建的Fiber 树,称为workInProgress Fiber树。这两颗树中的Fiber节点别离被称为current fiber workInProgress fiber,它们经过alternate特点链接,每次状况更新都会发生新的workInProgress Fiber树。

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

React经过运用current指针在不同Fiber树中的rootFiber间切换来完结current Fiber树的指向的改换。接下来咱们用组件的mount/update两个周期来展现创立/更新流程。 用如下组件做演示:

    const App = () => {
        const [num, setNum] = useState(0);
        return <div onClick={() => setNum(num + 1)}>{num}</div>
    }
    ReactDOM.render(<App/>, document.getElementById('root'));
  • Mount时

    • 首次履行时:履行办法ReactDOM.render,创立fiberRootrootFiber,其间fiberRoot为整个运用的根节点,rootFiber<App/>地点组件树的根节点。一个运用能够屡次调用ReactDOM.render创立多个rootFiber节点,但只要一个根节点,那便是fiberRoot,当时current指针指向当时fiber树,示意图如下:

      一文读懂 react Fiber

          fiberRoot.current = rootFiber
      
    • 进入render阶段:React 依据组件回来的jsx在内存中顺次创立fiber节点并连接起来构成Fiber树,在内存中创立的Fiber树workInProgress树,在此进程中React会尝试复用current Fiber树中已有的Fiber节点中的特点。

      一文读懂 react Fiber

    • 随后进入commit阶段:右侧现已构建完的 workInProgress Fiber树会替换掉当时的 Fiber 树,烘托到页面。此刻fiberRootcurrent指针指向workInProgress Fiber树,使其成为current fiber树

      一文读懂 react Fiber

  • Update

    接下来咱们来看看更新阶段时的详细进程:当咱们点击div节点时触发状况改动,此刻num值从0变为1。此刻React会开启新的render进程并创立一颗新的workInProgress Fiber树,此刻workInProgress Fiber树会尝试复用current Fiber树对应节点的数据。当然是否复用其间就涉及到react diff算法了。

一文读懂 react Fiber

此刻render阶段完结后会进入commit阶段烘托到页面上,烘托完结后 workInProgress Fiber 树变为current Fiber 树

一文读懂 react Fiber

Fiber作业核心是双缓存技能,其创立和更新的进程伴随着DOM的更新。

fiber 源码剖析

前面咱们说了React Fiber的发生的原因、什么是Fiber及作业原理。接下来咱们咱们从代码层面来看看Fiber从创立到履行的进程。其经历两个阶段,别离是render阶段commit阶段

render阶段

首先会调用performSyncWorkOnRoot performConcurrentWorkOnRoot,同步更新调用performSyncWorkOnRoot,异步更新调用performConcurrentWorkOnRoot

 // performSyncWorkOnRoot
 function workLoopSync() { 
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}
 //  performConcurrentWorkOnRoot
 function workLoopConcurrent() {
  // Perform work until Scheduler asks us to yield
  while (workInProgress !== null && !shouldYield()) {
    // $FlowFixMe[incompatible-call] found when upgrading Flow
    performUnitOfWork(workInProgress);
  }
}

这两个办法的区别是否调用shouldYield,表明假如当时帧没有空余时刻,shouldYield会停止循环,直至浏览器有空余时刻再康复遍历。两个办法都调用了performUnitOfWork,接下来看看performUnitOfWork办法。

function performUnitOfWork(unitOfWork: Fiber): void {
  const current = unitOfWork.alternate;
  let next;
  // 存在unitOfWork.child,不会处理sibling
  if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
    next = beginWork(current, unitOfWork, renderLanes);
  } else {
    next = beginWork(current, unitOfWork, renderLanes);
  }
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  // 回来下一个待处理的fiber
  if (next === null) {
   // 履行归办法,收集副作用
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }
  ReactCurrentOwner.current = null;
}

从这个办法办法中能够看出render阶段能够分为两个阶段。阶段:首先从rootFiber节点开端向下深度优先遍历。每个fiber节点调用beginWork办法,该办法会依据传入的fiber节点创立子fiber节点并将他们链接起来。当遍历到该链路的叶子节点时(没有子组件),进入阶段:履行completeUnitOfWork办法,该办法调用completeWork处理fiber节点。当某个Fiber节点履行完completeUnitOfWork,假如其存在兄弟Fiber节点(即fiber.sibling !== null),会进入其兄弟Fiber阶段。假如不存在兄弟Fiber,会进入父级Fiber阶段。“递”和“归”阶段会交织履行直到“归”到rootFiber

下面咱们举个例子,看上述流程:

function App() {
  return (
    <div>
      <div>欢迎来到</div>
      <span>jackbtone的博客</span>
    </div>
  )
}
ReactDOM.render(<App />, document.getElementById("root"));

对应的fiber节点结构如下

一文读懂 react Fiber

接下来我看看下在Render阶段所涉及的几个要害办法。

  • beginWork
function beginWork(current: Fiber | null,workInProgress: Fiber,renderLanes:Lanes,): Fiber | null {
  const updateLanes = workInProgress.lanes; // 获取更新优先级
   // 更新时复用上一个fiber节点的数据(优化功能) 
   if (current !== null) {
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;
    if (
      oldProps !== newProps ||
      hasLegacyContextChanged() ||
      // 热加载从头烘托
      (__DEV__ ? workInProgress.type !== current.type : false)
    ) {
      // 上下文特点发生变化,给fiber节点打个符号
      didReceiveUpdate = true;
    } else if (!includesSomeLane(renderLanes, updateLanes)) {
      // 两个节点持平,撤销设置
      didReceiveUpdate = false;
      switch (workInProgress.tag) {
        //....
      }
      // 复用current节点
      return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
    } else {
      if ((current.effectTag & ForceUpdateForLegacySuspense) !== NoEffect) {
        didReceiveUpdate = true;
      } else {
        didReceiveUpdate = false;
      }
    }
  } else {
    didReceiveUpdate = false;
  }
  workInProgress.lanes = NoLanes;
  // mount时依据不同类型tag创立不同的子Fiber节点
  switch (workInProgress.tag) {
    case IndeterminateComponent: {
      // ...
    }
    case LazyComponent: {
      // ...
    }
    // case.....
  }
   总结:该办法承受三个参数
   1.current: 当时组件对应的fiber节点在上次更新时对应的fiber节点(workInProgress.alternate)
   2.workInProgress:当时组件对应的fiber节点
   3.renderLanes: 优先级相关参数。
   由于组件初次烘托时 current === null,依据 fiber.tag类型不同,创立不同的子fiber节点。组件更新时 current !== null。此刻能够复用 current 节点,current.child 作为 workInProgress.child。

其间详细 tag 类型能够点击这里查看。

  • completeUnitOfWork
function completeUnitOfWork(unitOfWork: Fiber): void {
// 完结正在处理的作业单元, 然后转到下一个兄弟节点,假如没有兄弟节点则回来父节点Fiber
let completedWork: Fiber = unitOfWork;
do {
    // 当时处理的Fiber节点
    const current = completedWork.alternate;
    // 父级 Fiber
    const returnFiber = completedWork.return;
    // 创立next
    let next;
    next = completeWork(current, completedWork, renderLanes);
    if (next !== null) {  
       // 假如完结此 fiber 生成了新的作业,则持续处理该作业。  
       workInProgress = next;  
       return;  
    }  
    const siblingFiber = completedWork.sibling;  
    if (siblingFiber !== null) {  
        // 判别是否遍历兄弟节点组件树  
        workInProgress = siblingFiber;  
        return;  
    }  
     // 回来父节点Fiber  
    completedWork = returnFiber;  
    // Update the next thing we're working on in case something throws.  
    workInProgress = completedWork;  
   } while (completedWork !== null);
}
该段代码做了如下几件事
1. 创立下一个作业单元:将当时节点的 alternate 特点(用于存储 Fiber 节点的上一次状况)赋值给 current 变量,将当时节点的父级 Fiber 节点赋值给 returnFiber 变量,并调用 completeWork 函数来完结该节点的作业单元。
2. 处理下一个作业单元:假如当时节点的 completeWork 函数回来的是非空 Fiber 节点,则阐明该节点生成了新的作业单元,需求持续处理
3. 回来父级节点:假如当时节点不存在子节点或许兄弟节点,则阐明该作业单元现已被处理完毕,需求回溯到父级节点持续处理。为此,将 completedWork 变量指向上一级节点,重置 workInProgress 变量,并循环履行上述操作,直到一切的作业单元都被处理完结
  • completeWork
function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  const newProps = workInProgress.pendingProps;
  // 不同tag类型处理逻辑不通
  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: {
     // ...
    }
    case HostRoot: {
     // ...
    }
    case HostComponent: {
    // ....
    }
    case HostText: {
      // ...
    }
    // case....
  }
}
该段代码首要做了如下几件事
1. 获取新特点值:从 workInProgress.pendingProps 特点中获取节点的新特点值 newProps。。
2. 依据节点类型处理逻辑:依据节点类型的不同,调用的函数也不同,可是它们的目的都是生成一个新的 Fiber 节点,用于符号该节点的更新状况。
3. 回来新的 Fiber 节点:在处理完节点之后,completeWork 函数会回来生成的新的 Fiber 节点。假如节点是被删去或许没有改变,则回来 null

接下来咱们剖析页面烘托所用的hostComponent类型,即原生DOM组件所对应的fiber节点。详细代码如下

case HostComponent: {
  popHostContext(workInProgress);
  const rootContainerInstance = getRootHostContainer();
  const type = workInProgress.type;
  if (current !== null && workInProgress.stateNode != null) {
    // ...upadte 更新时
     updateHostComponent(
          current,
          workInProgress,
          type,
          newProps,
          rootContainerInstance,
        );
     // ...
  } else {
      // ... mount时 
      if (!newProps) {
          // ...
          return null;
      }
      // ... 
  }
  return null;
 }

能够看到completeWork办法分为updatemount两种状况,判别依据也是依据 current === null 判别。一起关于hostComponent,新加了判别条件workInProgress.stateNode !== null(该fiber节点是否存在对应的dom节点)。

update

由于fiber节点现已存在对应的DOM节点,不需求生成新的DOM节点。首要是处理Prop数据:

  • 注册回调函数OnClick、onChange
  • 处理 style prop
  • 处理 children prop

其间首要是调用updateHostComponent办法。

const updateHostComponent = function(
    current: Fiber,
    workInProgress: Fiber,
    type: Type,
    newProps: Props,
    rootContainerInstance: Container,
  ) {
    const oldProps = current.memoizedProps;
    if (oldProps === newProps) {
      return;
    }
    const instance: Instance = workInProgress.stateNode;
    const currentHostContext = getHostContext();
    const updatePayload = prepareUpdate(
      instance,
      type,
      oldProps,
      newProps,
      rootContainerInstance,
      currentHostContext,
    );
    workInProgress.updateQueue = (updatePayload: any);
    if (updatePayload) {
      markUpdate(workInProgress);
    }
  };

经过阅览源码能够发现,在updateHostComponent内部,被处理完的props会被赋值给workInProgress.updateQueue,并终究会在commit阶段被烘托在页面上。

mount
case HostComponent: {
  popHostContext(workInProgress);
  const rootContainerInstance = getRootHostContainer();
  const type = workInProgress.type;
  if (current !== null && workInProgress.stateNode != null) {
   // update
    if (current.ref !== workInProgress.ref) {
      markRef(workInProgress);
    }
  } else {
  // mount 
  const currentHostContext = getHostContext();
  // 创立对应DOM节点
  const instance = createInstance(
    type,
    newProps,
    rootContainerInstance,
    currentHostContext,
    workInProgress,
  );
  // 将后代DOM节点刺进刚生成的DOM节点中
  appendAllChildren(instance, workInProgress, false, false)
  // DOM节点赋值给fiber.stateNode
  workInProgress.stateNode = instance;
  // 处理props数据
   if (
    finalizeInitialChildren(
      instance,
      type,
      newProps,
      rootContainerInstance,
      currentHostContext,
    )
  ) {
    markUpdate(workInProgress);
  }
}

经过阅览上述源码能够发现mount时做了以下工作:

  • Fiber节点生成对应的DOM节点
  • 将后代DOM节点刺进刚生成的DOM节点中
  • 处理props数据
effectList

到此Render阶段的大部分作业现已完结了,但需求留意的是在completeWork的上层函数completeUnitOfWork中,每个履行完completeWork且存在effectTagFiber节点会被保存在一条被称为effectList的单向链表中。 effectList中第一个Fiber节点保存在fiber.firstEffect,终究一个元素保存在fiber.lastEffect。在“归”阶段,一切有effectTagFiber节点都会被追加在effectList中,终究构成一条以rootFiber.firstEffect为起点的单向链表。源码如下:

 if (returnFiber !== null &&
    // 假如某个兄弟节点未完结,则不将作用附加到其父级
 (returnFiber.effectTag & Incomplete) === NoEffect) {
    // 将子树的一切操作和该 fiber 的操作附加到父级的effect list中。子级完结的顺序会影响副作用的顺序。
    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 具有副作用,则将其附加在子级的副作用之后。
   // 假如需求,咱们能够经过对作用列表进行屡次传递来提前履行某些副作用。
   // 咱们不想在咱们自己的列表上安排咱们自己的副作用,由于假如咱们终究重用了子级,
   // 咱们将在自己身上安排此副作用,由于咱们处于列表的末尾。
    const effectTag = completedWork.effectTag;
    // 在创立 effect 列表时,越过 NoWork 和 PerformedWork 符号。
    if (effectTag > PerformedWork) {
      if (returnFiber.lastEffect !== null) {
        returnFiber.lastEffect.nextEffect = completedWork;
      } else {
        returnFiber.firstEffect = completedWork;
      }
      returnFiber.lastEffect = completedWork;
    }
}

借用React团队成员Dan Abramov的话:effectList相较于Fiber树,就像圣诞树上挂的那一串彩灯。

commit 阶段

进入commit阶段的函数是 commitRoot(root),其间root传入的是fiberRootNode,在rootFiber.firstEffect上保存了一条需求履行副作用Fiber节点的单向链表effectList,这些Fiber节点updateQueue中保存了变化的props。这些副作用对应的DOM操作commit阶段履行。

commit阶段也是分为三部分:

  • before mutation阶段(履行DOM操作前)
  • mutation阶段(履行DOM操作)
  • layout阶段(履行DOM操作后)

首要逻辑如下:

function commitRoot(root) {
  const renderPriorityLevel = getCurrentPriorityLevel();
  runWithPriority(
    ImmediateSchedulerPriority,
    commitRootImpl.bind(null, root, renderPriorityLevel),
  );
  return null;
}
function commitRootImpl(root, renderPriorityLevel) {
  do {
    flushPassiveEffects();
  } while (rootWithPendingPassiveEffects !== null);
  if (firstEffect !== null) {
    focusedInstanceHandle = prepareForCommit(root.containerInfo);
    shouldFireAfterActiveInstanceBlur = false;
    // before mutation阶段
    commitBeforeMutationEffects(finishedWork);
    // .....
    // mutation阶段
    commitMutationEffects(finishedWork, root, renderPriorityLevel);
    //....
    // layout阶段
    commitLayoutEffects(finishedWork, root, lanes);
  } else {
    //.....
  }
  const rootDidHavePassiveEffects = rootDoesHavePassiveEffects;
  return null;
}

从以上代码能够看出不同阶段代表着调用不同的办法,接下来剖析不同阶段调用的详细办法。

before mutation

before mutation 便是遍历effectList并调用 commitBeforeMutationEffects 函数处理。这里摘录了其间要害代码如下:

// 处理一切的 mutation effects,即一切的DOM操作,如删去、刺进、替换等
function commitBeforeMutationEffects(firstChild: Fiber) {
  let fiber = firstChild; // 初始化fiber变量
  while (fiber !== null) {
    if (fiber.deletions !== null) {
      // 处理有待删去的节点
      commitBeforeMutationEffectsDeletions(fiber.deletions);
    }
    // 有子节点的状况
    if (fiber.child !== null) {
      // 获取子树符号中 BeforeMutation 标志位的状况
      const primarySubtreeTag = fiber.subtreeTag & BeforeMutation;
      if (primarySubtreeTag !== NoSubtreeTag) {
        // 递归调用 commitBeforeMutationEffects 函数处理子节点的 mutation effects。
        commitBeforeMutationEffects(fiber.child);
      }
    }
    if (__DEV__) {
      // ......
    } else {
      try {
        // 处理当时节点的 mutation effects
        commitBeforeMutationEffectsImpl(fiber);
      } catch (error) {
        captureCommitPhaseError(fiber, error);
      }
    }
    // 将fiber变量更新为下一个兄弟节点(fiber.sibling)
    fiber = fiber.sibling;
  }
}

其间 commitBeforeMutationEffects 办法如下:

function commitBeforeMutationEffectsImpl(fiber: Fiber) {
  // 当时fiber节点及副作用类型
  const current = fiber.alternate;
  const effectTag = fiber.effectTag;
  if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) {
    // 对焦点事情进行处理
  }
  // 履行Snapshot副作用。假如当时 fiber 节点存在`Snapshot`类型的副作用,则设置当时 debug fiber,并履行`commitBeforeMutationEffectOnFiber`函数,终究重置当时 debug fiber;
  if ((effectTag & Snapshot) !== NoEffect) {
    setCurrentDebugFiberInDEV(fiber);
    // commitBeforeMutationEffectOnFiber 办法内部调用 getSnapshotBeforeUpdate,
    commitBeforeMutationEffectOnFiber(current, fiber);
    resetCurrentDebugFiberInDEV();
  }
  // 调度 useEffect,履行 Passive 副作用
  if ((effectTag & Passive) !== NoEffect) {
    if (!rootDoesHavePassiveEffects) {
      // 设置根节点特点 rootDoesHavePassiveEffects 为true
      rootDoesHavePassiveEffects = true;
      scheduleCallback(NormalSchedulerPriority, () => {
        // 调用 flushPassiveEffects 函数,将一切Passive类型的副作用进行批量处理
        flushPassiveEffects();
        return null;
      });
    }
  }
}

这段代码的首要作用是在 React 中对异步烘托时的副作用进行处理,确保界面的正确性和功能。首要做了以下几件事:

  • 处理DOM节点烘托/删去后的autoFocusblur逻辑。
  • 调用getSnapshotBeforeUpdate生命周期钩子。
  • 调度useEffect
调用 getSnapshotBeforeUpdate

commitBeforeMutationEffectOnFibercommitBeforeMutationLifeCycles的别名。 在该办法内会调用 getSnapshotBeforeUpdate。 删减后首要代码如下:

function commitBeforeMutationLifeCycles(
  current: Fiber | null,
  finishedWork: Fiber,
): void {
  // 经过不同类型的type履行不同操作
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case SimpleMemoComponent:
    case Block: {
      return;
    }
     // 处理类组件
    case ClassComponent: {
        // 判别是否需求履行 “Snapshot” 类型的副作用。经过调用instance.getSnapshotBeforeUpdate()获取组件在更新前的状况快照并记载
        if (finishedWork.effectTag & Snapshot) {
            // ......
            const snapshot = instance.getSnapshotBeforeUpdate(
            finishedWork.elementType === finishedWork.type
              ? prevProps
              : resolveDefaultProps(finishedWork.type, prevProps),
            prevState,
          );
        }
    }
    case HostRoot: {
      if (supportsMutation) {
        // 假如存在“Snapshot”类型的副作用,则清空根节点的容器信息
        if (finishedWork.effectTag & Snapshot) {
          const root = finishedWork.stateNode;
          clearContainer(root.containerInfo);
        }
      }
      return;
    }
    case HostComponent:
    case HostText:
    case HostPortal:
    case IncompleteClassComponent:
      // Nothing to do for these component types
      return;
  }
}

getSnapshotBeforeUpdate 生命周期的作用是回来要在更新后存储在实例上的值,以备更新后的组件实例中轻松康复它们的状况。在该函数中,通常会处理组件的 DOM 元素,核算它们在更新前的位置、大小、款式等信息。只要 ClassComponent 类型的组件和 HostRoot 类型的根节点有可能会在更新前需求记载状况快照。其他类型的组件都不需求处理,能够直接回来。究其原因,是由于Stack Reconciler重构为Fiber Reconciler后,render阶段的使命可能中止/从头开端,对应的组件在render阶段的生命周期钩子(即componentWillXXX)可能触发屡次。

调度UseEffect
if ((effectTag & Passive) !== NoEffect) {
    if (!rootDoesHavePassiveEffects) {
      // 设置根节点特点 rootDoesHavePassiveEffects 为true
      rootDoesHavePassiveEffects = true;
      scheduleCallback(NormalSchedulerPriority, () => {
        // 调用 flushPassiveEffects 函数,将一切Passive类型的副作用进行批量处理
        flushPassiveEffects();
        return null;
      });
    }
}

其间 scheduleCallbackReact内部运用的一个调度器,用来调度使命的履行。由Scheduler模块提供,该函数将flushPassiveEffects函数添加到处理行列中,并在浏览器空闲时履行。其间在在 flushPassiveEffects 办法内部会遍历rootWithPendingPassiveEffects(即effectList)履行effect回调函数(其间effectList中保存了需求履行副作用的Fiber节点,包括节点的刺进、更新、删去等)。 综上所述在 before mutation阶段,会遍历effectList,顺次履行

  • 处理DOM节点烘托/删去后的autoFocusblur逻辑。
  • 调用getSnapshotBeforeUpdate生命周期钩子。
  • 调度useEffect
mutation阶段

从上面能够知道在mutation阶段阶段调用的是办法 commitMutationEffects,办法如下:

function commitMutationEffects(
  firstChild: Fiber,
  root: FiberRoot,
  renderPriorityLevel,
) {
  let fiber = firstChild;
  // 遍历
  while (fiber !== null) {
      try {
        commitMutationEffectsImpl(fiber, root, renderPriorityLevel);
      } catch (error) {
       // ......
      }
    }
    fiber = fiber.sibling;
  }
}

其间首要调用了办法 commitMutationEffectsImpl,接下来咱们看看该办法做了什么事,代码如下:

function commitMutationEffectsImpl(
  fiber: Fiber,
  root: FiberRoot,
  renderPriorityLevel,
) {
  // 该办法第三个参数表明提交的 effect 的优先级 
  // 获取该fiber节点副作用类型
  const effectTag = fiber.effectTag;
  if (effectTag & ContentReset) {
    // 依据 effectTag 和 ContentReset 将文本内容重置为初始值
    commitResetTextContent(fiber);
  }
  // 更新ref 
  if (effectTag & Ref) {
    const current = fiber.alternate;
    if (current !== null) {
      commitDetachRef(current);
    }
    if (enableScopeAPI) {
      // TODO: This is a temporary solution that allows us to transition away
      // from React Flare on www.
      if (fiber.tag === ScopeComponent) {
        commitAttachRef(fiber);
      }
    }
  }
  // 依据 effectTag 类型别离履行相应的操作
  const primaryEffectTag = effectTag & (Placement | Update | Hydrating);
  switch (primaryEffectTag) {
    // 刺进DOM
    case Placement: {
      commitPlacement(fiber);
      // 移除 effectTag 类型
      fiber.effectTag &= ~Placement;
      break;
    }
    // 刺进并更新 DOM
    case PlacementAndUpdate: {
      // Placement 刺进
      commitPlacement(fiber);
      fiber.effectTag &= ~Placement;
      // Update  更新
      const current = fiber.alternate;
      commitWork(current, fiber);
      break;
    }
    // SSR相关
    case Hydrating: {
      fiber.effectTag &= ~Hydrating;
      break;
    }
     // SSR相关
    case HydratingAndUpdate: {
      fiber.effectTag &= ~Hydrating;
      // Update
      const current = fiber.alternate;
      commitWork(current, fiber);
      break;
    }
    // 更新
    case Update: {
      const current = fiber.alternate;
      commitWork(current, fiber);
      break;
    }
  }
}

经过阅览源码咱们能够知道commitMutationEffects办法会遍历effectList,并对每个Fiber节点做以下三件事:

  • 依据ContentReset effectTag重置文字节点
  • 更新ref
  • 依据effectTag履行不同的操作。

可见在 mutation阶段 会遍历 effectList,顺次履行commitMutationEffects。依据effectTag调用不同的处理函数处理Fiber。在以上履行完相应的操作之后,就完结了关于该 Fiber Mutation Effects 的提交。值得留意的是,假如该 FibereffectTag 中仍包括任何一个首要标志(Placement、Update、Hydrating)时,这部分标志在履行相应的操作之后都会被从effectTag中移除,防止屡次履行。

layout阶段

从上剖析可知layout阶段履行的函数是 commitLayoutEffects,其实也是遍历effectList,履行一系列函数。

function commitLayoutEffects(
  firstChild: Fiber,
  root: FiberRoot,
  committedLanes: Lanes,
) {
  let fiber = firstChild;
  while (fiber !== null) {
     // 处理子节点
    if (fiber.child !== null) {
      // 子树符号和布局符号
      const primarySubtreeTag = fiber.subtreeTag & Layout;
      if (primarySubtreeTag !== NoSubtreeTag) {
        commitLayoutEffects(fiber.child, root, committedLanes);
      }
    }
    // .....
    try {
        // 履行布局effect
        commitLayoutEffectsImpl(fiber, root, committedLanes);
    } catch (error) {
        captureCommitPhaseError(fiber, error);
    }
    fiber = fiber.sibling;
  }
}

以上代码很简单,递归履行 commitLayoutEffects办法,该办法内部调用了commitLayoutEffectsImpl,源码如下:

function commitLayoutEffectsImpl(
  fiber: Fiber,
  root: FiberRoot,
  committedLanes: Lanes,
) {
  // 获取副作用类型
  const effectTag = fiber.effectTag;
  // 设置调试形式
  setCurrentDebugFiberInDEV(fiber);
  // 调用生命周期函数和hook,是否存在回掉函数和更新操作
  if (effectTag & (Update | Callback)) {
    const current = fiber.alternate;
    // 提交布局
    commitLayoutEffectOnFiber(root, current, fiber, committedLanes);
  }
   // enableScopeAPI 表明是否启用 Scope API
  if (enableScopeAPI) {
    if (effectTag & Ref && fiber.tag !== ScopeComponent) {
      commitAttachRef(fiber);
    }
  } else {
     // 赋值ref特点 
    if (effectTag & Ref) {
      commitAttachRef(fiber);
    }
  }
  resetCurrentDebugFiberInDEV();
}

从以上剖析可知函数 commitLayoutEffectsImpl,承受三个参数别离为fiberrootcommittedLanes。其间 committedLanes 表明提交优先级。其实便是做了两件事

  • 调用生命周期钩子hook相关操作,完结函数为commitLayoutEffectOnFiber
  • 赋值 ref,其完结函数为commitAttachRef

commitLayoutEffectOnFiber 其实内部也是依据不同类型节点履行不同操作。commitAttachRef 则是获取dom实例更新ref,感兴趣的能够自己去阅览下,到此整个commit阶段也就完毕了。

fiber 总结

React Fiber 是 React 中的一个重要特性,它能够让 React 更高效地处理烘托使命和其他使命,并提高页面的功能和呼应速度。React Fiber 的完结原理根据时刻切片、使命调度和优先级办理等技能,经过分割使命、调度履行和办理状况等办法,完结了高效的组件烘托和更新流程。一起,React Fiber 还引入了Concurrent Mode、Suspense、Hooks和函数组件等新特性,能够让咱们更方便地办理组件的状况和生命周期函数,减少代码的冗余和复杂度。

参阅文章

  1. React 官网
  2. 知悉Fiber,方能百战不殆~
  3. React Fiber 源码解析