背景

因最近团队外部筹备技术分享,就想着把本人片段化的常识进行一个整顿和串联,随总结如下,供之后温习查阅。

Filber概念

Fiber 是 React 16 中采纳的新和谐(reconciliation)引擎,次要指标是反对虚构 DOM 的渐进式渲染。这是Facebook历时两年的突破性成绩。简要说是React外部实现的一套状态更新机制。反对工作不同优先级,可中断与复原,并且复原后能够复用之前的中间状态。
为了更好的探索fiber这么做的目标,咱们当然还是要从React15登程,看看之前的react都有哪些瓶颈和问题。

React15面临的问题

在正式说React15的问题前,咱们先来看下React15架构。
React15架构能够分为两层:
● Reconciler(协调器)—— 负责找出变动的组件
● Renderer(渲染器)—— 负责将变动的组件渲染到页面上

其中 Reconciler 是基于 Stack reconciler(栈和谐器),应用同步的递归更新的形式。说到递归更新也就是diffing的过程, 但递归的毛病还是很显著,不能暂停,一旦开始必须从头到尾。如果须要渲染的树结构层级嵌套多,而且特地深,那么组件树更新时常常会呈现页面掉帧、卡顿的景象。页面组件频繁更新时页面的动画总是卡顿,或者输入框用键盘输入文字,文字早已输完,然而迟迟不能呈现在输入框内。

卡顿真的是前端展现交互无法忍受的问题,接下来咱们剖析下页面为什么会卡顿,只有晓得问题的实质起因,能力找到最优的解决方案。这个要从浏览器执行机制说起。咱们都晓得浏览器常见的线程有JS引擎线程、GUI渲染线程、HTTP申请线程,定时触发线程,事件处理线程。其中,GUI渲染线程与JS线程是互斥的。当JS引擎执行时GUI线程会被挂起,GUI更新会被保留在一个队列中,等到JS引擎闲暇时,才会被执行。而支流浏览器刷新频率为60Hz,即16.6ms刷新一次,每个16.6ms浏览器都要执行 JS脚本 ---- 款式布局 ---- 款式绘制。所以一旦递归更新工夫超过了16ms工夫超过16.6ms,浏览器就没有工夫执行款式布局绘制了,体现进去的就是页面卡顿,这在页面有动画时尤为显著。

尽管说react 团队已将树操作的复杂度由O(n*3) 改良为 O(n),再去进行优化diff算法貌似有点钻牛角尖了,之所以这么说,因为diff算法都用于组件的新旧children比拟,children个别不会呈现过长的状况,有点大炮打蚊子。况且当咱们的利用变得十分宏大,页面有上万个组件,要diff这么多组件,再卓绝的算法也不能保障浏览器不会累趴。因为他们没想到浏览器也会累趴,也没有想到这是一个短跑的问题。如果是100米长跑,或者1000米比赛,当然越快越好。如果是马拉松,就须要思考到保留膂力了,须要留神劳动了,所以解决问题的要害是给浏览器适当的喘息。

Filber架构解决的问题

以下是React官网形容的Fiber架构的指标:

  • 可能把可中断的工作切片解决。
  • 可能调整优先级,重置并复用工作。
  • 可能在父元素与子元素之间交织解决,以反对 React 中的布局。
  • 可能在 render() 中返回多个元素。
  • 更好地反对谬误边界。

其中要害的前两点都是在解决上述React 15 的问题,再具体说之前,咱们先看下React 16之后的架构

  • Scheduler(调度器)—— 调度工作的优先级,高优工作优先进入Reconciler
  • Reconciler(协调器)—— 负责找出变动的组件
  • Renderer(渲染器)—— 负责将变动的组件渲染到页面上

Scheduler

能够看出在React16 多了Scheduler来调整工作优先级,重要工作优先执行,以浏览器是否有剩余时间作为工作中断的规范,当浏览器有剩余时间时,scheduler会告诉咱们,同时scheduler会进行一系列的工作优先级判断,保障工作工夫正当调配。其中scheduler蕴含两个性能:工夫切片和优先级调度。

工夫切片

因为浏览器的刷新频率为16.6ms,js执行超过16.6ms页面就会卡顿,而工夫切片就是在浏览器每一帧的工夫中,预留一些工夫给JS线程,React利用这部分工夫更新组件,预留的初始工夫是5ms。超过5ms,React将中断js,等下一帧工夫到来继续执行js,下面咱们大抵说了浏览器一帧的执行,接下来咱们具体再剖析下。

