开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 7 天,点击查看活动详情
一 前置知识
我们知道,在浏览器中,页面是一帧一帧绘制出来的,渲染的帧率与设备的刷新率保持一致。大多数设备的屏幕刷新率为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,代码执行时间越长卡顿越明显。传统的方法存在不能中断和执行栈太深的问题。
React 15 vs React 16
核心对比
| Stack | Fiber | |
|---|---|---|
| 版本 | 15.x | 16.x |
| 数据结构 | 数组(树) | 链表 |
| 任务调度 | 不能暂停 | 可暂停、中断、恢复 |
抽象对比
stack
fiber
效果对比
谢尔宾斯基三角形:百度百科
- 在每一帧渲染之前(通过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 实现原理
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 可以理解为一个执行的单元,如下图所示,浏览器在每一帧都有一个空闲时间,react正是利用这个空闲时间去执行分片后优先级较高的任务。一旦空闲时间结束之后把执行权再交给浏览器。
React 和浏览器配合调度关系图
每一个fiber节点都是一个 fiber。一个 fiber 包含child、sibling、return。
- return:指向父节点,若没有父fiber则为 null
- child:指向第一个子 fiber,若没有任何子 fiber 则为 null
- sibling:指向下一个兄弟 fiber,若没有下一个兄弟 fiber 则为 null
<App>
<div />
<input />
<List>
<div />
<div />
</List>
</App>
记住一个规则:优先儿子,再兄弟。
一种数据结构
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;
双缓存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。
Fiber 调和的过程
fiber reconciler 在执行的过程中,从根节点开始渲染和调度的过程可以分为两个阶段:render,commit
- render 阶段,生成 fiber 树,得出需要更新的节点信息,这一步是渐进的过程,是可以被中途打断的。
- commit 阶段,讲需要更新的节点一次性批量更新,这个过程是不可以被打断的。
render
在 render 阶段时,这个过程是实际上在内存中构建的,也称为(workInProgress树)。react 会采用深度优先遍历,对 fiber 树进行向下遍历再向上回溯到root的过程(下图就是向下遍历以及向上回溯的过程),其中每个Virtual DOM 都可以表示为一个 fiber。下图的 div和input等都是一个 fiber。
事实上,react在构建这个workInProgress树的过程中,会经历两个阶段:beginWork和completeWork。
beginWork:组件的状态计算、diff的操作以及render函数的执行,发生在beginWork阶段。
p3-juejin.byteimg.com/tos-cn-i-k3…
在 beginWork阶段,更新节点并返回子树,进入beginWork后,首先判断节点及其子树是否有更新,若有更新,则会在计算新状态和diff之后生成新的Fiber,然后在新的fiber上标记flags(effectTag),最后return它的子节点,以便继续针对子节点进行beginWork。若它没有子节点,则返回null,这样说明这个节点是末端节点,可以进行向上回溯,进入completeWork阶段。
看源码部分
beginWork阶段的整体工作是去更新节点,并返回子树,但真正的beginWork函数只是节点更新的入口,不会直接进行更新操作。作为入口,它的职责很明显,拦截无需更新的节点。同时,它还会将context信息入到栈中(beginWork入栈,completeWork出栈)
两个地方需要注意:
如何区分初始化还是更新?
判断 current 是否存在,我们知道,调度的过程是有两棵树的存在,展示在屏幕上的current Tree和正在后台基于current树构建的 workInProgress Tree。
如果是首次渲染,对具体的workInProgress节点来说,它是没有current节点的,如果是在更新过程,由于current节点已经在首次渲染时产生了,所以workInProgress节点有对应的current节点存在。
最终会根据节点是首次渲染还是更新来决定是创建fiber还是diff fiber。只不过更新时,如果节点的优先级不够会直接复用已有节点,即走跳出(bailout)的逻辑,而不是去走下面的更新逻辑。
如何复用节点?
通过 bailoutOnAlreadyFinishedWork 函数返回。
beginWork它的返回值有两种情况:
- 返回当前节点的子节点,然后会以该子节点作为下一个工作单元继续beginWork,不断往下生成fiber节点,构建workInProgress树。
- 返回null,当前fiber子树的遍历就此终止,从当前fiber节点开始往回进行completeWork。
复用的节点返回值仍然会有两种情况
- 返回当前节点的子节点,前置条件是当前节点的子节点有更新,此时当前节点未经处理,是可以直接复用的,复用的过程就是复制一份current节点的子节点,并把它return出去。
- 返回null,前提是当前子节点没有更新,当前子树的遍历过程就此终止。开始completeWork。
总结:beginWork的主要功能就是处理当前遍历到的fiber,经过一番处理之后返回它的子fiber,一个一个地往外吐出fiber节点,那么workInProgress树也就会被一点一点地构建出来。
completeWork:effect链表的收集、被跳过的优先级的收集,发生在completeWork阶段。
completeWork 发生在 beginWork之后,详细点说是发生在 Diff 之后。这时候workInProgress节点是经过 diff 算法调和过的。这时候fiber基本形态已经确定了。
- fiber 形态变了,但是原生DOM(HostComponent和HostTest)对应的DOM还没有更新变化。
- 经过diff生成的workInProgress节点有了的flag(effectTag)。
workInProgress节点的completeWork阶段主要做的事情再来回顾一下:
- 真实DOM节点的创建以及挂载
- DOM属性的处理
- effectList的收集
- 错误处理
commit
commit阶段主要做的是,根据 fiber 的effect list的 effectTag 去更新视图。(新增、更新、删除)。源码在此。
render 阶段结束之后,意味着内存中构建的 workInProgress 树所有的更新工作都已经完成,包括树中的 fiber 节点的更新、diff、effectTag的标记,effectList的收集。
和current树相比,它们的结构上固然存在区别,变化的fiber节点也存在于workInProgress树,但要将这些节点应用到DOM上却不会循环整棵树,而是通过循环effectList这个链表来实现,这样保证了只针对有变化的节点做工作。
所以循环effectList链表去将有更新的fiber节点应用到页面上是commit阶段的主要工作。
入口函数是 **commitRoot****, **会给调度器说,要以立即执行的优先级去调度 commit 阶段的工作。
commitRoot 又分为三个阶段
- before mutation:读取组件变更前的状态,针对类组件,调用getSnapshotBeforeUpdate,让我们可以在DOM变更前获取组件实例的信息;针对函数组件,异步调度useEffect。
- mutation:针对HostComponent,进行相应的DOM操作;针对类组件,调用componentWillUnmount;针对函数组件,执行useLayoutEffect的销毁函数。
- layout:在DOM操作完成后,读取组件的状态,针对类组件,调用生命周期componentDidMount和componentDidUpdate,调用setState的回调;针对函数组件填充useEffect 的 effect执行数组,并调度useEffect
五 scheduler
如果「组件 Render 过程耗时」或「参与调和阶段的虚拟 DOM 节点很多」时,那么一次性完成所有组件的调和阶段就会花费较长时间。<br />为了避免长时间执行调和阶段而引起页面卡顿,[React](https://so.csdn.net/so/search?q=React&spm=1001.2101.3001.7020) 团队提出了 Fiber 架构和 Scheduler 任务调度。Fiber 架构的目的是「能独立执行每个虚拟 DOM 的调和阶段」,而不是每次执行整个虚拟 DOM 树的调和阶段。<br />scheduler 的主要功能是时间分片,每隔一段时间就把主线程还给浏览器,避免长时间占用主线程。scheduler是一个独立的包,抛开react来讲,它可以作为第三方库使用,只是react使用了scheduler进行任务调度。<br />你只需要将 任务和 任务的优先级给到scheduler,他就可以帮你管理任务了,可以安排任务的执行顺序。**commitRoot 的例子**。
scheduler的两个行为
- 对于多个任务,会执行优先级高的那个。
- 对于单个任务,会有节制的执行。线程只有一个,不会一直占用线程执行任务,而是执行一会,中断一下。如此往复。
从scheduler的行为可以分析,多个任务直接是有一个任务优先级的概念。对与单个任务,是有时间片段(任务中断机制)。单个任务在一帧当中最大的执行时间,一旦执行时间超过时间片,则会被打断,有节制的执行任务。这样可以保证页面不会因为任务连续执行的时间过长而产生卡顿。
可以分析,scheduler的两大概念,任务队列管理和时间片下的任务中断恢复
任务队列管理
在Scheduler内部,把任务分成了两种:未过期的和已过期的,分别用两个队列存储,前者存到timerQueue中,后者存到taskQueue中。
如何区分任务是否过期?
用任务的开始时间(startTime)和当前时间(currentTime)作比较。
- 开始时间大于当前时间,说明未过期,放到timerQueue;
- 开始时间小于等于当前时间,说明已过期,放到taskQueue
任务入队两个队列之后要干嘛?
- 如果放到了taskQueue,那么立即调度一个函数去循环taskQueue,挨个执行里面的任务。
- 如果放到了timerQueue,那么说明它里面的任务都不会立即执行,那就等到了timerQueue里面排在第一个任务的开始时间,看这个任务是否过期,如果是,则把任务从timerQueue中拿出来放入taskQueue,调度一个函数去循环它,执行掉里面的任务;否则过一会继续检查这第一个任务是否过期。
任务队列管理相对于单个任务的执行,是宏观层面的概念,它利用任务的优先级去管理任务队列中的任务顺序,始终让最紧急的任务被优先处理。
时间片下的任务中断恢复
单个任务的中断以及恢复对应了Scheduler的单个任务执行控制这一行为。在循环taskQueue执行每一个任务时,如果某个任务执行时间过长,达到了时间片限制的时间,那么该任务必须中断,以便于让位给更重要的事情(如浏览器绘制)
这个任务中断实际上涉及到了两个角色:调度者、任务执行者(这两个角色实际上都在scheduler阶段)
调度的入口:scheduleCallback
调度者:requestHostCallback中进行实现,浏览器情况下是postMessage,非浏览器下是setTimeout
执行者:performWorkUntilDeadline,它的作用是按照时间片的限制去中断任务,并通知调度者再次调度一个新的执行者去继续任务。performWorkUntilDeadline
内部实际是调用了scheduledHostCallback,最初的时候是被赋值为了flushWork,所以,flushWork是真正的执行者。
flushWork 返回的是workLoop,工作循环进行事件的执行。
故,任务的中断和恢复是在 workLoop中进行的。看下workLoop干了什么事情。
优先级管理
react 中的优先级
- 事件优先级:按照用户事件的交互紧急程度,划分的优先级,用户输入一个字符。。。
离散事件(DiscreteEvent):click、keydown、focusin等,这些事件的触发不是连续的,优先级为0。
用户阻塞事件(UserBlockingEvent):drag、scroll、mouseover等,特点是连续触发,阻塞渲染,优先级为1。
连续事件(ContinuousEvent):canplay、error、audio标签的timeupdate和canplay,优先级最高,为2。
-
更新优先级:事件导致React产生的更新对象(update)的优先级(update.lane)
-
任务优先级:产生更新对象之后,React去执行一个更新任务,这个任务所持有的优先级
-
调度优先级:Scheduler依据React更新任务生成一个调度任务,这个调度任务所持有的优先级
前三者属于React的优先级机制,第四个属于Scheduler的优先级机制,Scheduler内部有自己的优先级机制,虽然与React有所区别,但等级的划分基本一致。下面我们从事件优先级开始说起。
三者的关系:
LanePriority和SchedulerPriority从命名上看, 它们代表的是优先级ReactPriorityLevel从命名上看, 它代表的是等级而不是优先级, 它用于衡量LanePriority和SchedulerPriority的等级.
在[SchedulerWithReactIntegration中](github.com/facebook/re…), 可以互转SchedulerPriorit和 ReactPriorityLevel
// 把 SchedulerPriority 转换成 ReactPriorityLevel
export function getCurrentPriorityLevel(): ReactPriorityLevel {
switch (Scheduler_getCurrentPriorityLevel()) {
case Scheduler_ImmediatePriority:
return ImmediatePriority;
case Scheduler_UserBlockingPriority:
return UserBlockingPriority;
case Scheduler_NormalPriority:
return NormalPriority;
case Scheduler_LowPriority:
return LowPriority;
case Scheduler_IdlePriority:
return IdlePriority;
default:
invariant(false, 'Unknown priority level.');
}
}
// 把 ReactPriorityLevel 转换成 SchedulerPriority
function reactPriorityToSchedulerPriority(reactPriorityLevel) {
switch (reactPriorityLevel) {
case ImmediatePriority:
return Scheduler_ImmediatePriority;
case UserBlockingPriority:
return Scheduler_UserBlockingPriority;
case NormalPriority:
return Scheduler_NormalPriority;
case LowPriority:
return Scheduler_LowPriority;
case IdlePriority:
return Scheduler_IdlePriority;
default:
invariant(false, 'Unknown priority level.');
}
}
在[ReactFiberLane中](github.com/facebook/re…), 可以互转LanePriority和 ReactPriorityLevel
export function schedulerPriorityToLanePriority(
schedulerPriorityLevel: ReactPriorityLevel,
): LanePriority {
switch (schedulerPriorityLevel) {
case ImmediateSchedulerPriority:
return SyncLanePriority;
// ... 省略部分代码
default:
return NoLanePriority;
}
}
export function lanePriorityToSchedulerPriority(
lanePriority: LanePriority,
): ReactPriorityLevel {
switch (lanePriority) {
case SyncLanePriority:
case SyncBatchedLanePriority:
return ImmediateSchedulerPriority;
// ... 省略部分代码
default:
invariant(
false,
'Invalid update priority: %s. This is a bug in React.',
lanePriority,
);
}
}
reconciler从输入到输出一共经历了 4 个阶段, 在每个阶段中都会涉及到与优先级相关的处理. 正是通过优先级的灵活运用, React实现了可中断渲染时间切片(time slicing),异步渲染(suspense)
react 团队自己实现的 requestIdleCallback
react 团队为什么不使用 requestIdleCallback,而是重写。
1 先看看 can i use,requestIdleCallback的兼容性其实很差。
2 requestIdleCallback的定位是处理不重要、不紧急的任务。和React可能不太符(React渲染内容,并非是不紧急不重要)
3 requestIdleCallback的FPS只有20ms,正常情况下渲染一帧时长控制在16.67ms,该时间是高于页面流畅的需求的。
react 如何重写 requestIdleCallback 的
老版本:scheduler 直接使用 MessageChannel 实现。
新版本:优先使用 setImmediate,如果没有再去使用 MessageChannel。 传送门
使用 MessageChannel 创建宏任务进行处理。
为什么不使用 setTimeout?
var count = 0
var startVal = +new Date()
console.log("start time", 0, 0)
function func() {
setTimeout(() => {
console.log("exec time", ++count, +new Date() - startVal)
if (count === 50) {
return
}
func()
}, 0)
}
func()
点击查看,setTimeout会有一个5ms的浪费。
六 总结
- 对大型复杂任务进行分片
- 对任务进行优先级划分,优先调度高优先级的任务
- 调度过程中,可以对任务进行挂起、恢复、终止等操作
react fiber 已经有一定的了解了,让我们一起来实现一个Mini-React吧。
学习react源码是极为漫长的事情,我们需要先大致了解一遍react执行的整个链路,然后再去逐个去学习每个知识点(createElement、fiber、hooks等)。