走进React Fiber 架构

本文重点:介绍React重构的原由和目的,理解Fiber tree单向链表结构中各属性含义,梳理调度过程和核心实现手段,深刻新的生命周期,hooks,suspense,异常捕获等特性的用法和原理。css

喜欢的就点个赞吧️,但愿跟你们在枯燥的源码中发掘学习的乐趣,一块儿分享进步。html

当react刚推出的时候,最具革命性的特性就是虚拟dom,由于这大大下降了应用开发的难度,相比较以往告诉浏览器我须要怎么更新个人ui,如今咱们只须要告诉react我应用ui的下个状态是怎么样的,react会帮咱们自动处理二者之间的全部事宜。前端

这让咱们能够从属性操做、事件处理和手动 DOM 更新这些在构建应用程序时必要的操做中解放出来。宿主树的概念让这个优秀的框架有无限的可能性,react native即是其在原生移动应用中伟大的实现。react

但在享受温馨开发体验的同时,有一些疑问一直萦绕在咱们脑海中:git

  • 是什么致使了react用户交互、动画频繁卡顿
  • 如何视线优雅的异常处理,进行异常捕获和备用ui渲染
  • 如何更好实现组件的复用和状态管理

这到底是人性的扭曲,仍是道德的沦丧 /狗头github

Fiber可否给咱们答案,又将带给咱们什么惊喜,卷起一波新的浪潮,欢迎收看《走进Fiber》ajax

那么,简而言之,React Fiber是什么?算法

Fiber是对React核心算法的重构,2年重构的产物就是Fiber reconciler。segmentfault

react协调是什么

协调是react中重要的一部分,其中包含了如何对新旧树差别进行比较以达到仅更新差别的部分。promise

如今的react通过重构后Reconciliation和Rendering被分为两个不一样的阶段。

  • reconciler协调阶段:当组件次初始化和其后的状态更新中,React会建立两颗不相同的虚拟树,React 须要基于这两棵树之间的差异来判断如何有效率的更新 UI 以保证当前 UI 与最新的树保持同步,计算树哪些部分须要更新。
  • renderer阶段:渲染器负责将拿到的虚拟组件树信息,根据其对应环境真实地更新渲染到应用中。有兴趣的朋友能够看一下dan本身的博客中的文章=》运行时的react=》渲染器,介绍了react的Renderer渲染器如react-dom和react native等,其能够根据不一样的主环境来生成不一样的实例。

为何要重写协调

动画是指由许多帧静止的画面,以必定的速度(如每秒16张)连续播放时,肉眼因视觉残象产生错觉,而误觉得画面活动的做品。——维基百科

老一辈人经常把电影称为“移动的画”,咱们小时候看的手翻书就是快速翻动的一页页画,其本质上实现原理跟动画是同样的。

帧:在动画过程当中,每一幅静止画面即为一“帧”;
帧率:是用于测量显示帧数的量度,测量单位为“每秒显示帧数”(Frame per Second,FPS)或“赫兹”;
帧时长:即每一幅静止画面的停留时间,单位通常是ms(毫秒);
丢帧:在帧率固定的动画中,某一帧的时长远高于平均帧时长,致使其后续数帧被挤压而丢失的现象;

当前大部分笔记本电脑和手机的常见帧率为60hz,即一秒显示60帧的画面,一帧停留的时间为16.7ms(1000/60≈16.7),这就留给了开发者和UI系统大约16.67ms来完成生成一张静态图片(帧)所须要的全部工做。若是在这分派的16.67ms以内没有可以完成这些工做,就会引起‘丢帧’的后果,使界面表现的不够流畅。

浏览器中的GUI渲染线程和JS引擎线程

