前言
作为使用最广泛的前端框架,React 虽然不是性能最好的框架,但一定是开发项目最稳妥的框架,无论什么样的项目基本上 React 都会有相应的社区解决方案;
JSX
我认为像理解jsx
首先要理解的就是在 React 中一切都是数据驱动的,所以我们书写的 jsx 归根结底是一段数据。
jsx
本质上是 React.createElement() 方法的语法糖,书写的 jsx 在构建阶段会被转化为 JS。 这一步的转换是使用Babel
实现的。
javascript
// JSX 代码
const element = <div className="container">Hello React</div>;
// 编译后的 JavaScript
const element = React.createElement(
"div",
{ className: "container" },
"Hello React"
);
这里React.createElement()
并不是直接创建 HTMLElement,的,而是创建一个虚拟 DOM 对象ReactElement
,这个对象包含了要创建的元素信息,比如标签名、属性、子元素等等。
javascript
const element = {
$$typeof: Symbol(react.element),
type: "div",
key: null,
ref: null,
props: {
className: "container",
children: {
$$typeof: Symbol(react.element),
type: "h1",
props: {
children: "Hello React",
},
},
},
_owner: null,
_store: { validated: false },
};
React 17 之后可以不需要引入 React 模块,import React from 'react'
,就可以直接书写jsx
这是因为在浏览器中无法直接使用 jsx,所以要借助工具如@babel/preset-react 将 jsx 语法转换为 React.createElement 的 js 代码,所以需要显式引入 React,才能正常调用 createElement。 为什么这样改动,官方文档的回答是为了更好的性能优化 React17 文档.
当然React.createElement()
方法官方还会继续支持。
javascript
//jsx语法
function App() {
return <h1>Hello React</h1>;
}
// 这里是编译器自动引入的
import { jsx as _jsx } from "react/jsx-runtime";
function App() {
return _jsx("h1", { children: "Hello React" });
}
Fiber
Fiber 是 React 16.0 引入的一个架构,用于解决 React 的性能问题 ;fiber 在 React 中是最小粒度的执行单元,无论 React 还是 Vue ,在遍历更新每一个节点的时候都不是用的真实 DOM ,都是采用虚拟 DOM ,所以可以理解成 fiber 就是 React 的虚拟 DOM ; React 的虚拟 Dom 更新的整个过程都相对复杂,所以只有理解 fiber 才能更好的理解 React 的更新过程。
什么是 fiber?
fiber 是一段用来描述元素和元素之间关系及更新优先级的数据结构。 每一个 ReactElement 对象都对应一个 fiber 对象;我们书写的 jsx 会通过 Babel 转化为 React.createElement()
方法调用,调用后会返回 ReactElement 对象,ReactElement 对象会有与之相对应的 fiber 对象。
Dom 是树结构,那么 fiber 之间是如何建立联系的,每个 fiber 节点是如何知道自己所处的位置的呢?
React 在初始化阶段也就是第一次渲染会自顶向下遍历 ReactEment 树,生成相对应的 fiber 链表树。 ReactEment 是标准的树结构 ,只会记录父子关系, fiber 中会记录子父关系,以及兄弟关系。 每一个 fiber 是通过 return , child ,sibling 三个属性建立起联系的。
- return: 指向父级 Fiber 节点。
- child: 指向子 Fiber 节点。
- sibling:指向兄弟 fiber 节点。
fiber 中还有一个重要概念 fiberRoot
和 rootFiber
fiberRoot
可以看作整个 React 应用的根节点, rootFiber
是通过 ReactDOM.render(element, container)
创建的。 一个应用中可以创建多个ReactDOM.render(element, container)
。
fiber 数据结构
javascript
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;
this.effectTag = NoEffect;
this.nextEffect = null;
this.firstEffect = null;
this.lastEffect = null;
// 调度优先级相关
this.lanes = NoLanes;
this.childLanes = NoLanes;
// 指向该fiber在另一次更新时对应的fiber
this.alternate = null;
}
Fiber 架构
虽然说可以将 fiber 看作是 React 的虚拟 Dom,但它又不止是虚拟 Dom,fiber 还承担着更多的含义。
Fiber 包含三层含义:引用卡颂 ReactReact 技术揭秘
-
作为架构来说,之前 React15 的 Reconciler 采用递归的方式执行,数据保存在递归调用栈中,所以被称为 stack Reconciler。React16 的 Reconciler 基于 Fiber 节点实现,被称为 Fiber Reconciler。
-
作为静态的数据结构来说,每个 Fiber 节点对应一个 React element,保存了该组件的类型(函数组件/类组件/原生组件...)、对应的 DOM 节点等信息。
-
作为动态的工作单元来说,每个 Fiber 节点保存了本次更新中该组件改变的状态、要执行的工作(需要被删除/被插入页面中/被更新...)
总结一下,fiber 是根据 ReactElement 树生成的 fiber 链表树,记录着 ReactElement 树和 DOM 树之间的映射关系,可以通过return
child
sibling
记录 fiber 节点之间的关系;除了 ReactElemnt 映射的 fiber 节点还有fiberRoot
和 rootFiber
这两个节点;fiberRoot 是根节点,rootFiber 是根节点的 fiber 节点。 fiber 不止是数据结构,在 React 中fiber
即可以指架构也可以指具体的节点FiberNode
;FiberNode 是 fiber 架构实现的数据基础。
为什么要使用 fiber
在 React15 以及之前的版本,使用递归的方式对虚拟 Dom 进行遍历更新的,随着项目扩大组件的层级会变深,JS 的执行时间就会越来越长,而递归一旦开始就无法停止,所以给用户的感觉就是页面卡顿,每一次交互页面都要等 JS 全部执行完(虚拟 Dom 树更新完成)才会出现变化 React15 架构缺点。
所以为了解决这一问题在 React16 中使用 fiber 节点作为数据结构记录更新的优先级,实现可中断的更新,从而解决卡顿的问题 也就是 fiber 所包含的三层含义。
fiber 解决卡断是多个模块共同作用下实现的;整个React架构
可以分为三层
- Scheduler(调度器)------ 调度任务的优先级,高优任务优先进入 Reconciler
- Reconciler(协调器)------ 负责找出变化的组件
- Renderer(渲染器)------ 负责将变化的组件渲染到页面上

