一 前置知识
我们知道,在浏览器中,页面是一帧一帧绘制出来的,渲染的帧率与设备的刷新率保持一致。大多数设备的屏幕刷新率为1s 60次,当每秒内绘制的帧数(FPS)超过60时,页面渲染是流畅的;而当FPS小于60时,会出现一定程度的卡顿现象。
1000ms / 60 = 16.7ms
浏览器是多进程的。
- 浏览器主进程
- GPU进程
- 第三方插件进程
- 浏览器渲染进程
浏览器渲染进程又是多线程的。
- GUI渲染线程
- JS引擎线程
- 定时触发线程
- 事件触发线程
- 异步http请求线程
我们知道,JS是一个单线程的语言。浏览器中UI渲染线程和js线程互斥,执行js时无法进行UI渲染,长时间执行js导致UI渲染线程长时间挂起,页面就会卡顿甚至直接卡死。
完整的一帧都做了什么事情。
1 用户进行输入或者执行点击等操作
2 执行事件的回调
3 处理开始帧,例如resize、scroll等
4 在绘制之前执行requestAnimationFrame(请求动画帧)
5 Layout 计算布局,位置信息
6 重绘,根据尺寸和位置进行元素的内容填充
7 前六个阶段处理完成之后,可能用时到不了16.7ms,仅仅只用了4ms,这时候会有一个空闲阶段。这时候是可以去执行requestIdleCallback里的注册任务。(下面再去讲这个回调函数,它是React Fiber 的实现基础)
二 React
JSX 是如何转换的
const element = (
<div>
<div>我是测试demo</div>
<div>我是测试demo</div>
<div>我是测试demo</div>
</div>
)
React 是如何工作的
import React from "react";
import ReactDOM from "react-dom";
const element = (
<div>
<div>我是测试demo</div>
<div>我是测试demo</div>
<div>我是测试demo</div>
</div>
);
console.log(element);
ReactDOM.render(element, document.getElementById("root"));
下面的就是我们常说的虚拟DOM
我们简化一下
const element = {
type: 'div',
props: {
children: [
{
type: 'div',
props: {
children: '我是测试demo'
}
},
{
type: 'div',
props: {
children: '我是测试demo'
}
},
{
type: 'div',
props: {
children: '我是测试demo'
}
}
]
}
}
一个React组件的渲染主要经历两个阶段
调度阶段:用新的数据生成一个新的树,通过 diff 算法遍历旧的树,快速找出需要更新的元素,放到更新队列中,得到新的更新队列。
渲染阶段:遍历更新队列,通过调用宿主环境api,实际更新渲染对应的元素。
React 15
在 react16 引入 Fiber 架构之前,react使用的架构是《栈调和》(stack reconciler),react 会采用递归对比虚拟DOM树,通过 diff算法 找出需要变动的节点,然后同步更新它们,这个过程 react 称为reconcilation(协调)。也就是通过例如react-dom类库使虚拟dom与真实的dom同步,这个过程就是协调。
在reconcilation 期间,react 会一直占用浏览器资源,会导致用户触发的事件得不到响应。react 15 是如何将 JSX 渲染到页面上的。通过深度优先遍历,遍历自上到下进行遍历。
遍历的顺序 A1 -> B1 -> C1 -> C2 -> B2 -> C3 -> C4
这种遍历是递归调用,执行栈会越来越深,而且不能中断,中断后就不能恢复了。递归如果非常深,就会十分卡顿。如果递归花了100ms,则这100ms浏览器是无法响应的,而我们感觉流畅的最大时间也就是16.7ms,代码执行时间越长卡顿越明显。传统的方法存在不能中断和执行栈太深的问题。
一个小🌰
谢尔宾斯基三角形:百度百科
- 在每一帧渲染之前(通过requestAnimationFrame方法)给最外层的div设置一个缩放的transform,也就是让整个div开启一个不停变大缩小的动画。
- 设置一个定时器,每过1秒钟就改变一次组件的状态,也就是执行一次setState,并且demo中的所有子组件都会受到影响并render一次。
stack 掉帧 claudiopro.github.io/react-fiber…
fiber 不掉帧 claudiopro.github.io/react-fiber…
三 React Fiber
为了解决react15中组件render过程耗时过多,或者参与调和阶段的虚拟DOM节点过多的问题,那么react团队在react16版本提出了fiber架构和scheduler任务调度。fiber架构的目的是【能够独立执行每个虚拟DOM的调和阶段】,而不是每次执行整个虚拟DOM树的调和阶段。
什么是 React Fiber
Fiber的英文含义叫做"纤维",计算机领域中有两个大家很熟悉的概念:进程(Process)和线程(Thread)意思就是指的比Thread更细的概念,也就是比线程控制的更加精密的并发处理结构。
React Fiber并不是所谓的纤程(微线程)那种概念。而是一种基于浏览器的单线程调度算法。背后其实是基于 requestIdleCallback 这个API,Fiber是一种将 recocilation (递归 diff),拆分成无数个小任务的算法;它随时能够停止,恢复。停止恢复的时机取决于当前的一帧(16ms)内,还有没有足够的时间允许计算。
四 React Fiber 实现原理
一个执行单元
Fiber 可以理解为一个执行的单元,如下图所示,浏览器在每一帧都有一个空闲时间,react正是利用这个空闲时间去执行分片后优先级较高的任务。一旦空闲时间结束之后把执行权再交给浏览器。
React 和浏览器配合调度关系图
requestAnimationFrame
react fiber 中使用了 requestAnimationFrame 。window.requestAnimationFrame 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行
如果我想浏览器在每一帧中,将页面 div 元素的宽度变长1px,到达 100 px 结束。例如这样。
const element = document.getElementById('some-element-you-want-to-animate');
let start;
function step(timestamp) {
if (start === undefined)
start = timestamp;
const elapsed = timestamp - start;
//这里使用`Math.min()`确保元素刚好停在200px的位置。
element.style.transform = 'translateX(' + Math.min(0.1 * elapsed, 200) + 'px)';
if (elapsed < 2000) { // 在两秒后停止动画
window.requestAnimationFrame(step);
}
}
window.requestAnimationFrame(step);
requestIdleCallback
react fiber 中的基础API,requesetIdleCallback。这个函数将在浏览器空闲时期被调用。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间timeout,则有可能为了在超时前执行函数而打乱执行顺序。
由于requestIdleCallback利用的是帧的空闲时间, 所以有可能出现浏览器一直处于繁忙状态, 导致回调一直无法执行, 那这时候就需要在调用requestIdleCallback的时候传递第二个配置参数timeout了.
const workLoop = (deadline) => {
console.log(`此帧的剩余时间为: ${deadline.timeRemaining()}`);
// 如果此帧剩余时间大于0或者已经到了定义的超时时间(上文定义了timeout时间为1000,到达时间时必须强制执行),且当时存在任务,则直接执行这个任务
// 如果没有剩余时间,则应该放弃执行任务控制权,把执行权交还给浏览器
while (
(deadline.timeRemaining() > 0 || deadline.didTimeout) &&
taskQueue.length > 0
) {
performUnitWork();
}
// 如果还有未完成的任务,继续调用requestIdleCallback申请下一个时间片
if (taskQueue.length > 0) {
requestIdleCallback(workLoop, { timeout: 1000 });
}
};
requestIdleCallback(workLoop, { timeout: 1000 });
拓展资料:requestIdleCallback
一种数据结构
协调的过程
fiber reconciler 在执行的过程中,从根节点开始渲染和调度的过程可以分为两个阶段:render,commit
- render 阶段,生成 fiber 树,得出需要更新的节点信息,这一步是渐进的过程,是可以被中途打断的。
- commit 阶段,讲需要更新的节点一次性批量更新,这个过程是不可以被打断的。
render
遍历
在 render 阶段时,react 会采用深度优先遍历,对 fiber 树进行遍历,其中每个Virtual DOM 都可以表示为一个 fiber。每一个节点都是一个 fiber。一个 fiber 包含child、sibling、return。下图的两个 div 都是一个 fiber。
- return:指向父节点,若没有父fiber则为 null
- child:指向第一个子 fiber,若没有任何子 fiber 则为 null
- sibling:指向下一个兄弟 fiber,若没有下一个兄弟 fiber 则为 null
<App>
<div />
<input />
<List>
<div />
<div />
</List>
</App>
记住一个规则:优先儿子,再兄弟。
双缓存Fiber树
什么是 双缓存?
当我们用canvas绘制动画,每一帧绘制前都会调用ctx.clearRect清除上一帧的画面。 如果当前帧画面计算量比较大,导致清除上一帧画面到绘制当前帧画面之间有较长间隙,就会出现白屏。 为了解决这个问题,我们可以在内存中绘制当前帧动画,绘制完毕后直接用当前帧替换上一帧画面,由于省去了两帧替换间的计算时间,不会出现从白屏到出现画面的闪烁情况。 这种在内存中构建并直接替换的技术叫做双缓存。 react fiber 就是使用双缓存完成Fiber树的构建与替换。DOM 的创建于更新。那么 react fiber 是如果进行双缓存 fiber 树的?
在React中最多会同时存在两棵Fiber树。当前屏幕上显示内容对应的Fiber树称为current Fiber树,正在内存中构建的Fiber树称为workInProgress Fiber树。
current Fiber树中的Fiber节点被称为current fiber,workInProgress Fiber树中的Fiber节点被称为workInProgress fiber,他们通过alternate属性连接。
即当workInProgress Fiber树构建完成交给Renderer渲染在页面上后,应用根节点的current指针指向workInProgress Fiber树,此时workInProgress Fiber树就变为current Fiber树。每次状态更新都会产生新的workInProgress Fiber树,通过current与workInProgress的替换,完成DOM更新。
在首次执行ReactDOM.render的时候,会创建一个 fiberRootNode,他是整个应用的根。
currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;
mount
首次执行 ReactDOM.render 的时候,页面还未挂载 DOM。current tree是空的。
update
workInProgress fiber的创建可以复用current Fiber树对应的节点数据。
effect list
与react15不同的是,fiber采用链表树的形式实现的,我们刚刚了解了在render阶段,react 采用深度优先遍历对 fiber 树进行遍历。把每个有副作用的 fiber 筛选出来,构建一个只带有副作用的effect list链表。
这个链表包括 firstEffect、nextEffect、lastEffect。
FiberNode:源码
// packages/react-reconciler/src/ReactInternalTypes.js
export type Fiber = {|
// 作为静态数据结构,存储节点 dom 相关信息
tag: WorkTag, // 组件的类型,取决于 react 的元素类型
key: null | string,
elementType: any, // 元素类型
type: any, // 定义与此fiber关联的功能或类。对于组件,它指向构造函数;对于DOM元素,它指定HTML tag
stateNode: any, // 真实 dom 节点
// fiber 链表树相关, 主要
return: Fiber | null, // 父 fiber
child: Fiber | null, // 第一个子 fiber
sibling: Fiber | null, // 下一个兄弟 fiber
index: number, // 在父 fiber 下面的子 fiber 中的下标
ref:
| null
| (((handle: mixed) => void) & {_stringRef: ?string, ...})
| RefObject,
// 工作单元,用于计算 state 和 props 渲染
pendingProps: any, // 本次渲染需要使用的 props
memoizedProps: any, // 上次渲染使用的 props
updateQueue: mixed, // 用于状态更新、回调函数、DOM更新的队列
memoizedState: any, // 上次渲染后的 state 状态
dependencies: Dependencies | null, // contexts、events 等依赖
mode: TypeOfMode,
// 副作用相关
flags: Flags, // 记录更新时当前 fiber 的副作用(删除、更新、替换等)状态
subtreeFlags: Flags, // 当前子树的副作用状态
deletions: Array<Fiber> | null, // 要删除的子 fiber
nextEffect: Fiber | null, // 下一个有副作用的 fiber
firstEffect: Fiber | null, // 指向第一个有副作用的 fiber
lastEffect: Fiber | null, // 指向最后一个有副作用的 fiber
// 优先级相关
lanes: Lanes,
childLanes: Lanes,
alternate: Fiber | null, // 指向 workInProgress fiber 树中对应的节点
actualDuration?: number,
actualStartTime?: number,
selfBaseDuration?: number,
treeBaseDuration?: number,
_debugID?: number,
_debugSource?: Source | null,
_debugOwner?: Fiber | null,
_debugIsCurrentlyTiming?: boolean,
_debugNeedsRemount?: boolean,
_debugHookTypes?: Array<HookType> | null,
|};
WorkTag:源码
组件的类型,取决于 react 的元素类型
export const FunctionComponent = 0;
export const ClassComponent = 1;
export const IndeterminateComponent = 2; // Before we know whether it is function or class
export const HostRoot = 3; // Root of a host tree. Could be nested inside another node.
export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer.
export const HostComponent = 5;
export const HostText = 6;
export const Fragment = 7;
export const Mode = 8;
export const ContextConsumer = 9;
export const ContextProvider = 10;
export const ForwardRef = 11;
export const Profiler = 12;
export const SuspenseComponent = 13;
export const MemoComponent = 14;
export const SimpleMemoComponent = 15;
export const LazyComponent = 16;
export const IncompleteClassComponent = 17;
export const DehydratedFragment = 18;
export const SuspenseListComponent = 19;
export const ScopeComponent = 21;
export const OffscreenComponent = 22;
export const LegacyHiddenComponent = 23;
export const CacheComponent = 24;
export const TracingMarkerComponent = 25;
commit
commit阶段主要做的是,根据 fiber 的effect list的 effectTag 去更新视图。(新增、更新、删除)。源码在此。
React 15 vs React 16
核心对比
Stack | Fiber | |
---|---|---|
版本 | 15.x | 16.x |
数据结构 | 数组(树) | 链表 |
任务调度 | 不能暂停 | 可暂停、中断、恢复 |
抽象对比
stack
fiber
效果对比
stack 掉帧 claudiopro.github.io/react-fiber…
fiber 不掉帧 claudiopro.github.io/react-fiber…
五 总结
- 对大型复杂任务进行分片
- 对任务进行优先级划分,优先调度高优先级的任务
- 调度过程中,可以对任务进行挂起、恢复、终止等操作
react fiber 已经有一定的了解了,让我们一起来实现一个Mini-React吧。
学习react源码是极为漫长的事情,我们需要先大致了解一遍react执行的整个链路,然后再去逐个去学习每个知识点(createElement、fiber、hooks等)。
六 写在最后
- 如有写的不对的地方希望大家提醒。
- 感谢大家看到这里,这次分享的是
React Fiber
,希望可以帮助到有需要的同学。 - 如果您觉得这篇文章有帮助到您的的话不妨🍉关注+点赞+收藏+评论+转发🍉支持一下哟~~😛您的支持就是我更新的最大动力。
参考