1. React笔记
来源:
React 课程:https://ke.segmentfault.com/course/1650000023864436/section/1500000023864578
Mini React:https://github.com/lizuncong/mini-react
https://segmentfault.com/a/1190000039227345
React 架构体系三大模块
- 调度
- 协调
- 渲染
React 设计理念
问题:React 运行机制 / 架构
设计理念
浏览器的性能瓶颈在 CPU 和 IO。
React 解决 CPU / IO 的瓶颈:异步可中断的更新。
浏览器的刷新率通常为 60Hz/s,也就是 16.6ms 刷新一次页面。
- 16.6ms 中,浏览器需要完成:JS脚本执行、样式布局、样式绘制。
当渲染的处理时间超过 16.6ms,页面就会卡顿。通常使用节流(间隔触发)、防抖(最后一个触发),都是通过限制更新频率来提升性能。
React 把同步更新,变为异步可中断进行更新。React 通过 fiber 把渲染任务切分为多个小段,每个小段执行完毕后会把主动权交还浏览器,浏览器则可根据当前任务的优先级,把剩余时间 分配给更重要的任务,以确保尽可能的响应及时。
- 16.6ms 中,当浏览器执行完重要任务后,会把剩余时间交给 React 取执行尚未处理完的任务,如果剩余时间不够,就会把剩余任务在下一个 16.6ms 周期中执行。
这样,浏览器就有充足的时间进行样式布局和页面绘制,而 React 也可以在分配给自己的时间内执行完任务。
异步可中断
React 15+ 实现了异步可中断的架构。
- Scheduler 调度器:调度更新,给更新任务安排优先级。
- Reconcoler 协调器:决定需要更新什么组件,当更高优任务来到时,可中断当前任务。
- Renderer 渲染器:将组件更新到视图中,渲染过程是同步且不可中断的,需要一气呵成。
在 render 函数的执行周期中,有调度(事件触发)、协调(render)、渲染(commit)三个阶段:
1 调度阶段
当 React 需要更新时,更新任务首先被 Scheduler 调度器 处理。
处理的工作有:
- 创建项目根 Fiber 节点。首次渲染页面时,会创建唯一的根节点 FiberRoot Node。
- 创建应用根 Fiber 节点。每一个
ReactDOM.render()
都会创建一个 rootFiber。 - 初始化事件。Scheduler 会把任 务按照优先级排序,让更高优的任务首先进入 Reconciler 协调器进行下一步处理。
2 协调阶段
协调阶段是 Reconcoler 协调器在参与,也被称为 render 阶段,如下图 renderRootSync 都为协调阶段。
Scheduler 调度器会通过 diff 算法,处理传入的更新任务。diff 完毕后,会由 fiber 构成一颗新的 fiber 树,然后提交给 Renderer 渲染器进一步处理。
- 关于 virtual DOM 的概念,有两种说法:
- 广义的说,由 fiber 组成的树为 fiber 树,也是一个 virtual DOM 树;
- 狭义的说,通过 React element 组成的树,是 virtual DOM 树,在协调阶段,会把 virtual DOM 转化为 fiber tree。
- 关于组件中的 render 函数的作用:
- 在调和阶段的整体任务,是通过 fiber 节点构建一棵 workInProgress fiber tree。这个构建过程是从根节点向子节点深度优先遍历完成的。
- 遇到,某个组件,会执行其调和阶段的生命周期函数,然后调用组件的 render。 render 会根据本次更新中的 state 变化,通过
React.createElement
生成新的 React Element 对象,在新的对象上保存了新的 state + props 状态。 - 该组件的 fiber 节点会根据对应的 React element 完成构建。同时对副作用变化打上标记。
该阶段的特点:
- 可暂停。而在 Reconciler 在进行 diff 算法时,如果调度器传来一个更高优的任务,那么当前处理的更新任务会被暂停,让调度器放入任务队列中,优先处理传入的更高优任务。
- 用户透明。由于调度器和协调器是在内存中工作,即使 diff 中断,用户也无法感知到页面渲染被中断 / 卡顿。
- 构建 work In Progres 树。diff 算法会构建一颗虚拟 dom 树,视图上真实存在的节点,都有一个对应的节点在虚拟 dom 上。需要更新的节点会被打上标记 Update。被打了标记的虚拟 dom 会交给渲染器。
该阶段的工作:
调和 work IN Progress Tree,也可以理解为构建 work IN Progress Tree。
构建过程是一个递归过程,从 rootFiber
开始向下深度优先遍历的,具体可以分为 n 个:递阶段 + 归阶段。
(1)递阶段
递阶段,是向下调和的过程。
调用 beginWork 方法,从 rootFiber
开始向下深度优先遍历,为遍历到的每个 fiber 节点。
该方法会根据传入的 Fiber节点
创建 子Fiber节点
,并将这两个 Fiber节点
连接起来(fiber.child 指针)。当遍历到叶子节点(即没有子组件的组件)时就会进入 “归” 阶段。
如果不是初次渲染,而是更新页面,此时已经存在一个构建好的 Current Tree,beginWork 方法会由 fiberRoot 根节点,按照 child 指针逐层向下 调和。
beginWork 具体的工作如下:
- 对于组件,执行部分生命周期,执行 render ,得到最新的 children。
- 向下遍历调和 children ,复用 oldFiber ( diff 算法)。
- 打不同的副作用标签 effectTag ,比如类组件的生命周期,或者元素的增加,删除,更新。这些被标记的节点,会在归阶段收集起来,形成单项链表 effectList。
// 生命周期钩子会在协调阶段被调用:
constructor
componentWillMount 废弃
componentWillReceiveProps 废弃
static getDerivedStateFromProps
shouldComponentUpdate
componentWillUpdate 废弃
render
// 常见的 effect tag
export const Placement = /* */ 0b0000000000010; // 插入节点
export const Update = /* */ 0b0000000000100; // 更新fiber
export const Deletion = /* */ 0b0000000001000; // 删除fiebr
export const Snapshot = /* */ 0b0000100000000; // 快照
export const Passive = /* */ 0b0001000000000; // useEffect的副作用
export const Callback = /* */ 0b0000000100000; // setState的 callback
export const Ref = /* */ 0b0000010000000; // ref
(2)归阶段
归阶段,是向上并归的过程。
调用 completeWork 方法,来处理当前的 Fiber 节点。
当某个 Fiber节点
执 行完 completeWork
,如果其存在 兄弟Fiber节点
(fiber.sibling 指针 ),会进入其 兄弟Fiber
的 “递”阶段。如果不存在 兄弟Fiber
,会进入 父级Fiber
的“归”阶段(fiber.return 指针)。
在此期间会形成 effectList 单项副作用链表。如果是初次渲染的初始化流程,会创建 DOM ,对于 DOM 元素进行事件收集,处理 style,className 等工作。
- completeUnitOfWork 会将 effectTag 的 Fiber 节点会被保存在一条被称为 effectList 的单向链表中。在 之后的 commit 阶段,将不再需要遍历每一个 fiber ,只需要执行更新 effectList 就可以了。
- completeWork 阶段 对于组件,会处理 context ; 对于元素标签初始化,会创建真实 DOM ,将子孙 DOM 节点插入刚生成的 DOM 节点中; 会触发 diffProperties 处理 props ,比如事件收集,style,className 处理,在15章讲到过。
(3)进入循环
“递”和“归”阶段会交错执行,直到 “归” 到 rootFiber
根结点。至此,render阶段
的工作就结束了。
- 构成(调和)了由 fiber 节点构成的 work IN Progress Tree 树。
- 构成了接下来 commit 阶段需要执行更新的 effectList 副作用链表。
3 渲染阶段
渲染器工作的阶段,是 Renderer 渲染器在参与,被称为 commit 阶段,下图中,commitRoot 就是 commit 阶段。
本次更新由哪些组件需要更新视图,会让渲染器来分别执行这些视图更新操作。
每一个子阶段都是一个 while 循环,从头开始 遍历副作用链表。
- 视图更新操作:对 DOM 节点的增、删、查、改。
- 渲染器把被打了标记的虚拟 dom 对应的真实 dom 节点执行更新 dom 的操作。
- Mutation 突变,对于浏览器来说,就是 DOM 操作。
该阶段触发的生命周期函数有:
// before mutation 阶段
getSnapshotBeforeUpdate
// mutation 阶段
componentWillUnmount
// layout 阶段
componentDidMount
componentDidUpdate
(1)mutation 前阶段
通常被称之为 before mutation 阶段,调用函数 commitBeforeMutationEffects
。
- class 组件。执行
getSnapshotBeforeUpdate
生命周期函数。因为 Before mutation 还没修改真实的 DOM 所以此时类组件可以获得更新 DOM 前的快照。 - 函数组件。创建微任务,给
useEffect
的回调函数设定 normal Scheduler Priority,然后等待 commit 完成后,再 异步执行。防止同步执行时阻塞浏览器做视图渲染(如果对 dom 操作,则又要重协调)。
(2)mutaiton 阶段
通常被称之为 mutation 阶段,调用函数 commitMutationEffects
。
遍历包含 EffectTag 的 fiber 节点,所组成的 effectList 链表。处理每一个 fiber 节点的副作用,这些副作用有:
- DOM 相关操作:增删改。Placement 插入,Update 更新 DOM 属性,Deletion 删除。
- class 组件。调用 class 组件 componentWillUnmount 生命周期函数。
- 函数组件。执行 useLayoutEffect 的销毁函数,useLayoutEffect 是 同步执行 的。
- Ref 属性。解绑/更新 Ref 属性。
(2.1)更改 current 指针
在进入 layout 节点前,会执行:
root.current = finishedWork;
这就是 React 架构的双缓存机制中,Work In Progress Fiber 树完成渲染,此时 fiberRoot.current
指针会从之前的 current Fiber 树,指向现在的 Work In Progress Fiber 树。此时,Work In Progress Fiber 树,就变成了新的 Current Fiber 树。
在该实际触发,是因为在 layout 阶段会执行 componentDidMount
和 componentDidUpdate
这两个生命周期函数,此时,Current Fiber 树已经指向了本次完成的 Work In Progress Fiber 树,这两个生命周期会对新的 Current Fiber 树进行操作。
(3)mutation 后阶段
通常被称之为 layout 阶段,调用函数 commitLayoutEffects
。
- 函数组件。
- 执行 useLayoutEffect 的回调函数,是 同步执行 的。
- 绑定 useEffect 的销毁 + 回调函数,以便在 commit 阶段完成后,异步执行 useEffect 销毁 + 回调函数。
- 类组件。
- 根据组件状态,同步执行,调用 class 生命周期:
- ComponentDidMount(组件挂载)、ComponentDidUpdate(组件卸载)
- 同步执行
this.setState(arg, callback)
中的callback
回调。
- 根据组件状态,同步执行,调用 class 生命周期:
- 处理 Ref 属性。
问题 :algebraic effects
Process 进程、Thread 线程、Coroutine 协程、Fiber 纤程
- JavaScript 通过 Generator,在协程层面实现了异步可中断更新。
algebraic effects 代数效应:是函数式编程中的概念,用于将副作用从函数调用中分离。
algebraic effects 的由来
- 在异步编程中,如果想用同步的思维编写代码,就要使用 async/await。但 async 函数具有传染性,调用 async 的函数也必须要用 async 定义。这样逐渐传染出去,所有的函数都需要变成异步函数,造成了大范围影响,同步异步代码也不易区分。
新的需求:在同步代码中,可以调用异步函数并获得结果。但不影响同步函数的逻辑(同步函数不要 async 定义)
- try/catch + throw 具有跳出当前代码块(throw 区),然后冒泡到 try/catch 被捕获,在 catch 继续执行代码的能力。
新的需求:Js 中,具有从同步代码跳出的逻辑,就是 try/catch 了,但只能从 throw 跳出到 catch,不可以再携带结果回到 throw 中。我们如何定义这样的功能呢?
- 此时我们需要一个可以在 throw 区暂停执行, 从 catch 区域获取答案,再跳回 throw 处继续执行代码的 “异步” 能力。这就是许多文章中说的定义一个语法:perform,try/handle,resume with。
function getName(user) {
let name = user.name;
if (name === null) {
// 1. 我们在这里 perform 了一个 effect:name = perform 'ask_name';
name = perform 'ask_name';
// 4. …… 然后最终回到了这里(name 现在是「Arya Stark」了
}
return name;
}
function makeFriends(user1, user2) {
user1.friendNames.add(getName(user2));
user2.friendNames.add(getName(user1));
}
const arya = { name: null };
const gendry = { name: 'Gendry' };
try {
makeFriends(arya, gendry);
} handle (effect) {
// 2. 我们跳到了handler(就像 try/catch)
if (effect === 'ask_name') {
// 3. 然而我们可以 resume with 一个值(这就不像 try / catch 了!)
resume with 'Arya Stark';
}
}
- 通过 perform + resume with 实现对后续流程的控制,控制反转 + 控制恢复。
- 通过 try/handle 实现跨调用栈捕获当前 continuation(延续—),在上面的例子中,就是让同步代码可以继续执行下去的那个值(name)。resume with 可以替换 当前的 continuaiton,让 perform 处恢复执行。
需求满足:通过上述的语法糖,实现了在同步代码中,调用一个异步函数但不影响同步代码区的目的。