关于react.js:走进React-Fiber

2次阅读

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

本文重点:介绍 React 重构的起因和目标,了解 Fiber tree 单向链表构造中各属性含意,梳理调度过程和外围实现伎俩,深刻新的生命周期,hooks,suspense,异样捕捉等个性的用法和原理。

喜爱的就点个赞吧️,心愿跟大家在干燥的源码中挖掘学习的乐趣,一起分享提高。

当 react 刚推出的时候,最具革命性的个性就是虚构 dom,因为这大大降低了利用开发的难度,相比拟以往通知浏览器我须要怎么更新我的 ui,当初咱们只须要通知 react 我利用 ui 的下个状态是怎么样的,react 会帮咱们主动解决两者之间的所有事宜。

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

但在享受舒服开发体验的同时,有一些疑难始终萦绕在咱们脑海中:

  • 是什么导致了 react 用户交互、动画频繁卡顿
  • 如何眼帘优雅的异样解决,进行异样捕捉和备用 ui 渲染
  • 如何更好实现组件的复用和状态治理

这到底是兽性的扭曲,还是道德的沦丧 / 狗头

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

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

Fiber是对 React 外围算法的重构,2 年重构的产物就是 Fiber reconciler。

react 协调是什么

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

当初的 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 树,其扁平化的单链表构造的特点将以往递归遍历改为了循环遍历,实现深度优先遍历。<!– 三个属性串联了整个利用,可能以很高的效率把整个利用遍历完。–>
<!– 在任何时候一个组件实例只有两个 Fiber=>current 和 workinprogress。–>
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.

<!– 能够看到大多数 state 和 props 更新将 side-effects。因为利用 effects 是一种 work,fiber 节点是一种不便的机制,能够跟踪除更新之外的 effects。每个 fiber 节点都能够具备与之相干的 effects, 通过 fiber 节点中的 effectTag 字段示意。–>

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;

<!– 通过把 expirationTime 和 currentTime 化为 ms 单位,并计算他们的差值,通过判断差值落在哪个区间去判断属于哪个优先级。–>

源码中的 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 算法源码

  • ReactFiberExpirationTime
  • SchedulerWithReactIntegration

举荐浏览:jokcy 大神解析 =》expirationTime 计算

执行优先级

那么 Fiber 是如何做到异步实现不同优先级工作的协调执行的

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

requestIdleCallback:
在浏览器闲暇时段内调用的函数排队。是开发人员能够在主事件循环上执行后盾和低优先级工作而不会影响提早要害事件,如动画和输出响应。

其在回调参数中 IdleDeadline 能够获取到以后帧残余的工夫。利用这个信息能够正当的安顿以后帧须要做的事件,如果工夫足够,那持续做下一个工作,如果工夫不够就歇一歇。
<!– 还能够配置 timeout 参数,当工作超过多少时限未被执行将被强制执行,但有可能会造成失帧。–>

requestAnimationFrame:通知浏览器你心愿执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画

单干式调度: 这是一种’契约‘调度,要求咱们的程序和浏览器紧密结合,相互信赖。比方能够由浏览器给咱们调配执行工夫片,咱们要依照约定在这个工夫内执行结束,并将控制权还给浏览器。


Fiber 所做的就是须要合成渲染工作,而后依据优先级应用 API 调度,异步执行指定工作:

  • 低优先级工作由 requestIdleCallback 解决,限度工作执行工夫,以切分工作,同时防止工作长时间执行,阻塞 UI 渲染而导致掉帧。
  • 高优先级工作,如动画相干的由 requestAnimationFrame 解决;

并不是所有的浏览器都反对 requestIdleCallback,然而 React 外部实现了本人的 polyfill,所以不用放心浏览器兼容性问题。polyfill 实现次要是通过 rAF+postmessage 实现的(最新版本去掉了 rAF,有趣味的童鞋能够看看 =》SchedulerHostConfig

生命周期

因为其在协调阶段工作可被打断的特点,工作在切片后运行完一段便将控制权交还到 react 负责任务调度的模块,再依据工作的优先级,持续运行前面的工作。所以会导致某些组件渲染到一半便会打断以运行其余紧急,优先级更高的工作,运行完却不会持续之前中断的局部,而是从新开始,所以在协调的所有生命周期都会面临这种被屡次调用的状况。
为了限度这种被多次重复调用,消耗性能的状况呈现,react 官网一步步把处在协调阶段的局部生命周期进行移除。

废除:

  • componentWillMount
  • componentWillUpdate
  • componentWillReceiveProps

新增:

  • static getDerivedStateFromProps(props, state)
  • getSnapshotBeforeUpdate(prevProps, prevState)
  • componentDidcatch
  • staic getderivedstatefromerror

为什么新的生命周期用 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);
  }

<!–componentDidcatch 和的差异 –>

<!–https://stackoverflow.com/que…;

<!–https://zh-hans.reactjs.org/d…;

生命周期相干材料点这里 =》生命周期

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>

<!–// This is not a Promise. It’s a special object from our Suspense integration.–>
<!–// 这里 fetchProfileData 返回的不是 promise,而是一个 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 函数分为两局部
1、遍历以后异样节点的所有父节点,找到对应的错误信息(谬误名称、调用栈等),这部分代码在下面中没有展现进去

2、第二局部是遍历以后异样节点的所有父节点,判断各节点的类型,次要还是下面提到的两种类型,这里重点讲 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).md)

正文完
 0