咱们能够来看下工夫切片应该放在哪里,宏工作貌似可行,先看看有没有更好的抉择:

微工作

微工作将在页面更新前全副执行完,所以达不到「将主线程还给浏览器」的目标。---no pass

requestAnimationFrame

requestAnimationFrame始终是浏览器js动画的首选。它采纳零碎工夫距离16.6ms,能放弃最佳绘制效率,始终是js动画的首选,它不会因为间隔时间过短,造成适度绘制,减少开销;也不会因为间隔时间太长,使动画卡顿不晦涩,让各种网页动画成果可能有一个对立的刷新机制,从而节俭系统资源,进步零碎性能,改善视觉效果,然而 React 依然没有应用,是因为当页面解决未激活的状态下,requestAnimationFrame 会进行执行;当页面前面再转为激活时,requestAnimationFrame 又会接着上次的中央继续执行,这种受到用户行为的烦扰的Api只能被Scheduler放弃。---no pass

requestIdleCallback

requestIdleCallback其实就是浏览器本人的工夫切片的性能,然而因为它存在以下两个缺点,react也
是只能放弃。---no pass

  • requestIdleCallback在各个浏览器兼容性不好,同requestAnimationFrame一样在当页面未激活的状态下进行执行。
  • requestIdleCallback 每50ms刷新一次,刷新频率远远低于16.6ms,远远低于页面晦涩度的要求。

    宏工作
    1. setTimeout

    既然是宏工作,那么是setTimeout能够吗?答案是能够但不是最佳。让咱们剖析下起因:
    当递归执行 setTimeout(fn, 0) 时,最初间隔时间会变成 4 ms,而不是最后的 1ms,因为setTimeout
    的执行机会是和js执行无关的,递归是会不准,4ms是因为W3C指定的规范,setTimeout第二个参数不能小于4ms,小于4ms默认为4ms。

var count = 0 var startVal = +new Date()console.log("start time", 0, 0)function func() {  setTimeout(() => {    console.log("exec time", ++count, +new Date() - startVal)    if (count === 50) {      return    }    func()  }, 0)} func()

运行后果如下:

2. messageChannel

Scheduler最终应用了 MessageChannel 产生宏工作,然而因为兼容,如果以后宿主环境不反对
MessageChannel,则还是应用setTimeout。
简略介绍下,window.MessageChannel和window.postMessage一样,都能够创立一个通信的通道,这个管道有两个端口,每个端口都能够通过postMessage发送数据,而一个端口只有绑定了onmessage回调办法,就能够接管从另一个端口传过来的数据。以下是MessageChannel的一个简略例子:

那为什么不应用postMessage而应用MessageChannel呢,咱们在应用postMessage的时候会触发所有通过addEventlistener绑定的message事件处理函数,同时postMessage意味着是一个全局的管道,而MessageChannel则是只能在肯定的作用域下才失效。因而react为了缩小串扰,用MessageChannel构建了一个专属管道,缩小了外界的串扰(当外界通信频繁数据量过大,引起缓冲队列溢出而导致管道阻塞便会影响到react的调度性能)以及对外界的烦扰。

任务调度

工作优先级的定义

这个是任务调度的要害,react依据用户对交互的预期程序为交互产生的状态更新赋予了不同的优先级:
其中React 16中应用expirationTime来判断优先级,过期工夫越小,优先级越高。

// 无优先级工作export const NoPriority = 0;// 立刻执行工作,像一些生命周期办法须要同步执行的export const ImmediatePriority = 1;// 用户阻塞工作,比方输入框内输出文字,须要立刻执行export const UserBlockingPriority = 2;// 失常工作export const NormalPriority = 3;// 低优先级工作,比方数据申请,不须要用户感知export const LowPriority = 4; // 闲暇执行工作, 比方暗藏界面意外的内容export const IdlePriority = 5;// 同时每个优先级对应的工作都对应一个过期工夫const IMMEDIATE_PRIORITY_TIMEOUT = -1;const USER_BLOCKING_PRIORITY_TIMEOUT = 250;const NORMAL_PRIORITY_TIMEOUT = 5000;const LOW_PRIORITY_TIMEOUT = 10000;const IDLE_PRIORITY_TIMEOUT = 1073741823;const priorityMap = {  [ImmediatePriority]: IMMEDIATE_PRIORITY_TIMEOUT,  [UserBlockingPriority]: USER_BLOCKING_PRIORITY_TIMEOUT,  [NormalPriority]: NORMAL_PRIORITY_TIMEOUT,  [LowPriority]: LOW_PRIORITY_TIMEOUT,  [IdlePriority]: IDLE_PRIORITY_TIMEOUT}

