react16 Fiber

157 阅读8分钟

React15和React16 调度机制

react15核心思想

维护一个虚拟dom树,当数据变化时(setState),会在will生命周期中合并更新队列自动更新虚拟DOM,得到一个新树,然后在updatecomponent阶段diff新老虚拟DOM树,找到有变化的部分,得到一个change(patch),将这个patch加入队列,最终批量更新这些path到DOM中,而且,React 并不是计算出一个差异就去执 行一次 Patch,而是计算出全部差异并放入差异队列后,再一次性地去执行 Patch 方法完成真实 DOM 的更新。简单说就是:diff + patch。

react主要可分为两个阶段

调度阶段 (Reconciler): 用新数据生成一颗新树,遍历虚拟dom,diff新老virtual dom树,搜集具体的UI差异,找到需要更新的元素,放到更新队列中。
渲染阶段(Renderer): 遍历更新队列,通过调用宿主环境的API,实际更新渲染对应元素。宿主环境,比如dom,native等。

缺陷

Fiber之前的Reconciler阶段采用的是Stack Reconciler, 其自顶向下遍历vdom tree, 递归组件执行任务,过程无法中断。
假设有一个层级很复杂的组件,在顶层组件内执行setState, 那么调用栈可能会很长。由于调用栈过长,中间可能还有一些复杂操作,这些任务无法中断,就导致主线程被长时间阻塞。由于浏览器里渲染和js执行共一个主线程,在对响应要求高的场景,比如手势,动画等,就容易造成卡顿,延迟等现象,从而影响用户体验。

React16 Fiber

卡顿原因

人眼不能分辨超过每秒30帧的画面~~

当一秒刷30帧以上时,肉眼就就觉得是连续动画无卡顿,但要求均匀刷新,所以每33ms就要刷新动画。但是,也许33ms执行完动画或者用户交互还剩下点时间,那么就轮到了requestidlework。

window里也有requestIdleWorkwindow.requestIdleCallback()方法将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。你可以在空闲回调函数中调用requestIdleCallback(),以便在下一次通过事件循环之前调度另一个回调。

这就是Fiber的主要目的啦。

总的来讲,通常,客户端线程执行任务时会以帧的形式划分,大部分设备控制在30-60帧是不会影响用户体验;在两个执行帧之间,主线程通常会有一小段空闲时间,requestIdleCallback可以在这个空闲期(Idle Period)调用空闲期回调(Idle Callback),执行一些任务

  1. 低优先级任务由requestIdleCallback处理;
  2. 高优先级任务,如动画相关的由requestAnimationFrame处理;
  3. requestIdleCallback可以在多个空闲期调用空闲期回调,执行任务;
  4. requestIdleCallback方法提供deadline,即任务执行限制时间,以切分任务,避免长时间执行,阻塞UI渲染而导致掉帧;

引自juejin.cn/post/684490…

React16的两个阶段

Render阶段

内容:调度、重新计算新的state和props,重新计算dom树和fiber树。

在这个过程,每个节点都是独立的,每个节点在更新完成后都可以跳出这个更新的循环,然后根据不同的更新模式,可以分片的进行更新。从而使react把更多的优先级给浏览器,及时进行用户反馈并进行连续动画展示,从而解决卡顿问题。

Commit阶段

render阶段结束的时候在root上设置一个对象叫finishwork的对象,即rootfiber,包含一个从firsteffect到lasteffect一个链。effect链式render阶段计算出来的一些更新,且尽可能少的对node进行变更,在commit阶段就会根据这个effect链进行不同的effect更新。中间不可以像render phase那样被打断。

暂时有点朦胧,可以看下面的总概览介绍。

React Fiber总概览

首先上一下Fiber的总框图:更正:addRootToScheduler->scheduleWorkToRoot

(图片来自jokcy ,react.jokcy.me/book/featur…)

Fiber的源码过于庞大,所以本文只是对总流程的一个概述。

建立requestwork部分:

* 1 ReactDOM.render、setState、forceUpdate都会引起createUpdate,从而触发scheduleWork,然后scheduleWorkToRoot:创建root节点并计算expirationtime,返回root。然后通过判断是否正处在render阶段,或者root是不是前后不同来判断是否要继续执行还是return。如果是render阶段,或者root前后相同那就停止调度,直接return。