Scheduler(调度器)
在全面了解调度器之前需要先了解一个概念时间切片 详情链接。
浏览器每执行一次事件循环的过程中处理事件,执行 js ,调用 requestAnimation ,布局 Layout ,绘制 Paint,在这期间如果有空闲时间就会执行 React 任务。
时间切片过程

React 是如何知道浏览器什么时候有空闲时间的呢?
- 帧时间计算(Frame Timing)通过 requestAnimationFrame 获取精确的帧时间基准;
- MessageChannel 实现任务调度获取浏览器空闲时间
MessageChannel 本质上就是在任务队列中添加一个宏任务,事件循环结束上一个宏任务后会执行当前我们添加的宏任务,也就是触发 onMessage,这样 React 就知道了当前是否处于空闲时间。
空闲时间检测流程

时间切片与事件循环的协作

完整的 Scheduler(调度器)
除了时间切片以外,还包含了任务优先级的控制,例如当前 React 有很多任务要执行什么样的任务先进入浏览器的空闲时间执行。 如果空闲时间无法完整执行任务,如何中断后恢复任务;还有低优先级的任务一直无法得到执行要如何处理。
Reconciler(协调器)
Reconciler
本质上就是计算 Dom 树变化生成更新指令供 Renderer 执行;
在 React16 之前,React 是递归处理虚拟 dom,处理完成后会直接渲染;React16 之后,通过时间切片来处理虚拟 dom,每个 fiber 都是最小工作单元,当 React 没有将所有 fiber 处理完,会等待下一个时间切片,处理剩余的 fiber;直到全部处理完成交由 Renderer 渲染。这中间对于 fiber 的处理都是由Reconciler
来进行的。 Scheduler
主导循环,workLoopConcurrentByScheduler
函数控制执行流程
javascript
//packages/react-reconciler/src/ReactFiberWorkLoop.js
export function scheduleUpdateOnFiber(
root: FiberRoot,
fiber: Fiber,
lane: Lane
) {
if (__DEV__ && ReactSharedInternals.actQueue !== null) {
workLoopSync();
} else if (enableThrottledScheduling) {
workLoopConcurrent(includesNonIdleWork(lanes));
} else {
workLoopConcurrentByScheduler(); // 重点看这里
}
}
Reconciler执行单元任务每次循环调用 performUnitOfWork()
处理单个 Fiber 节点 。
javascript
//packages/react-reconciler/src/ReactFiberWorkLoop.js
function workLoopConcurrentByScheduler() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
// $FlowFixMe[incompatible-call] flow doesn't know that shouldYield() is side-effect free
performUnitOfWork(workInProgress);
}
}
shouldYield()
检查是否应该中断(基于调度策略): 时间片耗尽(默认 5ms)| 有更高优先级任务(如用户输入)| 浏览器需要绘制帧
javascript
function shouldYield() {
// 检查是否接近渲染截止时间
const timeRemaining = frameDeadline - performance.now();
// 预留 1ms 给浏览器渲染
if (timeRemaining <= 1) {
return true;
}
// 检查是否有待处理的用户输入
if (navigator.scheduling?.isInputPending()) {
return true;
}
return false;
}
在render阶段会存在两棵fiber树, current fiber Tree 和 workInProgress fiber Tree。 current fiber Tree
是当前页面正在渲染的fiber树(初始化的时候构建好),workInProgress fiber Tree
是当前正在处理的fiber树。
双缓存 Fiber 树 和 diff
初始化: Reconciler render阶段
会从root开始深度遍历,创建fiberRoot 和 rootFiber,因为是首屏渲染所以当下是没有Current fiber Tree
,此时在内存中渲染 workInProgress fiber Tree
所以React会创建一个 rootFiber
作为 Current fiber Tree
,即fiberRoot.current = rootFiber;
两棵树会通过alternate
属性互相关联起来。
在commit阶段Current指针会指向刚刚创建好的
workInProgress fiber Tree
;

