React 设计理念
从 React 官网 我们可以看到这样一句话:
React 是用 JavaScript 构建
快速响应
的大型 Web 应用程序的首选方式。它在 Facebook 和 Instagram 上表现优秀。
我们知道目前主流浏览器刷新频率为 60Hz
,即每(1000ms / 60Hz)16.6ms
浏览器刷新一次。在这 16.6ms
要完成以下事情:
- 首先 JS 脚本执行;
- 执行请求动画帧 requestAnimationFrame(rAF),即在每次绘制之前,会执行 rAF 回调;
- 进行 Layout 操作,包括计算布局和更新布局;
- 进行 Paint 操作,得到树中每个节点的尺寸与位置等信息,浏览器针对每个元素进行内容填充;
- 接下来处于空闲阶段(Idle Peroid),可以在这时执行
requestIdleCallback
里注册的任务; 如果我们在 16.6ms 内没有完成,页面会出现一定程度的卡顿
现象。
React v15 架构
React 15 架构分为两层:
Reconciler
(协调器) : 负责找出变化的组件;Renderer
(渲染器): 负责将变化的组件渲染到页面上;
Reconciler(协调器)
React 中可以通过 this.setState、this.forceUpdate、ReactDOM.render
等API触发更新。
有更新发生时,Reconciler 会做如下工作:
- 调用函数组件、或 class 组件的 render 方法,将返回的
JSX
转化为虚拟 DOM
; - 将
虚拟 DOM
和上次更新时的虚拟 DOM 对比; - 通过对比找出本次更新中变化的虚拟 DOM;
- 通知
Renderer
(渲染器) 将变化的虚拟 DOM 渲染到页面上;
Renderer(渲染器)
React 支持跨平台,不同平台有不同的 Renderer。浏览器的Renderer 是 ReactDOM 库。
除此之外,还有其他平台的 Renderer:
- ReactNative:渲染 App 原生组件;
- ReactTest:渲染出纯 JS 对象用于测试;
- ReactArt:渲染到 Canvas,SVG 或 VML(IE8);
React 15 架构的缺点
在 React 15 版本中 Reconciler 叫 “stack” reconciler
,它会 mount(组件挂载)
时调用 mountComponent,update(组件更新)
的组件会调用 updateComponent。这两个方法都会使用递归更新
子组件,无法实现异步可中断的更新,调度优先级
。如果组件树的层级很深,递归会占用线程很多时间,导致的 JS 执行时间过长,超过16.6ms(主流的浏览器刷新频率为60Hz,即每(1000ms / 60Hz)),容易造成页面卡顿。
React 16 架构
React 16 架构可以分为三层:
Scheduler(调度器)
—— 调度任务的优先级
,高优先级的任务优先进入 Reconciler 中;Reconciler(协调器)
—— 负责找出变化的组件;Renderer(渲染器)
—— 负责将变化的组件渲染到页面上;
Scheduler(调度器)
我们知道部分浏览器已经实现了 requestIdleCallback
,当浏览器有剩余时间时通知我们。但是由于以下因素,React放弃使用:
- 浏览器兼容性;
- 触发频率不稳定;
基于以上原因,React 团队自己实现了 requestIdleCallback 的 polyfill
,就是 Scheduler。除了在空闲时触发回调的功能外,Scheduler 还提供了多种调度优先级供任务设置
。
Reconciler (协调器)
从 React15
到 React16
,协调器(Reconciler)重构的一大目的是:将老的同步更新
的架构变为异步可中断更新
。更新在执行过程中可能会被打断(浏览器时间分片用尽或有更高优任务插队),当可以继续执行时恢复之前执行的中间状态。适时地让出 CPU
执行权,可以让浏览器及时地响应用户的交互。
Generator
React 团队为什么没有使用 Generator 来实现异步可中断呢?React 团队成员 sebmarkbage
在 16 年的 issue
Fiber Principles: Contributing To Fiber 做出了解答。
放弃的主要原因如下:
- Generator 具有传染性,使用了 Generator 则上下文的其他函数也需要作出改变。
- 生成器是有状态的,无法在其中途恢复。
Fiber reconciler
“fiber” reconciler
是一个新尝试,致力于解决 stack reconciler 中固有的问题,同时解决一些历史遗留问题。Fiber 从 React 16 开始变成了默认的 reconciler。
Fiber
包含三层含义:
- 作为架构来说,之前
React15
的Reconciler
采用递归的方式执行,数据保存在递归调用栈中,所以被称为stack Reconciler
。React16 的Reconciler
基于 Fiber 节点实现,被称为Fiber Reconciler
。 - 作为
静态的数据结构
来说,每个 Fiber 节点对应一个React element
,采用链表
实现。保存了该组件的类型、对应的DOM节点等信息。 - 作为
动态的工作单元
来说,每个 Fiber 节点保存了本次更新中该组件改变的状态、要执行的工作(需要被删除/被插入页面中/被更新...)。
Fiber 主要目标是:
- 能够把可中断的任务切片处理。
- 能够调整优先级,重置并复用任务。
- 能够在父元素与子元素之间交错处理,以支持 React 中的布局。
- 能够在 render() 中返回多个元素。
- 更好地支持错误边界。
Fiber 结构
Fiber 结构在 React 源码中的定义,如下所示。
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// 作为静态数据结构的属性
this.tag = tag;
this.key = key;
this.elementType = null;
this.type = null;
this.stateNode = null;
// 用于连接其他 Fiber 节点形成 Fiber 树
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;
this.ref = null;
// 作为动态的工作单元的属性
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode;
// Effects
this.flags = NoFlags;
this.subtreeFlags = NoFlags;
this.deletions = null;
// 调度优先级相关
this.lanes = NoLanes;
this.childLanes = NoLanes;
// 指向该fiber在另一次更新时对应的fiber
this.alternate = null;
}
作为架构
// 指向父级 Fiber 节点
this.return = null;
// 指向子 Fiber 节点
this.child = null;
// 指向右边第一个兄弟 Fiber 节点
this.sibling = null;
比如像下面的例子:
function App() {
return (
<div>
<span> Hello </span>
<span> World </span>
</div>
)
}
对应的 Fiber
树结构:
作为静态的数据结构
// Fiber对应组件的类型 Function/Class/Host...
this.tag = tag;
// key属性
this.key = key;
// 大部分情况同type,某些情况不同,比如FunctionComponent使用React.memo包裹
this.elementType = null;
// 对于 FunctionComponent,指函数本身,对于ClassComponent,指class,对于HostComponent,指 DOM 节点 tagName
this.type = null;
// Fiber对应的真实 DOM 节点
this.stateNode = null;
作为动态的工作单元
作为动态的工作单元,Fiber 中如下参数保存了本次更新相关的信息。
// 保存本次更新造成的状态改变相关信息
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode;
// 保存本次更新会造成的 DOM 操作
this.effectTag = NoEffect;
this.nextEffect = null;
this.firstEffect = null;
this.lastEffect = null;
Fiber 双缓存树
在 React 中最多会同时存在两棵 Fiber 树。当前屏幕上显示内容对应的 Fiber 树称为current Fiber
树,正在内存中
构建的 Fiber 树称为 workInProgress Fiber
树。
current Fiber 树中的 Fiber 节点被称为 current fiber
,workInProgress Fiber树中的 Fiber 节点被称为 workInProgress fiber
,他们通过 alternate
属性连接。
currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;
React 应用的根节点通过使 current
指针在不同 Fiber 树的 rootFiber
间切换来完成current Fiber 树指向的切换。
当 workInProgress Fiber
树构建完成交给 Renderer
渲染在页面上后,应用根节点的current 指针指向 workInProgress Fiber 树,此时 workInProgress Fiber 树就变为current Fiber 树。
每次状态更新都会产生新的 workInProgress Fiber 树,通过 current 与 workInProgress 的替换,完成DOM更新。看下面的例子:
1. 组件 mount
时
import React from "react";
import ReactDOM from "react-dom";
function App() {
const [num, addNum] = React.useState(0);
return <div onClick={() => addNum(num + 1)}>{num}</div>;
}
ReactDOM.render(<App />, document.getElementById("root"));
- 首次执行
ReactDOM.render
会创建fiberRootNode
和rootFiber
。其中fiberRootNode 是整个应用的根节点
,rootFiber 是所在组件树的根节点
。之所以要区分 fiberRootNode 与 rootFiber,是因为在应用中我们可以多次调用 ReactDOM.render 渲染不同的组件树,他们会拥有不同的 rootFiber 。但是整个应用的根节点只有一个
,那就是fiberRootNode
。fiberRootNode 的 current 指针会指向当前页面上已渲染内容对应 Fiber 树,即 current Fiber 树。
由于是首屏渲染
,页面中还没有挂载任何 DOM ,所以 fiberRootNode.current 指向的rootFiber 没有任何子 Fiber 节点,此时 current Fiber 树为空
。
- 接下来进入
render
阶段,根据组件返回的JSX
在内存中依次创建 Fiber 节点并连接在一起构建 Fiber 树,被称为workInProgress Fiber
树。在构建 workInProgress Fiber 树时会尝试复用 current Fiber 树中已有的 Fiber 节点内的属性,在首屏渲染时只有 rootFiber 存在对应的 current fiber。
- 已构建完的 workInProgress Fiber 树在
commit
阶段渲染到页面。
2. 组件 update
时
- 当我们点击
div
节点触发状态改变,开启一次新的render
阶段并构建一棵新的workInProgress Fiber
树。
workInProgress Fiber
树在render
阶段完成构建后进入commit
阶段渲染到页面上。渲染完毕后,workInProgress Fiber 树变为 current Fiber 树。