同时将工作分成了两种:未过期的和已过期的,别离用两个队列存储:
taskQueue:根据工作的过期工夫(expirationTime)排序,过期工夫越小,阐明越紧急,过期工夫小的排在后面。过期工夫依据工作优先级计算得出,优先级越高,过期工夫越小。
timerQueue:根据工作的开始工夫(startTime)排序,开始工夫越小,说明会越早开始,开始工夫小的排在后面。工作进来的时候,开始工夫默认是以后工夫,如果进入调度的时候传了延迟时间,开始工夫则是以后工夫与延迟时间的和。

以后工夫: 这里的以后工夫并不是Date.now(), 而是window.performance.now(),它返回的是一个以毫秒为单位的数值,示意从关上以后页面到该命令执行的时候经验的毫秒数,比Date.now()更加精准

流程

Scheduler会定期将timerQueue中的过期工作放到taskQueue中,而后让调度者告诉执行者循环taskQueue执行掉每一个工作。执行者管制着每个工作的执行,一旦某个工作的执行工夫超出工夫片的限度。就会被中断,而后以后的执行者登场,登场之前会告诉调度者再去调度一个新的执行者持续实现这个工作,新的执行者在下一帧执行工作时依旧会依据工夫片中断工作,而后登场,反复这一过程,直到以后这个工作彻底实现后,将工作从taskQueue踢出。taskQueue中每一个工作都被这样解决,最终实现所有工作,这就是Scheduler的残缺工作流程。

算法缺点

React 16中 expirationTimes 模型比拟的是工作的绝对优先级。

const  isTaskIncludedInBatch  =  priorityOfTask  >=  priorityOfBatch ;

除非执行完更高优先级的工作,否则不容许执行较低优先级的工作。例如:给定优先级 A > B > C,如果不执行 A,就无奈执行 B;如果不执行 B 和 A,也就不能执行 C。
这种限度是在 Suspense 呈现之前设计的,在过后足够满足需要。当所有执行都受 CPU-bound(计算密集型) 限度时,除了按优先级之外,基本不须要按任何程序去解决工作。然而当引入了 IO-bound(即 Suspense)工作时,就可能会遇到较高优先级的 IO-bound 工作阻塞较低优先级的 CPU-bound 工作实现的状况。所以会呈现某些低优先级的工作始终无奈执行。
为了解决这一问题,React 17 采纳了新的 lane 模型, lane车道模型是一种更细粒度的启发式优先级更新算法。

React17更新算法

在新的算法中,指定一个间断的优先级区间,每次更新都会以区间内蕴含的优先级生成对应页面快照。
具体做法是:应用一个31位的二进制代表31种可能性。其中每个bit被称为一个lane(车道),代表优先级,某几个lane组成的二进制数被称为一个lanes,代表一批优先级。
Lanes 模型与 Expiration Times 模型相比有两个次要长处:

  • Lanes 模型将工作优先级的概念(例如:“工作 A 的优先级是否高于工作 B?”)与批量工作(例如:“工作 A 是否属于这组工作?”)划分开来。
  • 通道能够用繁多的 32 位数据类型表白许多不同的工作线程。
说了那么多scheduler的优良之处,可能大家在应用新版本react时仍有这样的疑难:为什么我降级了react16,甚至react17,更新渲染组件依然是同步的,页面性能仍然不好呢?
React16-17须要开启Concurrent模式能力真正体验Scheduler,在react18中将会
作为默认模式。

Fiber Reconciler

为了能更好的配合scheduler将工作可中断,可复用,可按优先级执行,就要将更新工作拆分成一个个小工作,Fiber 的拆分单位是 fiber(fiber tree上的一个节点),实际上是依照虚构DOM的节点拆分的。

Fiber 节点