更新: 当我们有setState或者其他操作的时候,React会创建一个 workInProgress fiber Tree

创建 workInProgress fiber Tree
的阶段 React不会直接创建而是判断是否可以复用旧树,如果可以复用则复用,不可以复用则创建新的树 ,这里用到diff算法。 也就是说React遍历jsx对象,对比current fiber Tree
创建 workInProgress fiber Tree
;
由于Diff操作本身也会带来性能损耗,React文档中提到,即使在最前沿的算法中,将前后两棵树完全比对的算法的复杂程度为 O(n 3 ),其中n是树中元素的数量。 所以React做出了限制
- 同层比较:只比较同层级节点同级节点如果节点类型不同会直接销毁旧节点创建新节点。
- 组件类型一致:相同类型更新属性 不同类型销毁整个子树。
- Key 优化 稳定的key可以帮助React识别出节点。
单点diff 单点diff就是判断key是否相同 type是否相同,来决定节点是否复用,如果相同会复用旧节点生成新节点。
多点diff 多点diff React会分两次进行遍历
第一次会遍历 更新的节点也就是顺序匹配相同位置的节点
第二轮遍历处理 删除 新增 移动 删除:workInProgress fiber Tree
遍历完后,current fiber Tree
还有节点,删除节点。 新增:current fiber Tree
遍历完,还有节点,新增节点。
移动是diff中最难处理的部分 current fiber Tree
和 workInProgress fiber Tree
都没有遍历完就代表组件顺序发生了变化。 移动过程中创建旧节点Map,遍历新节点标记移动。
总结下双缓存树和diff算法 为什么使用双缓存树,是因为避免只使用一棵树出现渲染闪烁的情况,所以使用双缓存树,一颗在内存中构建好,一颗是当前渲染树,我们可以在内存中绘制当前帧动画,绘制完毕后直接用当前帧替换上一帧画面,由于省去了两帧替换间的计算时间,不会出现从白屏到出现画面的闪烁情况。 diff算法本质上是O(n 3 )的算法,为了优化空间复杂度,所以React做出了三点限制也可以说是优化;所以我们作为开发者在开发的过程中也需要注意,
- Key值稳定:使用唯一稳定标识
- 组件稳定:避免频繁变更组件类型
- 状态提升:减少深层状态传递
- 虚拟列表:超长列表必须虚拟化
总结下Reconciler 当Scheduler将任务交给Reconciler后,Reconciler会为变化的虚拟 DOM 打上代表增/删/更新的标记。 当整棵fiber树都处理完成后 Reconciler
也就完成了核心阶段中的render
阶段。 假设更新触发了 3 个 Fiber 需要处理: Scheduler 启动任务 Reconciler 处理 Fiber 1(标记变更) Scheduler 检查:时间充裕 → 继续 Reconciler 处理 Fiber 2(标记变更) Scheduler 检测到用户点击 → 中断 处理点击事件(高优先级) Scheduler 重新安排 → Reconciler 处理 Fiber 3 完成整棵树协调 → 进入 Commit 阶段
Renderer(渲染器)
当Scheduler
和 Reconciler
全部执行完后,会把带着增/删/更新的标记的副作用链表(effect list),而非整棵树这个链表只包含需要变更的 Fiber 节点(通过 firstEffect 和 nextEffect 连接)Renderer(渲染器)
来渲染
Renderer(渲染器) 称为commit
阶段主要做的事就是执行effectList,新增元素,更新元素,删除元素。进行真实的 DOM 操作,执行useEffect或生命周期,获取ref等操作。
- Before Mutation 阶段(DOM 变更前)
- Mutation 阶段(DOM 变更中)
- Layout 阶段(DOM 变更后) 树切换发生在 Layout 阶段结束时
总结下React整个渲染流程