tips:同步的过期时间最大,过期时间越大.

**优先级**

* 2 然后进行requestwork操作,在本阶段里会先执行addRootToSchedule函数,是将root节点加入到schedule里。接下来会判断expirationtime,是进行同步任务还是异步任务。如果是同步任务会执行performsyncwork,是不需要进行存在异步callback队列里等待浏览器执行完任务后空闲时刷新的。如果是异步work就需要执行一系列函数(这里暂时不管),将work加入到callbacklist队列。

schedule阶段

可以主要参考文章

其实schedule阶段就是要实现前面提到的window.requestIdleCallback,因为很多浏览器兼容性不支持,所以react Fiber使用了polyfill的方式进行模拟requestIdleCallback。
先看两张 amazing 的图: 首先是 React 16 之前版本的


在之前的版本里面,若 React 要开始更新的时候,就会处于深度调用的状态,程序会一直处理更新,而不会接受处理外部的输入。如果更新的层级多而深则会导致更新时间长的问题。到了 React 16 fiber 的阶段呢,如下所示;

看看 react 中设计的 requestIdleCallback ployfill。最主要的就是idleTick和animationTick,其实就是通过postMessage和addeventlistener来进行链接。

  • 第一段代码中,利用浏览器都兼容的requestAnimationFrame来对requestIdleCallback进行替代。而第26行的animationTick其实就是requestAnimationFrame的回调函数。

    这个函数干了啥呢,就是

    • 1)更新 frameDeadline

    • 2)isAnimationFrameScheduled = ture

    • 3)window.postMessage(messageKey, '*')。

  • 看第二段代码里idleTick,里面window.addEventListener('message', idleTick, false)。idleTick里主要干了是啥呢?

    • 1)判断是否还有空余时间进行其他的react异步任务,如果有则执行优先级最高的异步任务

    • 2)callbacklist里是否有过期任务,如果有就直接执行。

    • 3)每执行完一个小任务,就判断是否还有空余时间,如果没有就执行20-26行代码继续调用requestAnimationFrame把主线程还给浏览器。

以上也就是最上面的流程框图里右上角的蓝框:async schedule work和下面要讲的perform阶段的大体流程

const localRequestAnimationFrame = requestAnimationFrame;
// 链表头部与尾部
let  headOfPendingCallbacksLinkedList = null;
let  tailOfPendingCallbacksLinkedList = null;
// frameDeadlineObject 为传入callback的参数 deadline
const frameDeadlineObject = {
  didTimeout: false,
  timeRemaining() {
    // 通过 frameDeadline 来判断,该帧剩余时间
    const remaining = frameDeadline - now();
    return remaining > 0 ? remaining : 0;
  },
};
// export 对外函数,也就是 requestIdleCallback ployfill
scheduleWork = function(callback, options) {
  const timeoutTime = now() + options.timeout;
  const scheduledCallbackConfig: CallbackConfigType = {
    scheduledCallback: callback,
    timeoutTime,
    prev: null,
    next: null,
  };
  // 省略将scheduledCallbackConfig插入到链表里面过程
  if (!isAnimationFrameScheduled) {
    isAnimationFrameScheduled = true;
    localRequestAnimationFrame(animationTick);
  }
}
// requestAnimationFrame 调用函数
const animationTick = function(rafTime) {
  isAnimationFrameScheduled = false;
  // 更新 frameDeadline
  frameDeadline = rafTime + activeFrameTime;
  if (!isIdleScheduled) {
    isIdleScheduled = true;
    window.postMessage(messageKey, '*');
  }
}
// 省略消息监听处理部分

// 执行 callback,与传参 deadline
const callUnsafely = function(callbackConfig, arg) {
  const callback = callbackConfig.scheduledCallback;
  callback(arg);
  // 总是会删除调用过的 callbackConfig
  cancelScheduledWork(callbackConfig);
}

cancelScheduledWork = function(callbackConfig) {
  // 在链表中删除对应节点,并维护好pre以及next关系
}

