react16源码(Fiber架构)

react16-Fiber架构:改变了之前react的组件渲染机制,新的架构使原来同步渲染的组件现在可以异步化,可中途中断渲染,执行更高优先级的任务,释放浏览器主线程。

  React 核心算法的更新 —— 这次更新提供了一个从底层重写了 React 的 reconciliation 算法(译注:reconciliation 算法,是 React 用来比较两棵 DOM 树差异、从而判断哪一部分应当被更新的算法)。这次算法重写带来的主要特性是异步渲染。异步渲染的意义在于能够将渲染任务划分为多块。浏览器的渲染引擎是单线程的,这意味着几乎所有的行为都是同步发生的。React 16 使用原生的浏览器 API 来间歇性地检查当前是否还有其他任务需要完成,从而实现了对主线程和渲染过程的管理。在之前的版本中,React 会在计算 DOM 树的时候锁住整个线程。这个 reconciliation 的过程现在被称作 “stack reconciliation”。尽管 React 已经是以快而闻名了,但是锁住整个线程也会让一些应用运行得不是很流畅。16 这个版本通过不要求渲染过程在初始化后一次性完成修复了该问题。React 计算了 DOM 树的一部分,之后将暂停渲染,来看看主线程是否有任何的绘图或者更新需要去完成。一旦绘图和更新完成了,React 就会继续渲染。这个过程通过引入了一个新的,叫做 “fiber” 的数据结构完成,fiber 映射到了一个 React 实例并为该实例管理其渲染任务,它也知道它和其他 fiber 之间的关系。

  react16以前的组件渲染方式存在一个问题,如果这是一个很大,层级很深的组件,react渲染它需要几十甚至几百毫秒,在这期间,react会一直占用浏览器主线程,任何其他的操作(包括用户的点击,鼠标移动等操作)都无法执行。好似一个潜水员,当它一头扎进水里,就要往最底层一直游,直到找到最底层的组件,然后他再上岸。在这期间,岸上发生的任何事,都不能对他进行干扰,如果有更重要的事情需要他去做(如用户操作),也必须得等他上岸。fiber架构—组件的渲染顺序:潜水员会每隔一段时间就上岸,看是否有更重要的事情要做。加入fiber的react将组件更新分为两个时期(phase 1 && phase 2),render前的生命周期为phase1,render后的生命周期为phase2。 

  phase1的生命周期是可以被打断的,每隔一段时间它会跳出当前渲染进程,去确定是否有其他更重要的任务。此过程,React 在 workingProgressTree (并不是真实的virtualDomTree)上复用 current 上的 Fiber 数据结构来一步步地构建新的 tree,标记需要更新的节点,放入队列中。phase2的生命周期是不可被打断的,React 将其所有的变更一次性更新到DOM上。这里最重要的是phase1这是时期所做的事。因此我们需要具体了解phase1的机制。

  如果不被打断,那么phase1执行完会直接进入render函数,构建真实的virtualDomTree。如果组件再phase1过程中被打断,即当前组件只渲染到一半(也许是在willMount,也许是willUpdate~反正是在render之前的生命周期),那么react会怎么干呢? react会放弃当前组件所有干到一半的事情,去做更高优先级更重要的任务(当然,也可能是用户鼠标移动,或者其他react监听之外的任务)。当所有高优先级任务执行完之后,react通过callback回到之前渲染到一半的组件,从头开始渲染

  React 16 也会在必要的时候管理各个更新的优先级。这就允许了高优先级更新能够排到队列开头从而被首先处理。关于此的一个例子就是按键输入。鉴于应用流畅性的考虑,用户需要立即获得按键响应,因而相对于那些可以等待 100-200 毫秒的低优先级更新任务,按键输入拥有较高优先级。

  • Fiber节点的数据结构
{
    tag: TypeOfWork, // fiber的类型,下一节会介绍
    alternate: Fiber|null, // 在fiber更新时克隆出的镜像fiber,对fiber的修改会标记在这个fiber上
    return: Fiber|null, // 指向fiber树中的父节点
    child: Fiber|null, // 指向第一个子节点
    sibling: Fiber|null, // 指向兄弟节点
    effectTag: TypeOfSideEffect, // side effect类型,下文会介绍
    nextEffect: Fiber | null, // 单链表结构,方便遍历fiber树上有副作用的节点
    pendingWorkPriority: PriorityLevel, // 标记子树上待更新任务的优先级
}