在浏览器中GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(至关于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时当即被执行。

浏览器拥挤的主线程

React16 推出Fiber以前协调算法是Stack Reconciler,即递归遍历全部的 Virtual DOM 节点执行Diff算法,一旦开始便没法中断,直到整颗虚拟dom树构建完成后才会释放主线程,因其JavaScript单线程的特色,若当下组件具备复杂的嵌套和逻辑处理,diff便会堵塞UI进程,使动画和交互等优先级相对较高的任务没法当即获得处理,形成页面卡顿掉帧,影响用户体验。

16年在 facebook 上 Seb 正式提到了 Fiber 这个概念,解释为何要重写框架:

Once you have each stack frame as an object on the heap you can do clever things like reusing it during future updates and yielding to the event loop without losing any of your currently in progress data.
一旦将每一个堆栈帧做为堆上的对象,您就能够作一些聪明的事情,例如在未来的更新中重用它并暂停于事件循环,而不会丢失任何当前正在进行的数据。

咱们来作一个实验

function randomHexColor() {
  return (
    "#" + ("0000" + ((Math.random() * 0x1000000) << 0).toString(16)).substr(-6)
  );
}

var root = document.getElementById("root");

// 一次性遍历100000次
function a() {
  setTimeout(function() {
    var k = 0;
    for (var i = 0; i < 10000; i++) {
      k += new Date() - 0;
      var el = document.createElement("div");
      el.innerHTML = k;
      root.appendChild(el);
      el.style.cssText = `background:${randomHexColor()};height:40px`;
    }
  }, 1000);
}

// 每次只操做100个节点,共100次
function b() {
  setTimeout(function() {
    function loop(n) {
      var k = 0;
      console.log(n);
      for (var i = 0; i < 100; i++) {
        k += new Date() - 0;
        var el = document.createElement("div");
        el.innerHTML = k;
        root.appendChild(el);
        el.style.cssText = `background:${randomHexColor()};height:40px`;
      }
      if (n) {
        setTimeout(function() {
          loop(n - 1);
        }, 40);
      }
    }
    loop(100);
  }, 1000);
}
复制代码

a执行性能截图:掉帧严重,广泛fps为1139.6ms

b执行性能截图: fps处于15ms~19ms

究其缘由是由于浏览器的主线程须要处理GUI描绘,时间器处理,事件处理,JS执行,远程资源加载等,当作某件事,只有将它作完才能作下一件事。若是有足够的时间,浏览器是会对咱们的代码进行编译优化(JIT)及进行热代码优化,一些DOM操做,内部也会对reflow进行处理。reflow是一个性能黑洞,极可能让页面的大多数元素进行从新布局。

而做为一只有梦想的前端菜🐤,为用户爸爸呈现最好的交互体验是咱们责无旁贷的责任,把困难扛在肩上,让咱们see see react是如何解决以上的问题。

Fiber你是个啥(第四音

那么咱们先看看做为看看解决方案的Fiber是什么,而后在分析为何它能解决以上问题。

定义:

  1. react Reconciliation协调核心算法的一次从新实现
  2. 虚拟堆栈帧
  3. 具有扁平化的链表数据存储结构的js对象,Reconciliation阶段所能拆分的最小工做单元

针对其定义咱们来进行拓展:

虚拟堆栈帧:

Andrew Clark的React Fiber体系文档很好地解释了Fiber实现背后的想法,我在这里引用一下:

Fiber是堆栈的从新实现,专门用于React组件。 您能够将单个Fiber视为虚拟堆栈框架。 从新实现堆栈的优势是,您能够将堆栈帧保留在内存中,并根据须要(以及在任什么时候候)执行它们。 这对于实现调度的目标相当重要。

JavaScript的执行模型:call stack

JavaScript原生的执行模型:经过调用栈来管理函数执行状态。
其中每一个栈帧表示一个工做单元(a unit of work),存储了函数调用的返回指针、当前函数、调用参数、局部变量等信息。 由于JavaScript的执行栈是由引擎管理的,执行栈一旦开始,就会一直执行,直到执行栈清空。没法按需停止。

react以往的渲染就是使用原生执行栈来管理组件树的递归渲染,当其层次较深component不断递归子节点,没法被打断就会致使主线程堵塞ui卡顿。

可控的调用栈

因此理想情况下reconciliation的过程应该是像下图所示同样,将繁重的任务划分红一个个小的工做单元,作完后可以“喘口气儿”。咱们须要一种增量渲染的调度,Fiber就是从新实现一个堆栈帧的调度,这个堆栈帧能够按照本身的调度算法执行他们。另外因为这些堆栈是可将可中断的任务拆分红多个子任务,经过按照优先级来自由调度子任务,分段更新,从而将以前的同步渲染改成异步渲染。

它的特性就是时间分片(time slicing)和暂停(supense)。

具有扁平化的链表数据存储结构的js对象:

fiber是一个js对象,fiber的建立是经过React元素来建立的,在整个React构建的虚拟DOM树中,每个元素都对应有一个fiber,从而构建了一棵fiber树,每一个fiber不只仅包含每一个元素的信息,还包含更多的信息,以方便Scheduler来进行调度。

让咱们看一下fiber的结构

type Fiber = {|
  // 标记不一样的组件类型
  //export const FunctionComponent = 0;
  //export const ClassComponent = 1;
  //export const HostRoot = 3; 能够理解为这个fiber是fiber树的根节点,根节点能够嵌套在子树中
  //export const Fragment = 7;
  //export const SuspenseComponent = 13;
  //export const MemoComponent = 14;
  //export const LazyComponent = 16;
  tag: WorkTag,

  // ReactElement里面的key
  // 惟一标示。咱们在写React的时候若是出现列表式的时候,须要制定key,这key就是对应元素的key。
  key: null | string,

  // ReactElement.type,也就是咱们调用`createElement`的第一个参数
  elementType: any,

  // The resolved function/class/ associated with this fiber.
  // 异步组件resolved以后返回的内容,通常是`function`或者`class`
  type: any,

  // The local state associated with this fiber.
  // 跟当前Fiber相关本地状态(好比浏览器环境就是DOM节点)
  // 当前组件实例的引用
  stateNode: any,

  // 指向他在Fiber节点树中的`parent`,用来在处理完这个节点以后向上返回
  return: Fiber | null,

  // 单链表树结构
  // 指向本身的第一个子节点
  child: Fiber | null,
  // 指向本身的兄弟结构
  // 兄弟节点的return指向同一个父节点
  sibling: Fiber | null,
  index: number,

  // ref属性
  ref: null | (((handle: mixed) => void) & {_stringRef: ?string}) | RefObject,

  // 新的变更带来的新的props
  pendingProps: any, 
  // 上一次渲染完成以后的props
  memoizedProps: any,

  // 该Fiber对应的组件产生的Update会存放在这个队列里面
  updateQueue: UpdateQueue<any> | null,

  // 上一次渲染的时候的state
  // 用来存放某个组件内全部的 Hook 状态
  memoizedState: any,

  // 一个列表,存放这个Fiber依赖的context
  firstContextDependency: ContextDependency<mixed> | null,

  // 用来描述当前Fiber和他子树的`Bitfield`
  // 共存的模式表示这个子树是否默认是异步渲染的
  // Fiber被建立的时候他会继承父Fiber
  // 其余的标识也能够在建立的时候被设置
  // 可是在建立以后不该该再被修改,特别是他的子Fiber建立以前
  //用来描述fiber是处于何种模式。用二进制位来表示(bitfield),后面经过与来看二者是否相同//这个字段实际上是一个数字.实现定义了一下四种//NoContext: 0b000->0//AsyncMode: 0b001->1//StrictMode: 0b010->2//ProfileMode: 0b100->4
  mode: TypeOfMode,

  // Effect
  // 用来记录Side Effect具体的执行的工做的类型:好比Placement,Update等等
  effectTag: SideEffectTag,

  // 单链表用来快速查找下一个side effect
  nextEffect: Fiber | null,

  // 子树中第一个side effect
  firstEffect: Fiber | null,
  // 子树中最后一个side effect
  lastEffect: Fiber | null,

  // 表明任务在将来的哪一个时间点应该被完成
  // 不包括他的子树产生的任务
  // 经过这个参数也能够知道是否还有等待暂停的变动、没有完成变动。
  // 这个参数通常是UpdateQueue中最长过时时间的Update相同,若是有Update的话。
  expirationTime: ExpirationTime,

  // 快速肯定子树中是否有不在等待的变化
  childExpirationTime: ExpirationTime,

  //当前fiber对应的工做中的Fiber。
  // 在Fiber树更新的过程当中,每一个Fiber都会有一个跟其对应的Fiber
  // 咱们称他为 current <==> workInProgress
  // 在渲染完成以后他们会交换位置
  alternate: Fiber | null,
  ...
|};
复制代码

ReactWorkTags组件类型

链表结构

fiber中最为重要的是return、child、sibling指针,链接父子兄弟节点以构成一颗单链表fiber树,其扁平化的单链表结构的特色将以往递归遍历改成了循环遍历,实现深度优先遍历。

React16特别青睐于链表结构,链表在内存里不是连续的,动态分配,增删方便,轻量化,对异步友好

current与workInProgress

current树:React 在 render 第一次渲染时,会经过 React.createElement 建立一颗 Element 树,能够称之为 Virtual DOM Tree,因为要记录上下文信息,加入了 Fiber,每个 Element 会对应一个 Fiber Node,将 Fiber Node 连接起来的结构成为 Fiber Tree。它反映了用于渲染 UI 和映射应用状态。这棵树一般被称为 current 树(当前树,记录当前页面的状态)。

workInProgress树:当React通过current当前树时,对于每个先存在的fiber节点,它都会建立一个替代(alternate)节点,这些节点组成了workInProgress树。这个节点是使用render方法返回的React元素的数据建立的。一旦更新处理完以及全部相关工做完成,React就有一颗替代树来准备刷新屏幕。一旦这颗workInProgress树渲染(render)在屏幕上,它便成了当前树。下次进来会把current状态复制到WIP上,进行交互复用,而不用每次更新的时候都建立一个新的对象,消耗性能。这种同时缓存两棵树进行引用替换的技术被称为双缓冲技术

function createWorkInProgress(current, ...) {
  let workInProgress = current.alternate;
  if (workInProgress === null) {
    workInProgress = createFiber(...);
  }
  ...
  workInProgress.alternate = current;
  current.alternate = workInProgress;
  ...
  return workInProgress;
}
复制代码

alternate fiber能够理解为一个fiber版本池,用于交替记录组件更新(切分任务后变成多阶段更新)过程当中fiber的更新,由于在组件更新的各阶段,更新前及更新过程当中fiber状态并不一致,在须要恢复时(如发生冲突),便可使用另外一者直接回退至上一版本fiber。

Dan在Beyond React 16演讲中用了一个很是恰当的比喻,那就是Git 功能分支,你能够将 WIP 树想象成从旧树中 Fork 出来的功能分支,你在这新分支中添加或移除特性,即便是操做失误也不会影响旧的分支。当你这个分支通过了测试和完善,就能够合并到旧分支,将其替换掉。

Update

  • 用于记录组件状态的改变
  • 存放于fiber的updateQueue里面
  • 多个update同时存在

好比设置三个setState(),React是不会当即更新的,而是放到UpdateQueue中,再去更新

ps: setState一直有人疑问为啥不是同步,将 setState() 视为请求而不是当即更新组件的命令。为了更好的感知性能,React 会延迟调用它,而后经过一次传递更新多个组件。React 并不会保证 state 的变动会当即生效。

export function createUpdate(
  expirationTime: ExpirationTime,
  suspenseConfig: null | SuspenseConfig,
): Update<*> {
  let update: Update<*> = {
    //任务过时事件
    //在建立每一个更新的时候,须要设定过时时间,过时时间也就是优先级。过时时间越长,就表示优先级越低。
    expirationTime,
    // suspense的配置
    suspenseConfig,

  // export const UpdateState = 0; 表示更新State
  // export const ReplaceState = 1; 表示替换State
  // export const ForceUpdate = 2; 强制更新
  // export const CaptureUpdate = 3; 捕获更新(发生异常错误的时候发生)
  // 指定更新的类型,值为以上几种
    tag: UpdateState,
    // 更新内容,好比`setState`接收的第一个参数
    payload: null,
    // 更新完成后的回调,`setState`,`render`都有
    callback: null,

    // 指向下一个update
    // 单链表update queue经过 next串联
    next: null,
    
    // 下一个side effect
    // 最新源码被抛弃 next替换
    //nextEffect: null,
  };
  if (__DEV__) {
    update.priority = getCurrentPriorityLevel();
  }
  return update;
}
复制代码

UpdateQueue

//建立更新队列
export function createUpdateQueue<State>(baseState: State): UpdateQueue<State> {
  const queue: UpdateQueue<State> = {
    //应用更新后的state
    baseState,
    //队列中的第一个update
    firstUpdate: null,
    //队列中的最后一个update
    lastUpdate: null,
     //队列中第一个捕获类型的update
    firstCapturedUpdate: null,
    //队列中最后一个捕获类型的update
    lastCapturedUpdate: null,
    //第一个side effect
    firstEffect: null,
    //最后一个side effect
    lastEffect: null,
    firstCapturedEffect: null,
    lastCapturedEffect: null,
  };
  return queue;
}
复制代码

update中的payload:一般咱们如今在调用setState传入的是一个对象,但在使用fiber conciler时,必须传入一个函数,函数的返回值是要更新的state。react从很早的版本就开始支持这种写法了,不过一般没有人用。在以后的react版本中,可能会废弃直接传入对象的写法。

setState({}, callback); // stack conciler
setState(() => { return {} }, callback); // fiber conciler
复制代码

ReactUpdateQueue源码

Updater

每一个组件都会有一个Updater对象,它的用处就是把组件元素更新和对应的fiber关联起来。监听组件元素的更新,并把对应的更新放入该元素对应的fiber的UpdateQueue里面,而且调用ScheduleWork方法,把最新的fiber让scheduler去调度工做。

const classComponentUpdater = {
  isMounted,
  enqueueSetState(inst, payload, callback) {
    const fiber = getInstance(inst);
    const currentTime = requestCurrentTimeForUpdate();
    const suspenseConfig = requestCurrentSuspenseConfig();
    const expirationTime = computeExpirationForFiber(
      currentTime,
      fiber,
      suspenseConfig,
    );

    const update = createUpdate(expirationTime, suspenseConfig);
    update.payload = payload;
    if (callback !== undefined && callback !== null) {
      if (__DEV__) {
        warnOnInvalidCallback(callback, 'setState');
      }
      update.callback = callback;
    }

    enqueueUpdate(fiber, update);
    scheduleWork(fiber, expirationTime);
  },
  enqueueReplaceState(inst, payload, callback) {
    //同样的代码
    //...
    update.tag = ReplaceState;
    //...
  },
  enqueueForceUpdate(inst, callback) {
    //同样的代码
    //...
    update.tag = ForceUpdate;
    //...
  },
};
复制代码

ReactUpdateQueue=>classComponentUpdater

Effect list

Side Effects:咱们能够将React中的一个组件视为一个使用state和props来计算UI的函数。每一个其余活动,如改变DOM或调用生命周期方法,都应该被认为是side-effects,react文档中是这样描述的side-effects的:

You’ve likely performed data fetching, subscriptions, or manually changing the DOM 的from React components before. We call these operations “side effects” (or “effects” for short) because they can affect other components and can’t be done during rendering.

React可以很是快速地更新,而且为了实现高性能,它采用了一些有趣的技术。其中之一是构建带有side-effects的fiber节点的线性列表,其具备快速迭代的效果。迭代线性列表比树快得多,而且没有必要在没有side effects的节点上花费时间。

每一个fiber节点均可以具备与之相关的effects, 经过fiber节点中的effectTag字段表示。

此列表的目标是标记具备DOM更新或与其关联的其余effects的节点,此列表是WIP tree的子集,并使用nextEffect属性,而不是current和workInProgress树中使用的child属性进行连接。

How it work

核心目标

  • 把可中断的工做拆分红多个小任务
  • 为不一样类型的更新分配任务优先级
  • 更新时可以暂停,终止,复用渲染任务

更新过程概述

咱们先看看其Fiber的更新过程,而后再针对过程当中的核心技术进行展开。

Reconciliation分为两个阶段:reconciliation 和 commit

reconciliation

从图中能够看到,能够把reconciler阶段分为三部分,分别以红线划分。简单的归纳下三部分的工做:

  1. 第一部分从 ReactDOM.render() 方法开始,把接收的React Element转换为Fiber节点,并为其设置优先级,记录update等。这部分主要是一些数据方面的准备工做。
  2. 第二部分主要是三个函数:scheduleWork、requestWork、performWork,即安排工做、申请工做、正式工做三部曲。React 16 新增的异步调用的功能则在这部分实现。
  3. 第三部分是一个大循环,遍历全部的Fiber节点,经过Diff算法计算全部更新工做,产出 EffectList 给到commit阶段使用。这部分的核心是 beginWork 函数。

commit阶段

这个阶段主要作的工做拿到reconciliation阶段产出的全部更新工做,提交这些工做并调用渲染模块(react-dom)渲染UI。完成UI渲染以后,会调用剩余的生命周期函数,因此异常处理也会在这部分进行

分配优先级

其上所列出的fiber结构中有个expirationTime。

expirationTime本质上是fiber work执行的优先级。

// 源码中的priorityLevel优先级划分
export const NoWork = 0;
// 仅仅比Never高一点 为了保证连续必须完整完成
export const Never = 1;
export const Idle = 2;
export const Sync = MAX_SIGNED_31_BIT_INT;//整型最大数值,是V8中针对32位系统所设置的最大值
export const Batched = Sync - 1;
复制代码

源码中的computeExpirationForFiber函数,该方法用于计算fiber更新任务的最晚执行时间,进行比较后,决定是否继续作下一个任务。

//为fiber对象计算expirationTime
function computeExpirationForFiber(currentTime: ExpirationTime, fiber: Fiber) {
  ...
  // 根据调度优先级计算ExpirationTime
    const priorityLevel = getCurrentPriorityLevel();
    switch (priorityLevel) {
      case ImmediatePriority:
        expirationTime = Sync;
        break;
        //高优先级 如由用户输入设计交互的任务
      case UserBlockingPriority:
        expirationTime = computeInteractiveExpiration(currentTime);
        break;
        // 正常的异步任务
      case NormalPriority:
        // This is a normal, concurrent update
        expirationTime = computeAsyncExpiration(currentTime);
        break;
      case LowPriority:
      case IdlePriority:
        expirationTime = Never;
        break;
      default:
        invariant(
          false,
          'Unknown priority level. This error is likely caused by a bug in ' +
            'React. Please file an issue.',
        );
    }
    ...
}

export const LOW_PRIORITY_EXPIRATION = 5000
export const LOW_PRIORITY_BATCH_SIZE = 250

export function computeAsyncExpiration(
  currentTime: ExpirationTime,
): ExpirationTime {
  return computeExpirationBucket(
    currentTime,
    LOW_PRIORITY_EXPIRATION,
    LOW_PRIORITY_BATCH_SIZE,
  )
}

export const HIGH_PRIORITY_EXPIRATION = __DEV__ ? 500 : 150
export const HIGH_PRIORITY_BATCH_SIZE = 100

export function computeInteractiveExpiration(currentTime: ExpirationTime) {
  return computeExpirationBucket(
    currentTime,
    HIGH_PRIORITY_EXPIRATION,
    HIGH_PRIORITY_BATCH_SIZE,
  )
}

function computeExpirationBucket(
  currentTime,
  expirationInMs,
  bucketSizeMs,
): ExpirationTime {
  return (
    MAGIC_NUMBER_OFFSET -
    ceiling(
    // 以前的算法
     //currentTime - MAGIC_NUMBER_OFFSET + expirationInMs / UNIT_SIZE,
      MAGIC_NUMBER_OFFSET - currentTime + expirationInMs / UNIT_SIZE,
      bucketSizeMs / UNIT_SIZE,
    )
  );
}
复制代码
// 咱们把公式整理一下:
// low
 1073741821-ceiling(1073741821-currentTime+500,25) =>
 1073741796-((1073742321-currentTime)/25 | 0)*25
// high 
1073741821-ceiling(1073741821-currentTime+15,10)
复制代码

简单来讲,最终结果是以25为单位向上增长的,好比说咱们输入102 - 126之间,最终获得的结果都是625,可是到了127获得的结果就是650了,这就是除以25取整的效果。

即计算出的React低优先级update的expirationTime间隔是25ms, React让两个相近(25ms内)的update获得相同的expirationTime,目的就是让这两个update自动合并成一个Update,从而达到批量更新的目的。就像提到的doubleBuffer同样,React为提升性能,考虑得很是全面!

expiration算法源码

推荐阅读:jokcy大神解析=》expirationTime计算

执行优先级

那么Fiber是如何作到异步实现不一样优先级任务的协调执行的

这里要介绍介绍浏览器提供的两个API:requestIdleCallback和requestAnimationFrame:

requestIdleCallback: 在浏览器空闲时段内调用的函数排队。是开发人员能够在主事件循环上执行后台和低优先级工做而不会影响延迟关键事件,如动画和输入响应。

其在回调参数中IdleDeadline能够获取到当前帧剩余的时间。利用这个信息能够合理的安排当前帧须要作的事情,若是时间足够,那继续作下一个任务,若是时间不够就歇一歇。

requestAnimationFrame:告诉浏览器你但愿执行一个动画,而且要求浏览器在下次重绘以前调用指定的回调函数更新动画

合做式调度:这是一种’契约‘调度,要求咱们的程序和浏览器紧密结合,互相信任。好比能够由浏览器给咱们分配执行时间片,咱们要按照约定在这个时间内执行完毕,并将控制权还给浏览器。

Fiber所作的就是须要分解渲染任务,而后根据优先级使用API调度,异步执行指定任务:

  • 低优先级任务由requestIdleCallback处理,限制任务执行时间,以切分任务,同时避免任务长时间执行,阻塞UI渲染而致使掉帧。
  • 高优先级任务,如动画相关的由requestAnimationFrame处理;

并非全部的浏览器都支持requestIdleCallback,可是React内部实现了本身的polyfill,因此没必要担忧浏览器兼容性问题。polyfill实现主要是经过rAF+postmessage实现的(最新版本去掉了rAF,有兴趣的童鞋能够看看=》SchedulerHostConfig

生命周期

由于其在协调阶段任务可被打断的特色,任务在切片后运行完一段便将控制权交还到react负责任务调度的模块,再根据任务的优先级,继续运行后面的任务。因此会致使某些组件渲染到一半便会打断以运行其余紧急,优先级更高的任务,运行完却不会继续以前中断的部分,而是从新开始,因此在协调的全部生命周期都会面临这种被屡次调用的状况。
为了限制这种被屡次重复调用,耗费性能的状况出现,react官方一步步把处在协调阶段的部分生命周期进行移除。

废弃:

  • componentWillMount
  • componentWillUpdate
  • componentWillReceiveProps

新增:

newLifeCircle

为何新的生命周期用static

static 是ES6的写法,当咱们定义一个函数为static时,就意味着没法经过this调用咱们在类中定义的方法

经过static的写法和函数参数,能够感受React在和我说:请只根据newProps来设定derived state,不要经过this这些东西来调用帮助方法,可能会越帮越乱。用专业术语说:getDerivedStateFromProps应该是个纯函数,没有反作用(side effect)。

getDerivedStateFromError和componentDidCatch之间的区别是什么?

简而言之,由于所处阶段的不一样而功能不一样。

getDerivedStateFromError是在reconciliation阶段触发,因此getDerivedStateFromError进行捕获错误后进行组件的状态变动,不容许出现反作用。

static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染能够显降级 UI
    return { hasError: true };
}
复制代码

componentDidCatch由于在commit阶段,所以容许执行反作用。 它应该用于记录错误之类的状况:

componentDidCatch(error, info) {
    // "组件堆栈" 例子:
    //   in ComponentThatThrows (created by App)
    //   in ErrorBoundary (created by App)
    //   in div (created by App)
    //   in App
    logComponentStackToMyService(info.componentStack);
  }

复制代码

生命周期相关资料点这里=》生命周期

Suspense

Suspense的实现很诡异,也备受争议。
用Dan的原话讲:你将会恨死它,而后你会爱上他。

Suspense功能想解决从react出生到如今都存在的「异步反作用」的问题,并且解决得很是的优雅,使用的是「异步可是同步的写法」.

Suspense暂时只是用于搭配lazy进行代码分割,在组件等待某事时“暂停”渲染的能力,并显示加载的loading,但他的做用远远不止如此,当下在concurrent mode实验阶段文档下提供了一种suspense处理异步请求获取数据的方法。

用法

// 懒加载组件切换时显示过渡组件
const ProfilePage = React.lazy(() => import('./ProfilePage')); // Lazy-loaded

// Show a spinner while the profile is loading
<Suspense fallback={<Spinner />}>
  <ProfilePage />
</Suspense>
复制代码
// 异步获取数据
import { unstable_createResource } from 'react-cache'

const resource = unstable_createResource((id) => {
  return fetch(`/demo/${id}`)
})

function ProfilePage() {
  return (
    <Suspense fallback={<h1>Loading profile...</h1>}>
      <ProfileDetails />
      <Suspense fallback={<h1>Loading posts...</h1>}>
        <ProfileTimeline />
      </Suspense>
    </Suspense>
  );
}

function ProfileDetails() {
  // Try to read user info, although it might not have loaded yet
  const user = resource.user.read();
  return <h1>{user.name}</h1>;
}

function ProfileTimeline() {
  // Try to read posts, although they might not have loaded yet
  const posts = resource.posts.read();
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.text}</li>
      ))}
    </ul>
  );
}
复制代码
  • 在render函数中,咱们能够写入一个异步请求,请求数据
  • react会从咱们缓存中读取这个缓存
  • 若是有缓存了,直接进行正常的render
  • 若是没有缓存,那么会抛出一个异常,这个异常是一个promise
  • 当这个promise完成后(请求数据完成),react会继续回到原来的render中(其实是从新执行一遍render),把数据render出来
  • 彻底同步写法,没有任何异步callback之类的东西

