Open jtwang7 opened 2 years ago
// 在完成的时候要收集有副作用的fiber,组成effect list
const completeUnitOfWork = (currentFiber) => {
// 每个fiber有两个属性:
// 1)firstEffect:指向第一个有副作用的子fiber
// 2)lastEffect:指向最后一个有副作用的子fiber
let returnFiber = currentFiber.return // fiber 父节点
if (returnFiber) {
// 将当前 fiber 中有副作用的子 fiber 链接到父节点的后面
if (!returnFiber.firstEffect) {
returnFiber.firstEffect = currentFiber.firstEffect
}
if (currentFiber.lastEffect) {
if (returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = currentFiber.firstEffect
}
returnFiber.lastEffect = currentFiber.lastEffect
}
// 若当前 fiber 自身存在副作用,则将其链接到父节点后面
const effectTag = currentFiber.effectTag
if (effectTag) { // 说明有副作用
// 中间的使用nextEffect做成一个单链表
if (returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = currentFiber
} else {
returnFiber.lastEffect = currentFiber
}
returnFiber.lastEffect = currentFiber
}
}
}
定义一个函数,从根节点出发,把全部的 fiber 节点遍历一遍,产出最终全部的effect list
const performUnitOfWork = (currentFiber) => {
// 递归到叶子节点
if (currentFiber.child) {
return currentFiber.child
}
// 该处开始执行时,currentFiber 为叶子节点,向上收集 effect 并构建链表
while (currentFiber) {
completeUnitOfWork(currentFiber) // 构建链表
// 遍历同级兄弟节点
if (currentFiber.sibling) {
return currentFiber.sibling
}
currentFiber = currentFiber.return // 同级遍历完成后,将结果收缩到父节点
}
}
根据一个 fiber 的effect list列表去更新视图 (这里只列举了新增节点、删除节点、更新节点的三种操作)
// 作用:基于当前 fiber 状态更新单元,更新节点
const commitWork = currentFiber => {
if (!currentFiber) return
let returnFiber = currentFiber.return
// 父节点元素
let returnDOM = returnFiber.stateNode
// fiber 内保存有更新操作的标识,基于该标识更新节点
if (currentFiber.effectTag === INSERT) {
// 如果当前fiber的effectTag标识位INSERT,则代表其是需要插入的节点
returnDOM.appendChild(currentFiber.stateNode)
} else if (currentFiber.effectTag === DELETE) {
// 如果当前fiber的effectTag标识位DELETE,则代表其是需要删除的节点
returnDOM.removeChild(currentFiber.stateNode)
} else if (currentFiber.effectTag === UPDATE) {
// 如果当前fiber的effectTag标识位UPDATE,则代表其是需要更新的节点
if (currentFiber.type === ELEMENT_TEXT) {
if (currentFiber.alternate.props.text !== currentFiber.props.text) {
currentFiber.stateNode.textContent = currentFiber.props.text
}
}
}
// 更新完成,清理标识符
currentFiber.effectTag = null
}
根据全部 fiber 的 effect list 更新视图
const commitRoot = () => {
let currentFiber = workInProgressRoot.firstEffect // 找到更新的头节点
while (currentFiber) {
commitWork(currentFiber) // 更新操作(将 effect 提交到真实 DOM)
currentFiber = currentFiber.nextEffect // 移动指针
}
currentRoot = workInProgressRoot // 把当前渲染成功的根fiber赋给currentRoot (保存)
workInProgressRoot = null // 清空,表明完成视图更新
}
const workloop = (deadline) => {
// render 阶段
// 可通过移交控制权的方式,打断 render,同时会记录打断的位置,下次从打断处继续执行
let shouldYield = false // 是否需要让出控制权
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork) // 构建 effect list
shouldYield = deadline.timeRemaining() < 1 // 如果执行完任务后,剩余时间小于1ms,则需要让出控制权给浏览器
}
// commit 阶段
// 不可被打断,将 effect list 内操作一次性完成更新
if (!nextUnitOfWork && workInProgressRoot) {
console.log('render阶段结束')
commitRoot() // 没有下一个任务了,根据effect list结果批量更新视图
}
// 递归视图更新操作,在浏览器空闲时间段内执行
requestIdleCallback(workloop, { timeout: 1000 })
}
React - Fiber 概述
参考文章:走进React Fiber的世界
浏览器帧渲染
在浏览器中,页面是一帧一帧展示更新的,渲染的帧率与设备的刷新率保持一致。一般情况下,设备的屏幕刷新率为 1s 60次,也就是说,浏览器大约每 1 s 绘制 60 帧。当每秒内绘制的帧数(FPS)超过60时,页面渲染是流畅的;而当FPS小于60时,会出现一定程度的卡顿现象。
一次完整的帧渲染流程
完整的一帧中,按顺序从上到下做了如下事情:
根据浏览器的渲染进程,我们可以得到帧渲染的另一种执行流程表达: 参考 JS - 进程与线程
在浏览器渲染进程中,GUI 线程和JS 线程是互斥的,因此你可以发现,一帧页面的渲染,也是先完成 JS 线程中所有的操作后,再执行 GUI 线程的。如果在某个阶段执行任务特别长,例如在定时器阶段或Begin Frame阶段执行时间非常长,时间已经明显超过了16ms,那么就会阻塞页面的渲染,从而出现卡顿现象。
React 为什么要引进 Fiber ?
reconcilation (协调):在 react16 引入 Fiber 架构之前,react 会采用深度优先遍历的递归方法对比虚拟DOM树,找出需要变动的节点,然后同步更新它们。
存在的问题
由于遍历是递归调用,执行栈会越来越深,而且不能中断,中断后就不能恢复了。递归如果非常深,就会十分卡顿。传统的方法存在不能中断和执行栈太深的问题。在 reconcilation 期间,react 会一直占用浏览器资源直到所有变化节点更新完成,会导致用户触发的事件得不到响应。
Fiber 设计思想
React 为了解决协调期间长时间占用主线程的问题,引入了 Fiber 来改变这种不可控的现状,把渲染/更新过程拆分为一个个小块的任务,通过合理的调度机制来调控时间,指定任务执行的时机,从而降低页面卡顿的概率,提升页面交互体验。通过Fiber架构,让 reconcilation 协调过程变得可被中断,适时地让出CPU执行权,可以让浏览器及时地响应用户的交互。
Vue 为什么没有引入 Fiber ?
Fiber
数据结构
Fiber 可以理解为是一种数据结构,在 React16 之后,每个 Virtual DOM 都有一个对应的 Fiber 对象表示。React Fiber 采用链表实现的,在虚拟 DOM 树结构中,每个节点都是一个 fiber。一个 fiber包括了 child(第一个子节点)、sibling(兄弟节点)、return(父节点)等属性。
执行单元
fiber 除了描述一个虚拟 DOM 节点之外,还可以被理解为一个执行单元。在 fiber 引入以前,react 更新 DOM 树需要遍历更新整颗 DOM 树,而且该过程不支持中断,这也就意味着 DOM 树结构在 react16 以前,会被视为一个大块的执行单元,引入 fiber 后,执行单元被拆分到各个节点上,虽然各个节点(小任务)的渲染执行必须是一次完成的,不能出现暂停,但是一个小块任务执行完后可以移交控制权给浏览器去响应用户,从而不用像之前一样要等那个大任务(整颗 DOM 树完成更新)一直执行完成再去响应用户。
具体流程: 首先 React 向浏览器请求调度,若浏览器在一帧中如果还有空闲时间,则会响应该调度请求,会去判断 react 任务队列中是否存在待执行任务,不存在就直接将控制权交给浏览器,如果存在就会执行对应的任务,执行完成后会判断是否还有时间,有时间且有待执行任务则会继续执行下一个任务,否则就会将控制权交给浏览器。
requestIdleCallback - 实现 Fiber 的基础 api
requestIdleCallback 能使开发者在主事件循环上执行后台和低优先级的工作,而不影响延迟关键事件,如动画和输入响应。正常帧任务完成后没超过16ms,说明有多余的空闲时间,此时就会执行 requestIdleCallback 里注册的任务。 react 虽然自己实现了 requestIdleCallback 的封装,但核心思想没有改变,通过注册回调,在每次帧渲染时请求时间片,并期望在剩余的空闲时间内执行低优先级的回调任务。
具体流程:
用 requestIdleCallback 方法注册对应的任务,告诉浏览器这个任务优先级不高,当每一帧内存在空闲时间时,再执行注册的这个任务即可。浏览器执行完这个方法后,如果没有剩余时间了,或者已经没有下一个可执行的任务了,React应该归还控制权,并同样使用requestIdleCallback去申请下一个时间片。
多帧执行
一帧页面渲染并非只能对应一个 requestIdleCallback 的注册回调,实际上 requestIdleCallback 的回调可在所有帧渲染时调用,前提是该帧所有操作完成后仍有剩余空闲时间 (或已经到达 timeout 规定时间需要强制执行)。假设剩余空间可以执行好几个注册回调,那么它们就会被依次取出来执行,若执行某次回调后没有剩余时间或者已经超出剩余时间了,那么就必须将控制权返还给浏览器,同时发起下一次的时间片请求,再下一段空闲时间继续执行剩余的回调函数。
fiber 节点设计
Fiber 的拆分单元是 fiber(fiber tree上的一个节点)。实际上一个 fiber 就对应一个虚拟 DOM 节点,虚拟 DOM 树会对应生成 Fiber 树。fiber 节点结构如下,源码详见 ReactInternalTypes.js。
fiber 节点的几个主要属性:
Fiber 状态更新的链表结构设计
Fiber 结构是使用链表实现的,Fiber tree 实际上是个单链表树结构。同样,Fiber 树内的状态更新也是由链表结构实现的,每个 fiber 节点对应保存有自己的一个状态更新单元,通过更新队列的形式组织成一个链表结构,统一更新状态:![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b440a981e4c040b2ab3523f065bffea7~tplv-k3u1fbpfcp-watermark.awebp)
fiber 状态单元
一个 fiber 状态单元包含 payload(数据)和 nextUpdate(指向下一个更新单元的指针):
更新队列
定义一个队列,把每个 fiber 状态单元串联起来,其中定义了两个指针:
作用是指向第一个更新单元和最后一个更新单元,并加入了 baseState 属性存储 React 中整体的 state 状态。
接下来定义两个方法:
插入节点单元时需要考虑是否已经存在节点,如果不存在直接将 firstUpdate、lastUpdate 指向此节点即可。 更新队列是遍历这个链表,根据 payload 中的内容去更新 state 的值。
Demo 模拟 Fiber 的状态更新
Fiber 在 React 生命周期中的执行流程
首先,我们知道 react 生命周期可被分为三个阶段:(render / pre-commit / commit)
Fiber 在 render 和 commit 阶段的表现是不同的:
render 阶段
此阶段会找出所有节点的变更,如节点新增、删除、属性变更等,这些变更 react 统称为副作用 (effect)。 此阶段会构建一棵虚拟 DOM 树以及对应的 Fiber tree,以虚拟dom节点为单位对任务进行拆分,即一个虚拟dom节点对应一个任务(fiber),最后产出的结果是effect list,从中可以知道哪些节点更新、哪些节点增加、哪些节点删除了。
深度优先遍历(后序遍历)
已知 render 阶段会基于虚拟 DOM 树映射出 Fiber tree,Fiber Tree 每个节点都有child、sibling、return属性,最终目的是得到一个 effect list,该列表中将包含 Fiber Tree 的所有更新任务。因此,我们最终期望在根结点获得 effect list,也就是后序遍历从下到上收集结果,采用深度优先后序遍历的方法:
收集effect list
在遍历过程中,收集所有节点的变更产出effect list,注意其中只包含了需要变更的节点。通过每个节点更新结束时向上归并effect list来收集任务结果,最后根节点的effect list里就记录了包括了所有需要变更的结果。 具体步骤:
commit阶段
commit 阶段需要将上阶段计算出来的需要处理的副作用一次性执行,此阶段不能暂停,否则会出现UI更新不连续的现象。此阶段需要根据effect list,将所有更新都 commit 到DOM树上 (从根节点出发递归,根据effect list完成全部更新)。