在实际的渲染过程中,Fiber节点构成了一颗树。这棵树在数据结构上是通过单链表的形式构成的,Fiber节点上的chlidsibling属性分别指向了这个节点的第一个子节点和相邻的兄弟节点。这样就可以遍历整个Fiber树了。

  • TypeOfWork:代表React中不同类型的fiber节点。
{
  IndeterminateComponent: 0, // Before we know whether it is functional or class
  FunctionalComponent: 1,
  ClassComponent: 2,
  HostRoot: 3, // Root of a host tree. Could be nested inside another node.
  HostPortal: 4, // A subtree. Could be an entry point to a different renderer.
  HostComponent: 5,
  HostText: 6,
  CoroutineComponent: 7,
  CoroutineHandlerPhase: 8,
  YieldComponent: 9,
  Fragment: 10,
}

ClassComponent:就是应用层面的React组件。ClassComponent是一个继承自React.Component的类的实例。

HostRoot:ReactDOM.render()时的根节点。

HostComponent:React中最常见的抽象节点,是ClassComponent的组成部分。具体的实现取决于React运行的平台。在浏览器环境下就代表DOM节点,可以理解为所谓的虚拟DOM节点。HostComponent中的Host就代码这种组件的具体操作逻辑是由Host环境注入的。

  • TypeOfSideEffect:这是以二进制位表示的,可以多个叠加。
{
  NoEffect: 0,          
  PerformedWork: 1,   
  Placement: 2, // 插入         
  Update: 4, // 更新           
  PlacementAndUpdate: 6, 
  Deletion: 8, // 删除   
  ContentReset: 16,  
  Callback: 32,      
  Err: 64,         
  Ref: 128,          
}
  • setState:用户触发的setState开启的一次渲染