function FiberNode(  tag: WorkTag,  pendingProps: mixed,  key: null | string,  mode: TypeOfMode,) {  // Instance  // 静态数据存储的属性  // 定义fiber的类型。在reconciliation算法中应用它来确定须要实现的工作。  this.tag = tag;  this.key = key;  this.elementType = null;  // 形容了他对一个的组件,对于复合组件,type是函数或者类组件自身,对于规范组件(例如div,span),type是string  this.type = null;  // 保留对组件,DOM节点或与fiber节点关联的其余React元素类型的类实例的援用。  this.stateNode = null;  // fiber关系相干属性,用于生成Fiber Tree构造  this.return = null;  this.child = null;  this.sibling = null;  this.index = 0;  this.ref = null;  // 动态数据&状态相干属性  // new props,新的变动带来的新的props,即nextProps  this.pendingProps = pendingProps;  // prev props,用于在上一次渲染期间创立输入的fiber的props  // 当传入的pendingProps和memoizedProps雷同的时候,示意fiber能够从新应用之前的fiber,以防止反复的工作  this.memoizedProps = null;  // 状态更新,回调和DOM更新的队列,fiber对应的组件,所产生的update,都会放在该队列中  this.updateQueue = null;  // 以后屏幕UI对应状态,上一次输出更新的fiber state  this.memoizedState = null;  // 一个列表,存储该fiber依赖的contexts,events  this.dependencies = null;  // conCurrentMode和strictMode  // 共存的模式示意这个子树是否默认是 异步渲染的  // fiber刚被创立时,会继承父fiber  this.mode = mode;  // Effects  // 以后fiber阶段须要进行工作,包含:占位、更新、删除等  this.flags = NoFlags;  this.subtreeFlags = NoFlags;  this.deletions = null;  // 优先级调度相干属性  this.lanes = NoLanes;  this.childLanes = NoLanes;  // 双缓存  // current tree和working in prgoress tree关联属性  // 在fiber树更新的过程中,每个fiber都有与其对应的fiber  // 咱们称之为 current <==> workInProgress  // 在渲染实现后,会指向对方  this.alternate = null;}

让咱们看下上面的例子:

下面的JSX会生成上面的Fiber树,这里的父指针叫做return而不是parent或者father,是因为作为一个工作单元,return指节点执行实现后会返回的下一个节点。子Fiber节点及其兄弟节点实现工作后会返回其父级节点,所以用return指代父级节点。

双缓冲技术

下面咱们看到fiber节点中有个属性是alternate,这里就波及到了双缓冲技术。

  • 简要概述下什么是双缓冲技术?
    咱们看电视时,看到的屏幕称为OSD层,咱们看到画面须要不停的重绘,这就很容易导致画面闪动,双缓冲应用内存缓冲区来解决这一问题,绘制操作首先出现到内存缓冲区,绘制实现后某一时机会绘制在OSD层。
  • Reconciler对于双缓冲技术的利用
    在React中最多会同时存在两棵Fiber树。以后屏幕上显示内容对应的Fiber树称为current Fiber树,正在内存中构建的Fiber树称为workInProgress Fiber树。
    current Fiber树中的Fiber节点被称为current fiber,workInProgress Fiber树中的Fiber节点被称为workInProgress fiber,他们通过alternate属性连贯。React利用的根节点通过使current指针在不同Fiber树的rootFiber间切换来实现current Fiber树指向的切换。
    即当workInProgress Fiber树构建实现交给Renderer渲染在页面上后,利用根节点的current指针指向workInProgress Fiber树,此时workInProgress Fiber树就变为current Fiber树。
    每次状态更新都会产生新的workInProgress Fiber树,通过current与workInProgress的替换,实现DOM更新

reconciliation过程

reconciler过程分为2个阶段:

1.(可中断)render/reconciliation:通过结构workInProgress tree得出change
2.(不可中断)commit:利用这些DOM change

流程
  • fiber 节点遍历流程
  • 从current(Root)开始通过child向下找
  • 如果有child先深度遍历子节点,直到null为止
  • 而后看是否有兄弟节点
  • 有兄弟节点则遍历兄弟节点
  • 而后再看兄弟节点是否有子节点
  • 如无其余兄弟节点,而后return看父节点是否有兄弟节点
  • 如果无,则return回root
  • reconciliation流程

小结

本文仅针对Scheduler和Reconciler原理做了浅析,这就是整个React Fiber架构中最外围的重构局部。name最初咱们通过一个例子再整体看下这两局部的执行过程:

下面的例子在React Fiber架构中的整个更新流程为:

其中红框中的步骤随时可能因为以下起因被中断:

  • 有其余更高优工作须要先更新
  • 以后帧没有剩余时间
    因为红框中的工作都在内存中进行,不会更新页面上的DOM,所以即便重复中断,用户也不会看见更新不齐全的DOM。