若是你尚未明白这是什么意思那我简单的表述成下面这句话:

调用render函数->发现有异步请求->悬停,等待异步请求结果->再渲染展现数据

看着是很是神奇的,用同步方法写异步,并且没有yield/async/await,简直能把人看傻眼了。这么作的好处天然就是,咱们的思惟逻辑很是的简单,清楚,没有callback,没有其余任何玩意,不能不说,看似优雅了很是多并且牛逼。

官方文档指出它还将提供官方的方法进行数据获取

原理

看一下react提供的unstable_createResource源码

export function unstable_createResource(fetch, maybeHashInput) {
  const resource = {
    read(input) {
      ...
      const result = accessResult(resource, fetch, input, key);
      switch (result.status) {
        // 还未完成直接抛出自身promise
        case Pending: {
          const suspender = result.value;
          throw suspender;
        }
        case Resolved: {
          const value = result.value;
          return value;
        }
        case Rejected: {
          const error = result.value;
          throw error;
        }
        default:
          // Should be unreachable
          return (undefined: any);
      }
    },
  };
  return resource;
}
复制代码

为此,React使用Promises。 组件能够在其render方法(或在组件的渲染过程当中调用的任何东西,例如新的静态getDerivedStateFromProps)中抛出Promise。 React捕获了抛出的Promise,并在树上寻找最接近的Suspense组件,Suspense其自己具备componentDidCatch,将promise当成error捕获,等待其执行完成其更改状态从新渲染子组件。

