关于react.js:React-Fibe架构

7次阅读

共计 8203 个字符,预计需要花费 21 分钟才能阅读完成。

背景

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

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。
正文完
 0