zoukankan      html  css  js  c++  java
  • 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中。

  • 相关阅读:
    Delphi数据类型转换
    js截取指定长度字符
    Internet Explorer无法打开站点,已终止操作
    关于最近做项目的一点点总结
    屏蔽右键菜单
    屏蔽回车自动提交
    自己写的一个客户端验证js
    如何播放声音文件
    如何:使用 CodeDOM 创建类
    js操作dom(4)关于xml节点属性的操作
  • 原文地址:https://www.cnblogs.com/colorful-coco/p/9579402.html
Copyright © 2011-2022 走看看