Component.prototype.setState = function (partialState, callback) {
  !(typeof partialState === 'object' || typeof partialState === 'function' || partialState == null) ? invariant_1(false, 'setState(...): takes an object of state variables to update or a function which returns an object of state variables.') : void 0;
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

setState调用了this.updater.enqueueSetState。updater是renderer在渲染的时候注入的对象,这个对象由reconciler提供。

var classComponentUpdater = {
  isMounted: isMounted,
  enqueueSetState: function (inst, payload, callback) {
    var fiber = get(inst); // 从全局拿到React组件实例对应的fiber
    var currentTime = recalculateCurrentTime(); 
    var expirationTime = computeExpirationForFiber(currentTime, fiber); // 计算fiber的优先级

    var update = createUpdate(expirationTime);
    update.payload = payload;
    if (callback !== undefined && callback !== null) {
      {
        warnOnInvalidCallback$1(callback, 'setState');
      }
      update.callback = callback;
    }

    enqueueUpdate(fiber, update, expirationTime); // 向队列中推入需要更新的fiber
    scheduleWork$1(fiber, expirationTime); // 触发调度器调度一次新的更新
  },
  //...
}
  • performUnitOfWork:React 16保持了之前版本的事务风格,一个“work”会被分解为begin和complete两个阶段来完成。
function performUnitOfWork(workInProgress) {
  // The current, flushed, state of this fiber is the alternate.
  // Ideally nothing should rely on this, but relying on it here
  // means that we don't need an additional field on the work in
  // progress.
  var current = workInProgress.alternate;

  // See if beginning this work spawns more work.
  startWorkTimer(workInProgress);
  {
    ReactDebugCurrentFiber.setCurrentFiber(workInProgress);
  }

  if (true && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
    stashedWorkInProgressProperties = assignFiberPropertiesInDEV(stashedWorkInProgressProperties, workInProgress);
  }

  var next = void 0;
  if (enableProfilerTimer) {
    if (workInProgress.mode & ProfileMode) {
      startBaseRenderTimer();
    }

    next = beginWork(current, workInProgress, nextRenderExpirationTime);

    if (workInProgress.mode & ProfileMode) {
      // Update "base" time if the render wasn't bailed out on.
      recordElapsedBaseRenderTimeIfRunning(workInProgress);
      stopBaseRenderTimerIfRunning();
    }
  } else {
    next = beginWork(current, workInProgress, nextRenderExpirationTime);
  }
  // ...
  if (next === null) {
    // If this doesn't spawn new work, complete the current work.
    next = completeUnitOfWork(workInProgress);
  }

  ReactCurrentOwner.current = null;

  return next;
}
  • beginWork:根据fiber节点不同的tag,调用对应的update方法。可以说是一个入口函数。
function beginWork(current, workInProgress, renderExpirationTime) {
  // ...
  switch (workInProgress.tag) {
    case ClassComponent:
      return updateClassComponent(current, workInProgress, renderExpirationTime); // ClassComponent对应的是React组件实例
    case HostRoot:
      return updateHostRoot(current, workInProgress, renderExpirationTime);
    case HostComponent:
      return updateHostComponent(current, workInProgress, renderExpirationTime); // HostComponent对应的是一个视图层节点,在浏览器环境中就等于DOM节点
    // ...
  }
}
  • updateClassComponent

updateHostComponent:HostComponent没有生命周期钩子需要处理,这个函数主要做的就是调用reconcileChildren对子节点进行diff。

  • reconcileChildren:Virtul DOM diff
// TODO: Remove this and use reconcileChildrenAtExpirationTime directly.
function reconcileChildren(current, workInProgress, nextChildren) {
  reconcileChildrenAtExpirationTime(current, workInProgress, nextChildren, workInProgress.expirationTime);
}

function reconcileChildrenAtExpirationTime(current, workInProgress, nextChildren, renderExpirationTime) {
  if (current === null) { // 首次渲染,创建子节点fiber实例
    // If this is a fresh new component that hasn't been rendered yet, we
    // won't update its child set by applying minimal side-effects. Instead,
    // we will add them all to the child before it gets rendered. That means
    // we can optimize this reconciliation pass by not tracking side-effects.
    workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderExpirationTime);
  } else { // 未处理过子节点;处理过子节点被中断,丢弃之前的处理工作
    // If the current child is the same as the work in progress, it means that
    // we haven't yet started any work on these children. Therefore, we use
    // the clone algorithm to create a copy of all the current children.

    // If we had any progressed work already, that is invalid at this point so
    // let's throw it out.
    workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderExpirationTime);
  }
}
var reconcileChildFibers = ChildReconciler(true);
var mountChildFibers = ChildReconciler(false);

mountChildFibersreconcileChildFibers这两个函数其实是同一个函数,通过传入不同的参数“重载”而来的。

ChildReconciler是一个工厂函数,它接收shouldTrackSideEffects这个参数。reconcileChildFibers函数的目的是产出effect list,mountChildFibers是组件初始化时用的,所以不用clone fiber来diff,也不用产出effect list。ChildReconciler内部有很多helper函数,最终返回的函数叫reconcileChildFibers,这个函数实现了对子fiber节点的reconciliation。

  总的,这个函数根据newChild的类型调用不同的方法。newChild可能是一个元素,也可能是一个数组(React16新特性)。如果是reconcile单个元素,以reconcileSingleElement为例比较key和type,如果相同,复用fiber,删除多余的元素(currentFirstChild的sibling),如果不同,调用createFiberFromElement,返回新创建的。

  如果是string,reconcileSingleTextNode;如果是array,reconcileChildrenArray;如果是空,deleteRemainingChildren删除老的子元素

