概述
React 16 之前的版本比对更新 VirtualDOM 的过程是采用循环加递归实现的,这种比对方式有一个问题,就是一旦任务开始进行就无法中断,如果应用中组件数量庞大,主线程被长期占用,直到整棵 VirtualDOM 树比对更新完成之后主线程才能被释放,主线程才能执行其他任务。这就会导致一些用户交互,动画等任务无法立即得到执行,页面就会产生卡顿, 非常的影响用户体验。
Fiber就是React提出的用于解决页面卡顿的方案,包含如下三个方面:
- 利用浏览器的空闲时间执行任务,不会长时间占用主线程。
- 因为利用了空闲时间执行任务,所以任务需要可以被随时中断,而迭代是无法中断的,循环是随时可以中断的,因此用循环替代迭代。
- 将对比更新操作拆分成一个个小的任务。
核心API
requestIdleCallback 是浏览器提供的API,其利用浏览器的空闲时间执行任务,如果有更高优先级的任务需要执行时,当前执行的任务可会被终止,优先执行更高优先级的任务。
requestIdleCallback接受一个函数作为参数,该函数是要执行的任务:
requestIdleCallback(function(deadline) {
// deadline.timeRemaining() 获取浏览器的空余时间
})
API示例
在下面的html实例中,页面上包含两个按钮,点击第一个按钮执行一段耗时操作,点击另一个按钮alert显示一段内容:
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
<title>Page Title</title>
<meta name='viewport' content='width=device-width, initial-scale=1'>
</head>
<body>
<button id="work">long time work</button>
<button id="interaction">another work</button>
</body>
<script>
var workBtn = document.getElementById("work")
var interactionBtn = document.getElementById("interaction")
var iterationCount = 100000000
var value = 0
// 模拟一段耗时操作
workBtn.addEventListener("click", function () {
while (iterationCount > 0) {
value =
Math.random() < 0.5 ? value + Math.random() : value + Math.random()
iterationCount = iterationCount - 1
}
})
interactionBtn.addEventListener("click", function () {
alert('done another work')
})
</script>
</html>
当点击第一个按钮之后迅速点击第二个按钮,会发现页面会卡顿一段时间之后才执行alert。
当用requestIdleCallback API改造之后:
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
<title>Page Title</title>
<meta name='viewport' content='width=device-width, initial-scale=1'>
</head>
<body>
<button id="work">long time work</button>
<button id="interaction">another work</button>
</body>
<script>
var workBtn = document.getElementById("work")
var interactionBtn = document.getElementById("interaction")
var iterationCount = 100000000
var value = 0
// 模拟一段耗时操作
var expensiveCalculation = function (IdleDeadline) {
// 空闲时间超过1秒才执行
while (iterationCount > 0 && IdleDeadline.timeRemaining() > 1) {
value =
Math.random() < 0.5 ? value + Math.random() : value + Math.random()
iterationCount = iterationCount - 1
}
requestIdleCallback(expensiveCalculation)
}
workBtn.addEventListener("click", function () {
requestIdleCallback(expensiveCalculation)
})
interactionBtn.addEventListener("click", function () {
alert('done another work')
})
</script>
</html>
再次快速点击第一个按钮和第二个按钮,会发现,页面迅速alert一段信息,说明第一个任务并没有阻塞第二个任务。
思路
Fiber将Dom对比算法分解成两步:
- 构建Fiber对象(可理解成一个个小的任务对象),这个过程可以随时被中断。
- 提交:将Fiber对象渲染成真实Dom,这个过程是不可以被中断的。
Fiber对象
为了能够模拟实现整个Fiber的核心代码,需要首先了解Fiber对象的结构,Fiber对象是一个普通的js对象,其包含如下属性:
属性名 | 说明 |
---|---|
type | 节点类型,和虚拟Dom对象的type相同,用于区分元素、文本、组件 |
props | 节点属性,同虚拟Dom对象 |
stateNode | 节点Dom对象或者组件实例 |
tag | 标记,用于标记节点 |
effects | 存储包含自身和所有后代的Fiber数组 |
effectTag | 标记当前节点需要进行的操作,包含插入、更新、移除等 |
parent | 父Fiber对象,在React源码中叫Return |
child | 当前Fiber对象的子级Fiber对象 |
sibling | 当前Fiber对象的下一级兄弟节点 |
alternate | Fiber对象备份,用于对比 |
最终虚拟Dom树会被转换成Fiber对象的树形结构数据,最顶层的节点effects属性中包含了该树结构所有的Fiber对象,其是一个数组,也就是前文说的能被中断的一个个小任务的任务操作对象。
模拟任务队列
本节将模拟实现一个任务队列,该任务队列将为后续Fiber执行提供基础,当浏览器有空闲时间时,会不断的从任务队列中取出任务并执行。
任务队列:
const createTaskQueue = () => {
const taskQueue = []
return {
/**
* 向任务队列中添加任务
*/
push: item => taskQueue.push(item),
/**
* 从任务队列中获取任务
*/
pop: () => taskQueue.shift(),
/**
* 判断任务队列中是否还有任务
*/
isEmpty: () => taskQueue.length === 0
}
}
export default createTaskQueue
实现空闲时间任务调度
render方法是节点挂载的入口方法,当调用render的时候,会执行首次渲染或者对比更新,因此需要修改render方法。利用浏览器提供的api实现空闲执行。
// 创建任务队列
const taskQueue = createTaskQueue()
// 空闲时间执行的具体方法
const performTask = deadline => {
// 执行任务,workLoop方法后续补充
workLoop(deadline)
// 实现持续调用
if (subTask || !taskQueue.isEmpty()) {
requestIdleCallback(performTask)
}
}
// 暴露的render方法
export const render = (element, dom) => {
// 1. 添加任务 =》 构建fiber对象
taskQueue.push({
dom,
props: { children: element }
})
// 2. 指定浏览器空闲时间执行performTask 方法
requestIdleCallback(performTask)
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。