Suspense组件将一个元素(fallback 做为其后备道具,不管子节点在何处或为何挂起,都会在其子树被挂起时进行渲染。

如何达成异常捕获

  1. reconciliation阶段的 renderRoot 函数,对应异常处理方法是 throwException
  2. commit阶段的 commitRoot 函数,对应异常处理方法是 dispatch

reconciliation阶段的异常捕获

react-reconciler中的performConcurrentWorkOnRoot

// This is the entry point for every concurrent task, i.e. anything that
// goes through Scheduler.
// 这里是每个经过Scheduler的concurrent任务的入口
function performConcurrentWorkOnRoot(root, didTimeout) {
    ...
    do {
        try {
            //开始执行Concurrent任务直到Scheduler要求咱们让步
            workLoopConcurrent();
            break;
        } catch (thrownValue) {
            handleError(root, thrownValue);
        }
    } while (true);
    ...
}

function handleError(root, thrownValue) {
    ...
      throwException(
        root,
        workInProgress.return,
        workInProgress,
        thrownValue,
        renderExpirationTime,
      );
      workInProgress = completeUnitOfWork(workInProgress);
   ...
}
复制代码

throwException

do {
    switch (workInProgress.tag) {
      ....
      case ClassComponent:
        // Capture and retry
        const errorInfo = value;
        const ctor = workInProgress.type;
        const instance = workInProgress.stateNode;
        if (
          (workInProgress.effectTag & DidCapture) === NoEffect &&
          (typeof ctor.getDerivedStateFromError === 'function' ||
            (instance !== null &&
              typeof instance.componentDidCatch === 'function' &&
              !isAlreadyFailedLegacyErrorBoundary(instance)))
        ) {
          workInProgress.effectTag |= ShouldCapture;
          workInProgress.expirationTime = renderExpirationTime;
          // Schedule the error boundary to re-render using updated state
          const update = createClassErrorUpdate(
            workInProgress,
            errorInfo,
            renderExpirationTime,
          );
          enqueueCapturedUpdate(workInProgress, update);
          return;
        }
    }
    ...
}
    
复制代码

throwException函数分为两部分 一、遍历当前异常节点的全部父节点,找到对应的错误信息(错误名称、调用栈等),这部分代码在上面中没有展现出来

二、第二部分是遍历当前异常节点的全部父节点,判断各节点的类型,主要仍是上面提到的两种类型,这里重点讲ClassComponent类型,判断该节点是不是异常边界组件(经过判断是否存在componentDidCatch生命周期函数等),若是是找到异常边界组件,则调用 createClassErrorUpdate函数新建update,并将此update放入此节点的异常更新队列中,在后续更新中,会更新此队列中的更新工做

commit阶段

ReactFiberWorkLoop中的finishConcurrentRender=》 commitRoot=》 commitRootImpl=》captureCommitPhaseError

commit被分为几个子阶段,每一个阶段都try catch调用了一次captureCommitPhaseError

  1. 突变(mutate)前阶段:咱们在突变前先读出主树的状态,getSnapshotBeforeUpdate在这里被调用
  2. 突变阶段:咱们在这个阶段更改主树,完成WIP树转变为current树
  3. 样式阶段:调用从被更改后主树读取的effect
export function captureCommitPhaseError(sourceFiber: Fiber, error: mixed) {
  if (sourceFiber.tag === HostRoot) {
    // Error was thrown at the root. There is no parent, so the root
    // itself should capture it.
    captureCommitPhaseErrorOnRoot(sourceFiber, sourceFiber, error);
    return;
  }

  let fiber = sourceFiber.return;
  while (fiber !== null) {
    if (fiber.tag === HostRoot) {
      captureCommitPhaseErrorOnRoot(fiber, sourceFiber, error);
      return;
    } else if (fiber.tag === ClassComponent) {
      const ctor = fiber.type;
      const instance = fiber.stateNode;
      if (
        typeof ctor.getDerivedStateFromError === 'function' ||
        (typeof instance.componentDidCatch === 'function' &&
          !isAlreadyFailedLegacyErrorBoundary(instance))
      ) {
        const errorInfo = createCapturedValue(error, sourceFiber);
        const update = createClassErrorUpdate(
          fiber,
          errorInfo,
          // TODO: This is always sync
          Sync,
        );
        enqueueUpdate(fiber, update);
        const root = markUpdateTimeFromFiberToRoot(fiber, Sync);
        if (root !== null) {
          ensureRootIsScheduled(root);
          schedulePendingInteractions(root, Sync);
        }
        return;
      }
    }
    fiber = fiber.return;
  }
}
复制代码

captureCommitPhaseError函数作的事情和上部分的 throwException 相似,遍历当前异常节点的全部父节点,找到异常边界组件(有componentDidCatch生命周期函数的组件),新建update,在update.callback中调用组件的componentDidCatch生命周期函数。

细心的小伙伴应该注意到,throwException 和 captureCommitPhaseError在遍历节点时,是从异常节点的父节点开始遍历,因此异常捕获通常由拥有componentDidCatch或getDerivedStateFromError的异常边界组件进行包裹,而其是没法捕获并处理自身的报错。

Hook相关

Function Component和Class Component

Class component 劣势

  1. 状态逻辑难复用:在组件之间复用状态逻辑很难,可能要用到 render props (渲染属性)或者 HOC(高阶组件),但不管是渲染属性,仍是高阶组件,都会在原先的组件外包裹一层父容器(通常都是 div 元素),致使层级冗余 趋向复杂难以维护:
  2. 在生命周期函数中混杂不相干的逻辑(如:在 componentDidMount 中注册事件以及其余的逻辑,在 componentWillUnmount 中卸载事件,这样分散不集中的写法,很容易写出 bug ) 类组件中处处都是对状态的访问和处理,致使组件难以拆分红更小的组件
  3. this 指向问题:父组件给子组件传递函数时,必须绑定 this

可是在16.8以前react的函数式组件十分羸弱,基本只能做用于纯展现组件,主要由于缺乏state和生命周期。

hooks优点

  • 能优化类组件的三大问题
  • 能在无需修改组件结构的状况下复用状态逻辑(自定义 Hooks )
  • 能将组件中相互关联的部分拆分红更小的函数(好比设置订阅或请求数据)
  • 反作用的关注点分离:反作用指那些没有发生在数据向视图转换过程当中的逻辑,如 ajax 请求、访问原生dom 元素、本地持久化缓存、绑定/解绑事件、添加订阅、设置定时器、记录日志等。以往这些反作用都是写在类组件生命周期函数中的。而 useEffect 在所有渲染完毕后才会执行,useLayoutEffect 会在浏览器 layout 以后,painting 以前执行。

capture props和capture value特性

capture props

class ProfilePage extends React.Component {
  showMessage = () => {
    alert("Followed " + this.props.user);
  };

  handleClick = () => {
    setTimeout(this.showMessage, 3000);
  };

  render() {
    return <button onClick={this.handleClick}>Follow</button>;
  }
}
复制代码
function ProfilePage(props) {
  const showMessage = () => {
    alert("Followed " + props.user);
  };

  const handleClick = () => {
    setTimeout(showMessage, 3000);
  };

  return <button onClick={handleClick}>Follow</button>;
}
复制代码

这两个组件都描述了同一个逻辑:点击按钮 3 秒后 alert 父级传入的用户名。

那么 React 文档中描述的 props 不是不可变(Immutable) 数据吗?为啥在运行时还会发生变化呢?

缘由在于,虽然 props 不可变,是 this 在 Class Component 中是可变的,所以 this.props 的调用会致使每次都访问最新的 props。

无可厚非,为了在生命周期和render重能拿到最新的版本react自己会实时更改this,这是this在class组件的本职。

这揭露了关于用户界面的有趣观察,若是咱们说ui从概念上是一个当前应用状态的函数,事件处理就是render结果的一部分,咱们的事件处理属于拥有特定props或state的render。每次 Render 的内容都会造成一个快照并保留下来,所以当状态变动而 Rerender 时,就造成了 N 个 Render 状态,而每一个 Render 状态都拥有本身固定不变的 Props 与 State。

然而在setTimeout的回调中获取this.props会打断这种的关联,失去了与某一特定render绑定,因此也失去了正确的props。

而 Function Component 不存在 this.props 的语法,所以 props 老是不可变的。

测试地址

hook中的capture value

function MessageThread() {
  const [message, setMessage] = useState("");

  const showMessage = () => {
    alert("You said: " + message);
  };

  const handleSendClick = () => {
    setTimeout(showMessage, 3000);
  };

  const handleMessageChange = e => {
    setMessage(e.target.value);
  };

  return (
    <>
      <input value={message} onChange={handleMessageChange} />
      <button onClick={handleSendClick}>Send</button>
    </>
  );
}
复制代码

hook重一样有capture value,每次渲染都有本身的 Props and State,若是要时刻获取最新的值,规避 capture value 特性,能够用useRef

const lastest = useRef("");

const showMessage = () => {
    alert("You said: " + lastest.current);
};

const handleSendClick = () => {
    setTimeout(showMessage, 3000);
};

const handleMessageChange = e => {
    lastest.current = e.target.value;
};
复制代码

测试地址

Hooks实现原理

在上面fiber结构分析能够看出如今的Class component的state和props是记录在fiber上的,在fiber更新后才会更新到component的this.state和props里面,而并非class component本身调理的过程。这也给了实现hooks的方便,由于hooks是放在function component里面的,他没有本身的this,但咱们自己记录state和props就不是放在class component this上面,而是在fiber上面,因此咱们有能力记录状态以后,也有能力让function component更新过程中拿到更新以后的state。

React 依赖于 Hook 的调用顺序

平常调用三次

function Form() {
  const [hero, setHero] = useState('iron man');
  if(hero){
    const [surHero, setSurHero] = useState('Captain America');
  }
  const [nbHero, setNbHero] = useState('hulk');
  // ...
}
复制代码

来看看咱们的useState是怎么实现的

// useState 源码中的链表实现
import React from 'react';
import ReactDOM from 'react-dom';

let firstWorkInProgressHook = {memoizedState: null, next: null};
let workInProgressHook;

function useState(initState) {
    let currentHook = workInProgressHook.next ? workInProgressHook.next : {memoizedState: initState, next: null};

    function setState(newState) {
        currentHook.memoizedState = newState;
        render();
    }
	
	// 假如某个 useState 没有执行,会致使Next指针移动出错,数据存取出错
    if (workInProgressHook.next) {
        // 这里只有组件刷新的时候,才会进入
        // 根据书写顺序来取对应的值
        // console.log(workInProgressHook);
        workInProgressHook = workInProgressHook.next;
    } else {
        // 只有在组件初始化加载时,才会进入
        // 根据书写顺序,存储对应的数据
        // 将 firstWorkInProgressHook 变成一个链表结构
        workInProgressHook.next = currentHook;
        // 将 workInProgressHook 指向 {memoizedState: initState, next: null}
        workInProgressHook = currentHook;
        // console.log(firstWorkInProgressHook);
    }
    return [currentHook.memoizedState, setState];
}

function Counter() {
    // 每次组件从新渲染的时候,这里的 useState 都会从新执行
    const [name, setName] = useState('计数器');
    const [number, setNumber] = useState(0);
    return (
        <>
            <p>{name}:{number}</p>
            <button onClick={() => setName('新计数器' + Date.now())}>新计数器</button>
            <button onClick={() => setNumber(number + 1)}>+</button>
        </>
    )
}

function render() {
    // 每次从新渲染的时候,都将 workInProgressHook 指向 firstWorkInProgressHook
    workInProgressHook = firstWorkInProgressHook;
    ReactDOM.render(<Counter/>, document.getElementById('root'));
}

render();
复制代码

咱们来还原一下这个过程 你们看完应该了解,当下设置currentHook实际上是上个workInProgressHook经过next指针进行绑定获取的,因此若是在条件语句中打破了调用顺序,将会致使next指针指向出现误差,这个时候你传进去的setState是没法正确改变对应的值,由于

各类自定义封装的hooks =》react-use

为何顺序调用对 React Hooks 很重要?

THE END

第二次在掘金上发文,小陈也是react小菜🐔,但愿能跟你们一块儿讨论学习,向高级前端架构进阶!让咱们一块儿爱上fiber

参考:

如何以及为何React Fiber使用链表遍历组件树
React Fiber架构
React 源码解析 - reactScheduler 异步任务调度
展望 React 17,回顾 React 往事 全面 深刻
这多是最通俗的 React Fiber(时间分片) 打开方式=>调度策略
全面了解 React 新功能: Suspense 和 Hooks 生命周期
详谈 React Fiber 架构(1)

相关文章
相关标签/搜索