在读完这篇文章之后,大家可以回到文章开头再捋一下以下几个关键词,将React的 Fiber架构原理彻底搞清楚。
关键词:
- requestIdleCallback、IdleDeadline
- Fiber:React的一个执行单元
在Fiber 出现之前,React存在什么问题?
React 16 之前,更新 Virtual DOM 的过程是采用Stack架构(循环加递归)实现的,任务一旦开始就无法中断;
如果 Virtual DOM 层级较深,主线程被长期占用,直到整颗虚拟DOM树对比更新完成之后主线程才能被释放,主线程才能执行其他任务,这会导致一些用户交互、动画无法立即执行;用户体感:页面卡顿。
Stack 架构的简单实现
这里,实现一个获取 jsx,然后将 jsx 转换成 DOM,然后添加到页面中的过程中。
const jsx = (
<div id="a1">
<div id="b1">
<div id="c1"></div>
<div id="c2"></div>
</div>
<div id="b2"></div>
</div>
)
function render(vdom, container) {
// 创建元素
const element = document.createElement(vdom.type)
// 为元素添加属性
Object.keys(vdom.props)
.filter(propName => propName !== "children") // 过滤 children 属性
.forEach(propName => (element[propName] = vdom.props[propName]))
// 递归创建子元素
if (Array.isArray(vdom.props.children)) {
vdom.props.children.forEach(child => render(child, element))
}
// 将元素添加到页面中
container.appendChild(element)
}
render(jsx, document.getElementById("root"))
jsx代码被转换成了真实的DOM添加到了页面中
Fiber 如何解决性能问题
- 放弃递归调用,因为递归调用一旦开始,无法终止;采用循环来模拟递归,因为循环可以随时被中断;(由链表取代了树,将虚拟dom连接,使得组件渲染的工作分片,到时会主动让出渲染主线程)
- 将大的渲染任务拆分成一个个小任务(小任务指的是 Fiber节点的构建);
- 使用 requestIdleCallback 去利用浏览器的空闲时间去执行小任务,React 在执行一个任务单元后,查看是否有其他高优先级的任务;如果有,放弃占用线程,先执行优先级高的任务。
fiber 这种数据结构使得节点可以回溯到其父节点、只要保留下中断的节点索引,就可以恢复之前的工作进度;
这里,简单解释一下 requestIdleCallback
window.requestIdleCallback()
方法将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间timeout,则有可能为了在超时前执行函数而打乱执行顺序。
requestIdleCallback
的作用:
浏览器的页面都是通过引擎一帧一帧绘制出来的,当每秒绘制的帧数达到 60 的时候,页面就是流畅的,玩过 fps 游戏的都知道,当这个帧数小于 60 的时候,人的肉眼就能感知出来卡顿。一秒 60 帧,每一帧分到的时间就是 1000/60 ≈ 16 ms,如果每一帧执行的时间小于 16 ms,就说明浏览器有空余时间,那么能不能通过浏览器的空余时间去处理任务呢,这样就不用一直等待主任务执行完了,requestIdleCallback 就是利用浏览器的空余时间去执行任务的。
requestIdleCallback 详解
Fiber 原理分析
Fiber 是一种数据结构,支撑 Fiber 构建任务的运转;
Fiber
其实就是JavaScript对象(这个对象中有child
属性表示节点的子节点,有sibling
属性表示节点的下一个兄弟节点,有return
属性表示节点的父级节点)
那么当一个 Fiber 任务执行完成后,通过 链表结构找到下一个要执行的任务单元。
// 简易版 Fiber 对象
type Fiber = {
// 组件类型 div、span、组件构造函数
type: any,
// DOM 对象
stateNode: any,
// 指向自己的父级 Fiber 对象
return: Fiber | null,
// 指向自己的第一个子级 Fiber 对象
child: Fiber | null,
// 指向自己的下一个兄弟 Fiber 对象
sibling: Fiber | null,
}
Fiber
工作共分为两个阶段:render
阶段和commit
阶段;
render
阶段:构建Fiber
对象,构建链表,在链表中标记要执行的DOM操作,可中断;commit
阶段:根据构建好的链表进行DOM操作,不可中断;
Fiber
的主要工作流程:
-
ReactDOM.render()
引导 React 启动或调用setState()
的时候开始创建或更新 Fiber 树。 -
从根节点开始遍历 Fiber Node Tree, 并且构建 WokeInProgress Tree(reconciliation 阶段)。
- 本阶段可以暂停、终止、和重启,会导致 react 相关生命周期重复执行。
- React 会生成两棵树,一棵是代表当前状态的 current tree,一棵是待更新的 workInProgress tree。
- 遍历 current tree,重用或更新 Fiber Node 到 workInProgress tree,workInProgress tree 完成后会替换 current tree。
- 每更新一个节点,同时生成该节点对应的 Effect List。
- 为每个节点创建更新任务。
-
将创建的更新任务加入任务队列,等待调度。
- 调度由 scheduler 模块完成,其核心职责是执行回调。
- scheduler 模块实现了跨平台兼容的 requestIdleCallback。
- 每处理完一个 Fiber Node 的更新,可以中断、挂起,或恢复。
-
根据 Effect List 更新 DOM (commit 阶段)。
- React 会遍历 Effect List 将所有变更一次性更新到 DOM 上。
- 这一阶段的工作会导致用户可见的变化。因此该过程不可中断,必须一直执行直到更新完成。
const jsx = (
<div id="a1">
<div id="b1">
<div id="c1"></div>
<div id="c2"></div>
</div>
<div id="b2"></div>
</div>
)
const container = document.getElementById("root")
// 构建根元素的 Fiber 对象
const workInProgressRoot = {
stateNode: container,
props: {
children: [jsx]
}
}
// 下一个要执行的任务
let nextUnitOfWork = workInProgressRoot
function workLoop(deadline) {
// 1. 是否有空余时间
// 2. 是否有要执行的任务
while (nextUnitOfWork && deadline.timeRemaining() > 0) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
}
// 表示所有的任务都已经执行完成了
if (!nextUnitOfWork) {
// 进入到第二阶段 执行DOM
commitRoot()
}
requestIdleCallback(workLoop)
}
function commitRoot() {
let currentFiber = workInProgressRoot.firstEffect
while (currentFiber) {
currentFiber.return.stateNode.appendChild(currentFiber.stateNode)
currentFiber = currentFiber.nextEffect
}
}
// 在浏览器空闲的时候执行任务
requestIdleCallback(workLoop)
function performUnitOfWork(workInProgressFiber) {
// 1. 创建 DOM 对象并将它存储在 stateNode 属性
// 2. 构建当前 Fiber 的子级 Fiber
// 向下走的过程
// 构建子集
beginWork(workInProgressFiber);
// 如果当前Fiber有子级
if (workInProgressFiber.child) {
// 返回子级 构建子级的子级
return workInProgressFiber.child
}
while (workInProgressFiber) {
// 向上走,构建链表
completeUnitOfWork(workInProgressFiber)
// 如果有同级
if (workInProgressFiber.sibling) {
// 返回同级 构建同级的子级
return workInProgressFiber.sibling
}
// 更新父级
workInProgressFiber = workInProgressFiber.return
}
}
// 构建子集
function beginWork(workInProgressFiber) {
// 1. 创建 DOM 对象并将它存储在 stateNode 属性
if (!workInProgressFiber.stateNode) {
// 创建 DOM
workInProgressFiber.stateNode = document.createElement(
workInProgressFiber.type
)
// 为 DOM 添加属性
for (let attr in workInProgressFiber.props) {
if (attr !== "children") {
workInProgressFiber.stateNode[attr] = workInProgressFiber.props[attr]
}
}
}
// 2. 构建当前 Fiber 的子级 Fiber
if (Array.isArray(workInProgressFiber.props.children)) {
let previousFiber = null
workInProgressFiber.props.children.forEach((child, index) => {
let childFiber = {
type: child.type,
props: child.props,
effectTag: "PLACEMENT",
return: workInProgressFiber
}
if (index === 0) {
// 构建子集,只有第一个子元素是子集
workInProgressFiber.child = childFiber
} else {
// 不是第一个,则构建子集的 兄弟级
previousFiber.sibling = childFiber
}
previousFiber = childFiber
})
}
// console.log(workInProgressFiber)
}
function completeUnitOfWork(workInProgressFiber) {
// 获取当前 Fiber 的父级
const returnFiber = workInProgressFiber.return
// 父级是否存在
if (returnFiber) {
// 需要执行 DOM 操作的 Fiber
if (workInProgressFiber.effectTag) {
if (!returnFiber.lastEffect) {
returnFiber.lastEffect = workInProgressFiber.lastEffect
}
if (!returnFiber.firstEffect) {
returnFiber.firstEffect = workInProgressFiber.firstEffect
}
if (returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = workInProgressFiber
} else {
returnFiber.firstEffect = workInProgressFiber
}
returnFiber.lastEffect = workInProgressFiber
}
}
}
为什么 vue 不需要 fiber 架构?
react 直到哪个组件触发了更新,但是不知道哪些子组件会受到影响。因此react 需要生成该组件下的所有虚拟DOM结构,与原本的虚拟DOM结构进行对比,找出变动的部分。
在 vue 中,一切影响页面内容的数据都应该是响应式的, vue通过拦截响应式数据的修改,知道哪些组件应该被修改,不需要遍历所有子树。vue的diff算法是对组件内部的diff,如果存在子组件,会判断子组件上与渲染相关的属性是否发生变化,无需变化的化则复用原本的DOM,不会处理子组件。
模板语法让vue能够进行更好地编译时分析,提高优化过程的效率,react缺少这部分,无法识别哪些是静态节点,哪些是动态节点。