function reconcileChildFibers(returnFiber, currentFirstChild, newChild, expirationTime) {
  // This function is not recursive.
  // If the top level item is an array, we treat it as a set of children,
  // not as a fragment. Nested arrays on the other hand will be treated as
  // fragment nodes. Recursion happens at the normal flow.

  // Handle top level unkeyed fragments as if they were arrays.
  // This leads to an ambiguity between <>{[...]}</> and <>...</>.
  // We treat the ambiguous cases above the same.
  var isUnkeyedTopLevelFragment = typeof newChild === 'object' && newChild !== null && newChild.type === REACT_FRAGMENT_TYPE && newChild.key === null;
  if (isUnkeyedTopLevelFragment) {
    newChild = newChild.props.children;
  }
  // Handle object types
  var isObject = typeof newChild === 'object' && newChild !== null;

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

  if (typeof newChild === 'string' || typeof newChild === 'number') {
    return placeSingleChild(reconcileSingleTextNode(returnFiber, currentFirstChild, '' + newChild, expirationTime));
  }
  if (isArray$1(newChild)) {
    return reconcileChildrenArray(returnFiber, currentFirstChild, newChild, expirationTime);
  }
  if (getIteratorFn(newChild)) {
    return reconcileChildrenIterator(returnFiber, currentFirstChild, newChild, expirationTime);
  }
  if (isObject) {
    throwOnInvalidObjectType(returnFiber, newChild);
  }

  {
    if (typeof newChild === 'function') {
      warnOnFunctionType();
    }
  }
  // ...
  // Remaining cases are all treated as empty.
  return deleteRemainingChildren(returnFiber, currentFirstChild);
}

React的reconcile算法采用的是层次遍历,这种算法是建立在一个节点的插入、删除、移动等操作都是在节点树的同一层级中进行这个假设下的。所以reconcile算法的核心就是如何diff两个子节点数组

  • reconcileChildrenArray:React16的diff算法采用和来自社区的两端同时比较法同样结构的算法

因为fiber树是单链表结构,没有子节点数组这样的数据结构,也就没有可以供两端同时比较的尾部游标。所以React的这个算法是一个简化的两端比较法,只从头部开始比较。第一次遍历新数组,对上了,新老index都++,比较新老数组哪些元素是一样的,(通过updateSlot,比较key),如果是同样的就update。第一次遍历玩了,如果新数组遍历完了,那就可以把老数组中剩余的fiber删除了。如果老数组完了新数组还没完,那就把新数组剩下的都插入。如果这些情况都不是,就把所有老数组元素按key放map里,然后遍历新数组,插入老数组的元素,这是移动的情况。最后再删除没有被上述情况涉及的元素(也就是老数组中有新数组中无的元素,上面的删除只是fast path,特殊情况)

  • completeUnitOfWork

completeUnitOfWork是complete阶段的入口。complete阶段的作用就是在一个节点diff完成之后,对它进行一些收尾工作,主要是更新props和调用生命周期方法等等。completeUnitOfWork主要的逻辑是调用completeWork完成收尾,然后将当前子树的effect list插入到HostRoot的effect list中。

  • completeWork:complete阶段主要工作都是在completeWork中完成的

completeWork主要是完成reconciliation阶段的扫尾工作,重点是对HostComponent的props进行diff,并标记更新。reconciliation阶段主要负责产出effect list。reconcile的过程相当于是一个纯函数,输入是fiber节点,输出一个effect list。side-effects是在commit阶段被应用到UI中的,这样就将side-effects从reconciliation中隔离开了。因为纯函数的可预测性,让我们可以随时中断reconciliation阶段的执行,而不用担心side-effects给让组件状态和实际UI产生不一致

  • commitRoot

reconciliation阶段结束之后,我们需要将effect list更新到UI中。这就是commit节点的工作。commit这个阶段有点像Git的commit概念。在缓冲区中的代码改动只有在commit之后才会被添加到Git的Object store中。

posted @ 2018-09-03 16:54  Colorful_coco  阅读(4019)  评论(0编辑  收藏  举报