之前写了一篇stack版的mini-react实现,这里再写一篇fiber版的实现。
这里如果不知道两者的区别的话,推荐先看看我这一篇文章:
stack和fiber架构的区别
从我上面连接这篇文章我们可以知道:React 16 之前的版本比对更新 VirtualDOM 的过程是采用循环加递归实现的,这种比对方式有一个问题,就是一旦任务开始进行就无法中断,如果应用中组件数量庞大,主线程被长期占用,直到整棵 VirtualDOM 树比对更新完成之后主线程才能被释放,主线程才能执行其他任务。这就会导致一些用户交互,动画等任务无法立即得到执行,页面就会产生卡顿, 非常的影响用户体验。
其主要问题是:递归无法中断,执行重任务耗时长。 JavaScript 又是单线程,无法同时执行其他任务,导致任务延迟页面卡顿,用户体验差。
我们得解决方案是:
- 利用浏览器空闲时间执行任务,拒绝长时间占用主线程
- 放弃递归只采用循环,因为循环可以被中断
- 任务拆分,将任务拆分成一个个的小任务
基于以上几点,在这里我们先了解下requestIdleCallback这个api
核心 API 功能介绍:利用浏览器的空余时间执行任务,如果有更高优先级的任务要执行时,当前执行的任务可以被终止,优先执行高级别任务。
requestIdleCallback(function(deadline) {
// deadline.timeRemaining() 获取浏览器的空余时间
})
这里我们了解下什么是浏览器空余时间:页面是一帧一帧绘制出来的,当每秒绘制的帧数达到 60 时,页面是流畅的,小于这个值时, 用户会感觉到卡顿,1s 60帧,每一帧分到的时间是 1000/60 ≈ 16 ms,如果每一帧执行的时间小于16ms,就说明浏览器有空余时间。
如果任务在剩余的时间内没有完成则会停止任务执行,继续优先执行主任务,也就是说 requestIdleCallback 总是利用浏览器的空余时间执行任务。
我们先用这个api做个例子,来看:
html
playground
css
js
var play = document.getElementById("play")
var workBtn = document.getElementById("work")
var interactionBtn = document.getElementById("interaction")
var iterationCount = 100000000
var value = 0
var expensiveCalculation = function (IdleDeadline) {
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 () {
play.style.background = "palegreen"
})
从这个示例中我们知道了,这个api该如何使用,该如何中断任务。
然后我们来实现我们得fiber架构得react-mini版本。
在 Fiber 方案中,为了实现任务的终止再继续,DOM比对算法被分成了两部分:
- 构建 Fiber (可中断)
- 提交 Commit (不可中断,更新dom)
目前我们设计得fiber对象有以下属性:
{
type 节点类型 (元素, 文本, 组件)(具体的类型)
props 节点属性
stateNode 节点 DOM 对象 | 组件实例对象
tag 节点标记 (对具体类型的分类 hostRoot || hostComponent || classComponent || functionComponent)
effects 数组, 存储需要更改的 fiber 对象
effectTag 当前 Fiber 要被执行的操作 (新增, 删除, 修改)
parent 当前 Fiber 的父级 Fiber
child 当前 Fiber 的子级 Fiber
sibling 当前 Fiber 的下一个兄弟 Fiber
alternate Fiber 备份 fiber 比对时使用
}
这时我们的项目结构
react/index.js 是入口文件,在这里我们对它做react主要使用api的导出
import createElement from "./CreateElement"
export { render } from "./reconciliation"
export { Component } from "./Component"
export default {
createElement
}
然后我们先来写createElement 用来把jsx生成 vnode的函数:
react/CreateElement
export default function createElement(type, props, ...children) {
const childElements = [].concat(...children).reduce((result, child) => {
if (child !== false && child !== true && child !== null) {
if (child instanceof Object) {
result.push(child)
} else {
result.push(createElement("text", { textContent: child }))
}
}
return result
}, [])
return {
type,
props: Object.assign({ children: childElements }, props)
}
}
这个的实现是和stack架构一样的
接下来就是我们的render函数,reconciliation/index.js 这里也就是我们核心的协调算法了.
在这里我们主要工作是分两个阶段:
1.根据vnode来生成fiber对象,生成fiber的过程是通过循环来生成的,因为我们生成fiber以及它的子集fiber的过程是可以被打断的,所以我们通过循环的方式来生成,也就是说我们现在的fiber的结构是一个链表的结构.
这是一个树的结构,在我们生成的fiber对象中,会有parent指向父节点,child指向我们当前子集的第一个的节点,也就是最左侧的节点,子集中其他节点是我们这个child的兄弟节点sibling。
每次构建一个子集就是一个子任务,fiber任务是可以被打断的。
2.我们每次构建fiber会把当前fiber以及自身收集到的子集fiber的effects数组放入父级的effects数组中,然后第二个阶段是commit生成dom阶段,我们只需要循环最外层的fiber中effects数组就可以,这个数组中放着所有的fiber对象,我们只需要依次把它们的stateNode 也就是dom对象根据effectTag操作符,来对比更新,删除新增到它的parent父级的stateNode中.
这样我们一个完整流程就完成了.