React Fiber 架构理解

React Fiber 架构理解

引用原文: React Fiber Architecture

React Fiber is an ongoing reimplementation of React's core algorithm. It is the culmination of over two years of research by the React team.javascript

The goal of React Fiber is to increase its suitability for areas like animation, layout,and gestures. Its headline feature is incremental rendering: the ability to split rendering work into chunks and spread it out over multiple frames.html

Other key features include the ability to pause, abort, or reuse work as new updates come in; the ability to assign priority to different types of updates; and new concurrency primitives.java

React Fibre 是 React 核心算法正在进行的从新实现。它是 React 团队两年多的研究成果。react

React Fiber 的目标是提升其对动画,布局和手势等领域的适用性。它的主体特征是增量渲染:可以将渲染工做分割成块,并将其分散到多个帧中。git

其余主要功能包括在进行更新时暂停,停止或从新使用工做的能力,为不一样类型的更新分配优先权的能力和新的并发原语。github

React16以前组件的渲染逻辑

先来看一下react组件渲染时经历的生命周期:web

挂载阶段:算法

  • constructor()
  • componentWillMount()
  • render()
  • componentDidMount()

更新阶段:npm

  • componentWillReceiveProps()
  • shouldComponentUpdate()
  • componentWillUpdate()
  • render()
  • componentDidUpdate

卸载阶段:redux

  • componentWillUnmount()

在以前的版本中,若是你实现一个很复杂的深度嵌套的复合组件,会出现下面的状况:

现有层级关系以下的四个组件:

图片描述

组件渲染时调用的生命周期顺序:

图片描述

上图展现的是A,B,C,D的挂载阶段调用的生命周期渲染顺序,能够看到从顶层组件开始调用各生命周期,一直向下,直至调用完最底层子组件的生命周期。而后再向上调用。
组件更新阶段同理。

组件挂载以后,假如修改最上层组件的数据(state),组件更新时的调用栈:

图片描述

若是这是一个很大,层级很深的组件,能够想像到,组件在渲染时,调用栈过长,再加上若是在期间进行了各类复杂的操做,就可能致使长时间阻塞主线程,react渲染它须要几十甚至几百毫秒,这样的话react就会一直占用浏览器主线程,任何其余的操做(包括用户的点击,鼠标移动等操做)都没法执行,带来很是很差的用户体验。

React Fiber的出现

React Fiber 就是为了解决上面的问题而生。

好似一个潜水员,当它一头扎进水里,就要往最底层一直游,直到找到最底层的组件,而后他再上岸。 在这期间,岸上发生的任何事,都不能对他进行干扰,若是有更重要的事情须要他去作(如用户操做),也必须得等他上岸。

Fiber 本质上是一个虚拟的堆栈帧,新的调度器会按照优先级自由调度这些帧,从而将以前的同步渲染改为了异步渲染,在不影响体验的状况下去分段计算更新。它让潜水员会每隔一段时间就上岸,看是否有更重要的事情要作。

对于如何区别优先级,React 有本身的一套逻辑。对于动画这种实时性很高的东西,也就是 16 ms 必须渲染一次保证不卡顿的状况下,React 会每 16 ms(之内) 暂停一下更新,返回来继续渲染动画。

React Fiber 架构

调度拆分为小任务

浏览器自己也不断进化中,随着页面由简单的展现转向WebAPP,它须要一些新能力来承载更多节点的展现与更新。
下面是一些自救措施:

  • requestAnimationFrame
  • requestIdleCallback
  • web worker
  • IntersectionObserver

react官方采用的是 requestIdleCallback,为了兼容全部平台,facebook 单独实现了其功能,做为一个独立的 npm 包使用 react-schedule

其做用是会在浏览器空闲时期依次调用函数, 这就能够在主事件循环中执行后台或低优先级的任务,并且不会对像动画和用户交互这样延迟触发并且关键的事件产生影响。函数通常会按先进先调用的顺序执行,除非函数在浏览器调用它以前就到了它的超时时间。

简化后的大体流程图以下:

图片描述

Fiber Node 及 Fiber Tree

  • 从流程图上看到会有 Fiber Node 节点,这个是在 react 生成的 Virtual Dom 基础上增长的一层数据结构,主要是为了将递归遍历转变成循环遍历,配合 requestIdleCallback API, 实现任务拆分、中断与恢复。为了实现循环遍历,Fiber Node 上携带了更多的信息。
  • 每个 Fiber Node 节点与 Virtual Dom 一一对应,全部 Fiber Node 链接起来造成 Fiber tree, 是个单链表树结构

两个阶段:reconciliationcommit

对于异步渲染,如今渲染有两个阶段:reconciliationcommit 。前者过程是能够打断的,后者不能暂停,会一直更新界面直到完成。

reconciliation 处理过程

  • 当执行 setState() 或首次 render() 时,进入工做循环,循环体中处理的单元为 Fiber Node, 便是拆分任务的最小单位,从根节点开始,自顶向下逐节点构造 workInProgress tree(构建中的新 Fiber Tree)。
  • 每一个工做处理单元作的事情,由 beginWork(), completeUnitOfWork() 两部分构成。
  • beginWork()主要作的事情是从顶向下生成全部的 Fiber Node,并标记 Diff, 不包括兄弟节点,每一个 Fiber Node 的处理过程根据组件类型略有差别,以 ClassComponent 为例:

1 若是当前节点不须要更新,直接把子节点clone过来,要更新的话标记更新类型
2 更新当前节点状态(props, state, context等)
3 调用shouldComponentUpdate()
4 调用组件实例方法 render() 得到新的子节点,并为子节点建立 Fiber Node(建立过程会尽可能复用现有 Fiber Node,子节点增删也发生在这里)
5 若是没有产生 child fiber,进入下一阶段 completeUnitOfWork

  • completeUnitOfWork() 当没有子节点,开始遍历兄弟节点做为下一个处理单元,处理完兄弟节点开始向上回溯,直到再次回去根节点为止,将收集向上回溯过程当中的全部 diff,拿到 diff 后开始进入 commit 阶段。
  • 构建 workInProgress tree 的过程就是 diff 的过程,经过 requestIdleCallback 来调度执行一组任务,每完成一个任务后回来看看有没有插队的(更紧急的),把时间控制权交还给主线程,直到下一次 requestIdleCallback 回调再继续构建workInProgress tree。

两个阶段涉及到的生命周期:

Reconciliation 阶段 (React算法,用来比较2颗树,以肯定哪些部分须要从新渲染

  • componentWillMount
  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate

Commit 阶段 (用于呈现React应用的数据更改。一般是setState的结果。最终致使从新渲染。

  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

由于 reconciliation 阶段是能够被打断的,因此 reconciliation 阶段会执行的生命周期函数就可能会出现调用屡次的状况,从而引发 Bug。因此对于 reconciliation 阶段调用的几个函数,除了 shouldComponentUpdate 之外,其余都应该避免去使用,而且 React16 中也引入了新的 API 来解决这个问题。

因而官方推出了getDerivedStateFromProps,让你在render设置新state,你主要返回一个新对象,它就主动帮你setState。因为这是一个静态方法,你不能取到 this,固然你也不能操做instance,这就阻止了你屡次操做setState。这样一来,getDerivedStateFromProps的逻辑应该会很简单,这样就不会出错,不会出错,就不会打断DFS过程。

getDerivedStateFromProps取代了原来的componentWillMountcomponentWillReceiveProps方法,该函数会在组件 初始化 和 更新 时被调用

class ExampleComponent extends React.Component {
  // Initialize state in constructor,
  // Or with a property initializer.
  state = {};

  static getDerivedStateFromProps(nextProps, prevState) {
    if (prevState.someMirroredValue !== nextProps.someValue) {
      return {
        derivedData: computeDerivedState(nextProps),
        someMirroredValue: nextProps.someValue
      };
    }

    // Return null to indicate no change to state.
    return null;
  }
}

在进入commi阶段时,组件多了一个新钩子叫getSnapshotBeforeUpdate,它与commit阶段的钩子同样只执行一次。
getSnapshotBeforeUpdate 用于替换 componentWillUpdate ,该函数会在 update 后 DOM 更新前被调用,用于读取最新的 DOM 数据。

因而整个流程变成这样:(引用大神@司徒正美的图)

React16 生命周期函数用法建议

结合 React Fiber 架构 建议以下使用react生命周期

class ExampleComponent extends React.Component {
  // 用于初始化 state
  constructor() {}
  // 用于替换 `componentWillReceiveProps` ,该函数会在初始化和 `update` 时被调用
  // 由于该函数是静态函数,因此取不到 `this`
  // 若是须要对比 `prevProps` 须要单独在 `state` 中维护
  static getDerivedStateFromProps(nextProps, prevState) {}
  // 判断是否须要更新组件,多用于组件性能优化
  shouldComponentUpdate(nextProps, nextState) {}
  // 组件挂载后调用
  // 能够在该函数中进行请求或者订阅
  componentDidMount() {}
  // 用于得到最新的 DOM 数据
  getSnapshotBeforeUpdate() {}
  // 组件即将销毁
  // 能够在此处移除订阅,定时器等等
  componentWillUnmount() {}
  // 组件销毁后调用
  componentDidUnMount() {}
  // 组件更新后调用
  componentDidUpdate() {}
  // 渲染组件函数
  render() {}
  // 如下函数不建议使用
  UNSAFE_componentWillMount() {}
  UNSAFE_componentWillUpdate(nextProps, nextState) {}
  UNSAFE_componentWillReceiveProps(nextProps) {}
}

16 大版本主要更新还解决如下痛点:

  • 组件不能返回数组,最见的场合是UL元素下只能使用LI,TR元素下只能使用TD或TH,这时这里有一个组件循环生成LI或TD列表时,咱们并不想再放一个DIV,这会破坏HTML的语义。
  • 弹窗问题,以前一直使用不稳定的unstable_renderSubtreeIntoContainer。弹窗是依赖原来DOM树的上下文,所以这个API第一个参数是组件实例,经过它获得对应虚拟DOM,而后一级级往上找,获得上下文。它的其余参数也很好用,但这个方法一直没有转正。。。
  • 异常处理,咱们想知道哪一个组件出错,虽然有了React DevTool,可是太深的组件树查找起来仍是很吃力。但愿有个方法告诉我出错位置,而且出错时能让我有机会进行一些修复工做
  • HOC的流行带来两个问题,毕竟是社区兴起的方案,没有考虑到ref与context的向下传递。
  • 组件的性能优化全凭人肉,而且主要集中在SCU,但愿框架能干些事情,即便不用SCU,性能也能上去。

新特性:

  • render / 纯组件可以 return 任何数据结构
  • CreatePortal API,更好的处理 Dialog 这种场景组件
  • 新的 context api,尝试代替一部分 redux 的职责
  • 异步渲染/时间切片(time slicing),成倍提升性能
  • componentDidCatch,错误边界,框架层面上提升用户 debug 的能力
  • 网络请求 IO(Suspense),更好的处理异步网络 IO

参考资料:

相关文章
相关标签/搜索