React是用JavaScript构建快速响应的大型Web应用程序的首选方式。
ui = render(data) --> 单向数据流
可见,关键是快速响应。
我们知道JS执行与DOM渲染是互斥的,JS的执行会阻塞DOM渲染,DOM渲染同样阻塞JS执行。那么浏览器为什么要这样设计呢?
因为这样简单!JS诞生之初只是一些处理表单校验的脚本,考虑的非常简单,试想一下,如果JS执行与DOM渲染并行,那么JS多次操作DOM时,DOM的状态是怎样的?先响应哪一次操作呢?想要解决这个问题就必须要引入线程锁,在深层考虑还需要事务机制,总之就会比较复杂。
制约快速响应的因素是什么?
为什么浏览器渲染会丢帧?
浏览器根据垂直同步信号进行渲染,该信号的周期是16.67ms
,如果JS执行或渲染时间过长,就会错过一个垂直同步信号,等待下一次信号, 大于人眼能分辨的最小帧率,被识别出来丢帧
React是如何解决上述问题的?
javascript// 通过使用ReactDOM.unstable_createRoot开启Concurrent Mode
// ReactDOM.render(<App/>, rootEl);
ReactDOM.unstable_createRoot(rootEl).render(<App/>);
总结: 解决CPU瓶颈的关键是时间切片,而时间切片的关键是,将同步更新变为可中断的异步更新
网络延迟是开发者无法解决的。如何在网络延迟客观存在的前提下,减少用户对网络延迟的感知?
React给出的答案是,将人机交互研究的结果整合到真实的UI中,
为此React实现了Suspense功能及配套的hook useDeferredValue
React15架构可以分为两层:
我们知道React可以通过this.setState
、this.forceUpdate
、ReactDOM.render
等API触发更新
每当有更新发生,Reconciler会做如下工作:
class
组件的render
方法,将返回的JSX
转换为VDOM
VDOM
VDOM
Renderer
将变化的VDOM
渲染到页面上由于react支持跨平台,所以不同平台有不同的Renderer
ReactDOM
web渲染ReactNative
渲染APP原生组件ReactTest
渲染出纯JS对象,仅用于测试ReactArt
渲染到Canvas/SVG或VML(IE8)React15 架构的缺点
react15
采用递归更新,递归一旦开始就无法中断,当DOM层级很深时,交互就会卡顿对此React
团队提出了新的方案,使用可中断的异步更新代替同步的更新
React16架构如下
Scheduler
(调度器)—— 调度任务的优先级,高优任务优先进入Reconciler
Reconciler
(协调器)—— 负责找出变化的组件 (render阶段)
Renderer
(渲染器)—— 负责将变化的组件渲染到页面上 (commit阶段)
effectList
链表,将副作用反应到真实的DOM中可见,react16 增加了调度器。
解决卡顿的关键是使用可中断异步更新,那么什么时候可以中断呢?
需要一种机制,判断当前浏览器是否有空余时间,其实部分浏览器已经有这个API
requestIdleCallback
,但是它存在兼容性和不稳定性问题,还有不支持调度优先级。
所以react
决定自己实现一套调度机制,这就是Scheduler
react15中的reconciler
采用递归处理VDOM
,递归不支持中断,如果继续在15架构基础上改造支持中断更新,会出现更新时DOM渲染不完全的问题。
如何解决更新DOM不完整的问题?
为此react16的reconciler
与renderer
不再是交替工作,当Scheduler
将任务交给Reconciler
后,Reconciler
会为变化的VDOM打上标记(增/删/改)。整个Scheduler
和Reconciler
是在内存中进行,只有所有组件都完成Reconciler
工作才会统一交给Rendener
。
红框中的步骤可以随时被打断
由于红框中的工作都在内存中进行,不会更新页面上的DOM,所以即使反复中断,用户也不会看见更新不完全的DOM
Fiber
是一个计算机术语,中文译为纤程
。它与线程、协程同为程序的执行过程 我们把纤程
、协程
理解为代数效应
思想在JS中的体现。 个人理解:React Fiber
就是为了践行代数效应准备的。它是组织纤程代码执行的单元
async
/await
,但是 async
具有传染性(调用它的函数也必须是async
)try...handle
和perform
、resume
javascriptfunction getPrice(id) {
const price = perform id;
return price;
}
function getTotalPirce(id1, id2) {
const p1 = getPrice(id1);
const p2 = getPrice(id2);
return p1 + p2;
}
try {
getTotalPrice('001', '002');
} handle (productId) {
fetch(`xxx.com?id=${productId}`).then((res)=>{
resume with res.price
})
}
/*
如何理解上述代码?
- 其中perform和handle是虚构的语法
- 当代码执行到perform的时候会暂停当前函数的执行,并且被handle捕获
- handle函数体会拿到productId参数获取数据之后resume价格
- resume会回到之前的perform暂停的地方并返回price
- 这就完全把副作用分离到了getTotalPrice和getPrice之外。
这里的关键流程是perform暂停函数的执行,handle获取函数的执行权,resume交出函数的执行权
*/
// 上述代码是抽象的,实际代码如下
function usePrice(id){
useEffect(() => {
fetch(`xxx.com?id=${productId}`).then((res) => res.price);
}, [])
}
function TotalPrice({id1, id2}) {
const price1 = usePrice(id1);
const price2 = usePrice(id2);
return <TotalPrice props={...}>
}
Q: 为什么不使用Generator?
若只考虑单一任务的中断与继续情况下Generator可以很好的实现异步可中断更新 当高优先级插队,就需要引入全局变量保存当前generator的中间状态,另外在新建一个高优先级generator,引入新的复杂度
如何理解Fiber架构如何践行(模拟or实现)代数效应?
- 严格上讲react是不支持代数效应的,try...handle可以用try...catch模拟,throw errer放在微任务中即可,但是 中断与恢复无法模拟
- 但是react有fiber,执行完这个Fiber的更新之后交还给执行权给浏览器,让浏览器决定怎么调度 (中断与恢复可理解为渲染一帧完成后继续执行JS, Fiber会保存中间状态)
Susperse也是这种概念的延伸
- throw一个Promise,拿到数据通过Promise回调结果,外层组件收到回调结果触发setData, 给人的感觉是实现了代数效果
定义: React内部实现的一套状态更新机制。支持任务不同优先级,可中断与恢复,并且恢复后可以复用之前的中间状态。
功能:
React15
的Reconciler
采用递归的方式执行,被称为stack Reconciler
。React16
的Reconciler
基于Fiber
节点实现,被称为Fiber Reconciler
;Fiber
节点对应一个React emement
,保存了该组件的状态(函数需要被删除/被插入页面中/被更新...);Fiber
节点保存了本次更新中该组件改变的状态、要执行的工作(增删改)Q:ReactFiber如何更新DOM?
使用双缓存(在内存中构建并直接执行替换的技术叫做双缓存)。
在React
中最多会同时存在两课Fiber
树,屏幕显示的为current fiber树
,内存中正在构建的Fiber
树称为workInProgress Fiber树
。它们通过alternate
属性连接
fiberRootNode
整个应用的跟节点rootFiber
<APP/>
所在组件的根节点所有fiber
节点都有alternate
属性,通过任意组件发生更新,修改alternate
指针
JSX与Fiber
JSX
和Fiber
节点是同一个东西吗?React Component
、React Element
是同一个东西吗,它们和JSX有什么关系?JSX在编译时会被React.createElement
方法,这也是为什么要引入 import React from 'react';
的原因。
JSX
是一种描述当前组件内容的数据结构,他不包含组件schedule
、reconciler
、render
所需的相关信息(组件在更新中的优先级、组件的state、组件被打上的用于Renderer的标记)。Fiber
更多的是一种更新机制mount
时,Reconciler
根据JSX描述的组件内容生成组件对应的Fiber
节点update
时,Reconciler
将JSX
与Fiber
节点保存的数据对比,生成组件对应的Fiber
节点,并根据对比结果为Fiber
节点打上标记reconciler
的执行对应的就是render
阶段,该阶段主要是生成新的workInProgressFiberTree
虽然fiber reconciler
是从stack reconciler
重构而来,但都是通过遍历的方式实现可中断的异步递归
beginWork
传入当前Fiber
节点,创建子Fiber
节点completeWork
针对不同的fiber.tag
调用不同的处理逻辑mount
时:除fiberRootNode
以外,current === null
。会根据fiber.tag
不同,创建不同类型的子Fiber
节点;update
时:如果current
存在,在满足一定条件时可以复用current
节点,这样就能克隆current.child
作为workInProgress.child
,而不需要新建workInProgress.child
;总结:beginWork
就是创建Fiber
节点
effectTag
根据不同的fiber.tag生成不同的处理逻辑
update
时我们还需要考虑workInProgress.stateNode != null
?(即该Fiber节点是否存在对应的DOM节点)update
时,Fiber
节点已经存在对应DOM节点,所以不需要生成DOM节点。需要做的主要是处理props
,比如:
onClick
、onChange
等回调函数的注册style
prop
DANGEROUSLY_SET_INNER_HTML prop
children prop
Fiber
节点生成对应的DOM
节点DOM
节点插入刚生成的DOM
节点中update
逻辑中的updateHostComponent
类似的处理props
的过程总结:completeWork
就是创建Fiber
节点
effectList
Q:作为DOM操作的依据,completeWork
阶段需要找到所有effectTag
的Fiber
节点并依次执行effectTag
对应操作。难道需要在commit
阶段再遍历一次Fiber
树寻找effectTag !== null
的Fiber节点么?
A: completeWork
在上层函数completeUnitOfWork
上维护了一个单向链表
effectList
中第一个Fiber
节点保存在fiber.firstEffect
,最后一个元素保存在fiber.lastEffect
。
类似appendAllChildren
,在“归”阶段,所有有effectTag
的Fiber
节点都会被追加在effectList
中,最终形成一条以rootFiber.firstEffect
为起点的单向链表。
commitRoot(root);
rootFiber.firstEffect
上保存了一条需要执行副作用的Fiber
节点的单向链表effectList
,Fiber
节点的updateQueue
中保存了变化的props
DOM
操作在commit
阶段执行。componentDidXXX
)、hook
(比如useEffect
)需要在commit
阶段执行。commit
阶段的主要工作(即Renderer
的工作流程)分三个部分:before mutation
阶段(执行DOM操作前)
autoFocus
、blur
等操作getSnapshotBeforeUpdate
useEffect
mutation
阶段(执行DOM操作前)
ContentReset
effectTag
重置文字节点ref
effectTag
分别处理,其中effectTag
包括(Placement
| Update
| Deletion
| Hydrating
)layout
(执行DOM操作前)
useEffect
相关处理Q:为什么从Reactv16开始,componentWillXXX
钩子前增加了UNSAFE_
前缀?
在
React
更新里,每次发起更新都会创建一个Update
对象,同一组件的多个Update
,会以链表的形式保存在updateQueue
中。 假设,某个组件updateQueue
存在4个update
,数字代表优先级
javascriptbaseState = '';
A1 - B2 - C1 - D2
// 为了保证更新的连贯性,第一个被跳过的update(B)和后面的update会作为第二次渲染的baseUpdate
// 为BCD
// 首次渲染后
baseState: ''
Updates: [A1, C1]
Result state: 'AC'
// 第二次渲染,B在第一次渲染时被跳过,
// 所以在他之后的C造成的渲染结果不会体现在第二次渲染的baseState中。
// 所以baseState为A而不是上次渲染的Result state AC。
// 这也是为了保证更新的连贯性
baseState: 'A' // 为了保证一致性,C不在
Updates: [B2, C1, D2]
Result state: 'ABCD'
// Updates里出现了两次C
与
componentDidMount
、componentDidUpdate
不同的是,在浏览器完成布局与绘制之后,传给useEffect
的函数会延迟调用。这使得它适用于许多常见的副作用场景,比如设置订阅和事件处理等情况,因此不应在函数中执行阻塞浏览器更新屏幕的操作。
防止同步执行时阻塞浏览器渲染
Q:渲染DOM中时间复杂度最高的操作是?
getHostSibling
(获取兄弟DOM节点)
当在同一个父Fiber节点下依次执行多个插入操作,getHostSibling算法的复杂度为指数级。
这是由于Fiber节点不只包括HostComponent,所以Fiber树和渲染的DOM树节点并不是一一对应的。要从Fiber节点找到DOM节点很可能跨层级遍历。
javascriptfunction Item() {
return <li><li>;
}
function App() {
return (
<div>
<Item/>
</div>
)
}
ReactDOM.render(<App/>, document.getElementById('root'));
// Fiber树
child child child child
rootFiber -----> App -----> div -----> Item -----> li
// DOM树
#root ---> div ---> li
// 在div的子节点Item前加一个p
function App() {
return (
<div>
<p></p>
<Item/>
</div>
)
}
// Fiber树
child child child
rootFiber -----> App -----> div -----> p
| sibling child
| -------> Item -----> li
// DOM树
#root ---> div ---> p
|
---> li
// 此时dom中p的兄弟节点是li
// fiber中fiberP的兄弟节点是fiberItem,fiberItem的子节点才是li
Q:双缓存切换执行时间
mutation阶段结束后,layout阶段开始前
在render阶段,对于update的组件,他会将当前组件与该组件在上次更新时对应的Fiber节点比较(也就是俗称的Diff算法),将比较的结果生成新Fiber节点。 官网对diff算法的介绍
结合render和commit阶段,一个DOM节点最多有4个节点与之相关:
diff算法:对比1 4 生成2
diff操作本身也会带来性能损耗,React文档中提到,即使在最前沿的算法中,将前后两棵树完全比对的算法的复杂程度为 ,其中是树中元素的数量;如果在React中使用了该算法,那么展示1000个元素所需要执行的计算量将在十亿的量级范围。这个开销实在是太过高昂;
为了降低算法复杂度,React的diff会预设三个限制:
根据同级的节点数量将Diff分为两类:
单节点 diff
react团队提供的思路:2轮遍历
第一轮遍历 --- 找出可复用(以头部为标志)的节点, lastIndex表示最后一个可复用节点的索引
第二轮遍历 分析第一轮遍历有四种情况
如何处理更新后的节点? -- 标记节点是否移动
javascript// 之前
abcd
// 之后
acdb
===第一轮遍历开始===
a(之后)vs a(之前)
key不变,可复用
此时 a 对应的oldFiber(之前的a)在之前的数组(abcd)中索引为0
所以 lastPlacedIndex = 0;
继续第一轮遍历...
c(之后)vs b(之前)
key改变,不能复用,跳出第一轮遍历
此时 lastPlacedIndex === 0;
===第一轮遍历结束===
===第二轮遍历开始===
newChildren === cdb,没用完,不需要执行删除旧节点
oldFiber === bcd,没用完,不需要执行插入新节点
将剩余oldFiber(bcd)保存为map
// 当前oldFiber:bcd
// 当前newChildren:cdb
继续遍历剩余newChildren
key === c 在 oldFiber中存在
const oldIndex = c(之前).index;
此时 oldIndex === 2; // 之前节点为 abcd,所以c.index === 2
比较 oldIndex 与 lastPlacedIndex;
如果 oldIndex >= lastPlacedIndex 代表该可复用节点不需要移动
并将 lastPlacedIndex = oldIndex;
如果 oldIndex < lastplacedIndex 该可复用节点之前插入的位置索引小于这次更新需要插入的位置索引,代表该节点需要向右移动
在例子中,oldIndex 2 > lastPlacedIndex 0,
则 lastPlacedIndex = 2;
c节点位置不变
继续遍历剩余newChildren
// 当前oldFiber:bd
// 当前newChildren:db
key === d 在 oldFiber中存在
const oldIndex = d(之前).index;
oldIndex 3 > lastPlacedIndex 2 // 之前节点为 abcd,所以d.index === 3
则 lastPlacedIndex = 3;
d节点位置不变
继续遍历剩余newChildren
// 当前oldFiber:b
// 当前newChildren:b
key === b 在 oldFiber中存在
const oldIndex = b(之前).index;
oldIndex 1 < lastPlacedIndex 3 // 之前节点为 abcd,所以b.index === 1
则 b节点需要向右移动
===第二轮遍历结束===
最终acd 3个节点都没有移动,b节点被标记为移动
我们改一下案例,再分析一波
javascript// 之前
abcd
// 之后
dabc
===第一轮遍历开始===
d(之后)vs a(之前)
key改变,不能复用,跳出遍历
===第一轮遍历结束===
===第二轮遍历开始===
newChildren === dabc,没用完,不需要执行删除旧节点
oldFiber === abcd,没用完,不需要执行插入新节点
将剩余oldFiber(abcd)保存为map
继续遍历剩余newChildren
// 当前oldFiber:abcd
// 当前newChildren dabc
key === d 在 oldFiber中存在
const oldIndex = d(之前).index;
此时 oldIndex === 3; // 之前节点为 abcd,所以d.index === 3
比较 oldIndex 与 lastPlacedIndex;
oldIndex 3 > lastPlacedIndex 0
则 lastPlacedIndex = 3;
d节点位置不变
继续遍历剩余newChildren
// 当前oldFiber:abc
// 当前newChildren abc
key === a 在 oldFiber中存在
const oldIndex = a(之前).index; // 之前节点为 abcd,所以a.index === 0
此时 oldIndex === 0;
比较 oldIndex 与 lastPlacedIndex;
oldIndex 0 < lastPlacedIndex 3
则 a节点需要向右移动
继续遍历剩余newChildren
// 当前oldFiber:bc
// 当前newChildren bc
key === b 在 oldFiber中存在
const oldIndex = b(之前).index; // 之前节点为 abcd,所以b.index === 1
此时 oldIndex === 1;
比较 oldIndex 与 lastPlacedIndex;
oldIndex 1 < lastPlacedIndex 3
则 b节点需要向右移动
继续遍历剩余newChildren
// 当前oldFiber:c
// 当前newChildren c
key === c 在 oldFiber中存在
const oldIndex = c(之前).index; // 之前节点为 abcd,所以c.index === 2
此时 oldIndex === 2;
比较 oldIndex 与 lastPlacedIndex;
oldIndex 2 < lastPlacedIndex 3
则 c节点需要向右移动
===第二轮遍历结束===
总结: 设置参照物,尽量减少节点从后面移动到前面的操作
typescript/**
* react diff
* 1. abcd -> acdb
* 2. abcd -> dabc
*/
export function diffChildren(newStr: string, oldStr: string): string[] {
// 第一轮循环 找出头部无变更的元素
let lastPlaceIndex = 0;
const result: string[] = [];
for (let i = 0; i < newStr.length; i++) {
if (newStr[i] && oldStr[i] && newStr[i] === oldStr[i]) {
lastPlaceIndex = i+1;
} else {
break;
}
}
if (
lastPlaceIndex === newStr.length &&
lastPlaceIndex === oldStr.length
) {
result.push("第一轮遍历结束,全部节点都可以复用");
return result;
}
if (lastPlaceIndex === oldStr.length) {
result.push("第一轮遍历结束, 新children有剩余");
result.push(`将剩余的新节点${newStr.slice(lastPlaceIndex)}插入尾部`);
return result;
}
if (lastPlaceIndex === newStr.length) {
result.push("第一轮遍历结束, 旧children有剩余");
result.push(`将剩余的新节点${oldStr.slice(lastPlaceIndex)}删除`);
return result;
}
result.push(`第一轮遍历结束新旧children都有剩余的情况`);
// 第二轮循环
const restOfOld = [...oldStr.slice(lastPlaceIndex)];
const [map, deletionsMap] = restOfOld.reduce(([map1, map2], item, index) => {
map1[item] = index + lastPlaceIndex;
const map2Index = index + lastPlaceIndex + '';
map2[map2Index] = true;
return [map1, map2];
}, [{}, {}] as [{[key: string]: number}, {[key: string]: boolean}]);
const restOfNew = [...newStr.slice(lastPlaceIndex)];
for (let j = 0; j < restOfNew.length; j++) {
const oldIndex = map[restOfNew[j]];
if(oldIndex !== undefined) {
deletionsMap[oldIndex] = false;
}
if(oldIndex === undefined) {
result.push(`将新元素${restOfNew[j]}插入到尾部`);
lastPlaceIndex = Math.max(oldStr.length, lastPlaceIndex) + 1;
} else if (oldIndex > lastPlaceIndex) {
lastPlaceIndex = oldIndex;
} else if (oldIndex < lastPlaceIndex) {
result.push(`将${restOfNew[j]}移动到尾部`);
}
}
Object.keys(deletionsMap)
.filter(index => deletionsMap[index])
.forEach(index => {
console.log(`删除${oldStr[index]}`)
result.push(`删除${oldStr[index]}`);
})
return result;
}
// diff.test.ts
import { diffChildren } from "./diff";
import { describe, it, expect } from "vitest";
describe("diffChildren", () => {
it("第一轮遍历结束,全部节点都可以复用", async () => {
const result = diffChildren('abcd', 'abcd');
console.log(result);
expect(result).toEqual(['第一轮遍历结束,全部节点都可以复用'])
});
it("第一轮遍历结束,新children有剩余", () => {
const result = diffChildren('abcd', 'abc');
console.log(result);
expect(result).toEqual([
'第一轮遍历结束, 新children有剩余',
'将剩余的新节点d插入尾部'
])
})
it("第一轮遍历结束,旧children有剩余", () => {
const result = diffChildren('abc', 'abcd');
console.log(result);
expect(result).toEqual([
'第一轮遍历结束, 旧children有剩余',
'将剩余的新节点d删除'
])
})
it("元素移动:abcd --> acdb", () => {
const result = diffChildren('acdb', 'abcd');
console.log(result);
expect(result).toEqual([
'第一轮遍历结束新旧children都有剩余的情况',
'将b移动到尾部'
])
})
it("元素移动:abcd --> dabc", () => {
const result = diffChildren('dabc', 'abcd');
console.log(result);
expect(result).toEqual([
'第一轮遍历结束新旧children都有剩余的情况',
"将a移动到尾部",
'将b移动到尾部',
'将c移动到尾部',
])
});
it("有元素移动和新增元素:abcd --> dabce", () => {
const result = diffChildren('dabce', 'abcd');
console.log(result);
expect(result).toEqual([
'第一轮遍历结束新旧children都有剩余的情况',
"将a移动到尾部",
'将b移动到尾部',
'将c移动到尾部',
"将新元素e插入到尾部",
]);
});
it("有元素移动和新增元素:abcd --> daebc", () => {
const result = diffChildren('daebc', 'abcd');
console.log(result);
expect(result).toEqual([
'第一轮遍历结束新旧children都有剩余的情况',
"将a移动到尾部",
"将新元素e插入到尾部",
'将b移动到尾部',
'将c移动到尾部',
]);
})
it("有元素移动和新增元素:abcd --> dac", () => {
const result = diffChildren('dac', 'abcd');
console.log(result);
expect(result).toEqual([
'第一轮遍历结束新旧children都有剩余的情况',
"将a移动到尾部",
'将c移动到尾部',
'删除b',
]);
});
});
在react里,触发状态更新的操作包括:
Q:如何在调用场景不同的情况下,接入同一套状态管理机制?
在每次状态更新,都会创建保存一个更新状态相关的对象,称为Update,在render的beginwork中会根据Update得到新的state
Update思路 可类比代码版本控制类比
Update与Fiber联系:
updateQueue
javascriptconst queue: UpdateQueue<State> = {
baseState: fiber.memoizedState,
firstBaseUpdate: null,
lastBaseUpdate: null,
shared: {
pending: null,
},
effects: null,
};
Q:render阶段可能会被中断,如何保证updateQueue中的Update不会丢失?
在render阶段,shared.pending的环被剪开并连接在updateQueue.lastBaseUpdate后面。
实际上shared.pending会被同时连接在workInProgress updateQueue.lastBaseUpdate与current updateQueue.lastBaseUpdate后面。
当render阶段被中断后重新开始时,会基于current updateQueue克隆出workInProgress updateQueue。由于current updateQueue.lastBaseUpdate已经保存了上一次的Update,所以不会丢失
当commit阶段完成渲染,由于workInProgress updateQueue.lastBaseUpdate中保存了上一次的Update,所以 workInProgress Fiber树变成current Fiber树后也不会造成Update丢失
架构运行策略--lane
基于当前的架构,当一次更新在运行过程中被中断,过段时间再继续运行,这就是“异步可中断的更新”;
当一次更新在运行过程中被中断,转而重新开始一次新的更新,我们可以说:后一次更新打断了前一次更新;
这就是优先级的概念:后一次更新的优先级更高,就会打断正在进行的前一次更新。
多个优先级之间如何互相打断?优先级能否升降?本次更新应该赋予什么优先级?
这就需要一个模型控制不同优先级之间的关系与行为,也就是lane
时间切片原理 时间切片的本质是模拟实现requestIdleCallback(opens new window) 除去“浏览器重排/重绘”,下图是浏览器一帧中可以用于执行JS的时机。 一个task(宏任务) -- 队列中全部job(微任务) -- requestAnimationFrame -- 浏览器重排/重绘 -- requestIdleCallback
Scheduler的时间切片功能是通过task(宏任务)实现的。
所以Scheduler将需要被执行的回调函数作为MessageChannel的回调执行。如果当前宿主环境不支持MessageChannel,则使用setTimeout 在React的render阶段,开启Concurrent Mode时,每次遍历前,都会通过Scheduler提供的shouldYield方法判断是否需要中断遍历,使浏览器有时间渲染:
https://pomb.us/build-your-own-react/ 笔者建议 多写多练
为什么条件语句里面不能写hooks?
legacy模式 与 concurrent模式的差异
legacy模式 使用ReactDOM.render()创建的应用 并发模式concurrent 使用ReactDOM.createRoot().render()创建的应用
本文作者:郭敬文
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!