// messageKey 为特点字符串
const idleTick = function(event) {
  if (event.source !== window || event.data !== messageKey) {
    return;
  }
  isIdleScheduled = false;
  callTimedOutCallbacks();
  let currentTime = now();
  // 空闲时间判断
  while (
    frameDeadline - currentTime > 0 &&
    headOfPendingCallbacksLinkedList !== null
  ) {
    const latestCallbackConfig = headOfPendingCallbacksLinkedList;
    frameDeadlineObject.didTimeout = false;
    callUnsafely(latestCallbackConfig, frameDeadlineObject);
    currentTime = now();
  }
  // 继续下一个节点,调用requestAnimationFrame
  if (
    !isAnimationFrameScheduled &&
    headOfPendingCallbacksLinkedList !== null
  ) {
    isAnimationFrameScheduled = true;
    localRequestAnimationFrame(animationTick);
  }
}
window.addEventListener('message', idleTick, false);
// 如果设置了 timeoutTime 的话,自然是无脑执行到底的,而不会把时间让渡予下一帧
const callTimedOutCallbacks = function() {
  const currentTime = now();
  const timedOutCallbacks = [];
  let currentCallbackConfig = headOfPendingCallbacksLinkedList;
  while (currentCallbackConfig !== null) {
    if (timeoutTime !== -1 && timeoutTime <= currentTime) {
      timedOutCallbacks.push(currentCallbackConfig);
    }
  }
  // 存在 timeoutTime 的事件,并且发生超时了,那就执行,不考虑帧的问题了
  if (timedOutCallbacks.length > 0) {
    frameDeadlineObject.didTimeout = true;
    for (let i = 0, len = timedOutCallbacks.length; i < len; i++) {
      callUnsafely(timedOutCallbacks[i], frameDeadlineObject);
    }
  }
}

preform阶段

简单来说就是:如果是同步任务,没有deadline,立即执行同步任务。如果有deadline,就执行异步任务,没执行完一个异步任务就判断是否还有空余时间,如果没有了就回去再执行schedulecallbackwithexpirationtime把任务放到callbacklist里等待下一个33ms的空余时间来执行。

undefined

commit阶段

上面的框图呢其实就只是render阶段的,只是在执行过程中更新state和props,js逻辑运算、虚拟节点树进行更新,但是并没有真正的将效果反馈在页面上。

render阶段时,会有一个current fiber tree也就是上面一直说的在schedulework一开始就会创建的root对象,同时也会根据current fiber tree和更新state和props构建workinprogress(其实也是个fiber tree),然后在构建过程中会对有变化的节点进行effect tag标签,这个effect对象就挂在Root(即fiber tree)上,如下图。同时,会有一个effect 单链表,这个单链表已经在render阶段做好了计算,能够尽可能的少更新节点(毕竟commit阶段不能中途停下)。在commit阶段呢,就是根据root上的effect 链表进行节点更新并渲染的。

假设这里有个react页面,是点击button 会对item里的state进行平方并渲染。

第一次 render 的时候会生成下图所示的 Fiber Tree:

undefined

因为我需要对 Item 里面的数值做平方运算,于是我点击了 Button,react 根据之前生成的 Fiber Tree 开始构建workInProgress Tree。在构建的过程中,以一个 fiber 节点为单位自顶向下对比,如果发现根节点没有发生改变,根据其 child 指针,把 List 节点复制到 workinprogress Tree 中。 每处理完一个 fiber 节点,react 都会检查当前时间片是否够用,如果发现当前时间片不够用了,就是会标记下一个要处理的任务优先级,根据优先级来决定下一个时间片要处理什么任务。

在平方运算这一过程中,react 通过依次对比 fiber 节点发现 List,Item2,Item3 发生了变化,就会在对应生成的 workInProgress Tree 中打一个 Tag,并且推送到 effect list 中。

当调度阶段也就是render阶段结束后,根节点的 effect list 里记录了包括 DOM change 在内的所有 side effect,在第二阶段(commit)执行更新操作,这样一个流程就算结束了。

但是commit的具体流程本文就不讲了,因为React16最重要的还是render阶段的粒度分化与根据优先级执行异步work。况且我也没仔细看。

有兴趣的同学可以继续看下:Lin Clark去年 react conf 中的演讲。

参考文献:

github.com/easy1090/bl…
juejin.im/post/684490…

juejin.im/post/684490…

juejin.im/post/684490…

juejin.im/post/684490…

www.cnblogs.com/yadiblogs/p…

react.jokcy.me/book/flow/s…