为什么是“从一到二”?因为“一”在上一篇,本篇的内容是在“一”的基础上实现的。
mini-react 第一篇:从零到一实现最最最基本的 mini-react
mini-react 第三篇: 从二到三实现统一提交
一、为什么使用 fiber 架构?
从已知者的角度来讲,fiber 架构其实是对 DOM 挂载的优化。那么它又是针对什么的优化呢?
请考虑这样一个场景:当我们有非常大的一棵虚拟 DOM 树需要创建标签并挂载时,js 引擎一直在工作,页面会丢失交互的响应。——这里补充一个小知识,js 引擎是单线程工作的,js 文件、鼠标点击事件、页面滚动事件等都需要由 js 引擎来执行,这些动作需一个执行完毕后才能进入下一个。
由于 js 引擎执行完任务会进入空闲阶段,我们正好可以利用这个空闲阶段来执行上述场景中的动作 —— 创建并挂载 DOM 节点。
这样一来,就需要一个算法,使得我们能够边遍历到树的每一个虚拟 dom 节点。—— 像我这种笨蛋第一时间想到的是数组(开始浪费空间.jpg。
而聪明的工程师想到了 fiber 架构 —— 将一棵不规则的树转化成一棵 child-sibling 二叉树(当一棵树的每个节点,子节点是它的 child 和 sibling,这不正是一棵二叉树吗!),于是只需记录根节点。
二、利用空闲阶段
—— 任务调度器
浏览器为我们提供了一个空闲时期回调的 API —— requestIdleCallback
function workLoop(deadline) {
let shouldYield = false;
if (!shouldYield) {
console.log(111);
shouldYield = deadline.timeRemaining() < 1;
}
requestIdleCallback(workLoop);
}
requestIdleCallback(workLoop);
三、Fiber 架构
3.1 建立 child-sibling 树
/**
* 普通组件
* @param {Object} vnode
*/
function updateHostComponent(vnode) {
// ... 创建 DOM 节点,处理 props(除children)
// 3. 处理 props.children
let prevChild = null;
vnode.props.children.forEach((child, index) => {
if (index === 0) {
vnode.child = child;
} else {
prevChild.sibling = child;
}
child.parent = vnode;
prevChild = child;
});
// ... 挂载 DOM
}
/**
* 函数组件
* @param {Object} vnode
*/
function updateFunctionComponent(vnode) {
const child = vnode.type();
vnode.child = child;
child.parent = vnode;
}
3.2 遍历 fiber 架构
简而言之,是先序遍历。
// 1. return child
if (fiber.child) return fiber.child;
// 2. return sibling
if (fiber.sibling) return fiber.sibling;
// 3. return 叔叔/祖叔叔/太叔叔...
let fiberParent = fiber.parent;
while (fiberParent && !fiberParent.sibling) fiberParent = fiberParent.parent;
return fiberParent?.sibling;
3.3 完整代码
/** React.js */
// 记录当前根节点
let root = null;
// 记录下一个空闲时期要执行的任务
let nextUnitOfWork = null;
function render(vnode, container) {
root = {
dom: container,
props: {
children: [vnode]
}
};
nextUnitOfWork = root;
}
/**
* 函数组件
* @param {Object} vnode
*/
function updateFunctionComponent(vnode) {
const child = vnode.type();
vnode.child = child;
child.parent = vnode;
}
/**
* 普通组件
* @param {Object} vnode
*/
function updateHostComponent(vnode) {
if (!vnode.dom) {
// 1. 创建 DOM 节点
const dom = (vnode.dom =
vnode.type === 'ELEMENT_TEXT'
? document.createTextNode('')
: document.createElement(vnode.type));
// 2. 赋值 props
for (const key in vnode.props) {
if (key === 'children') continue;
dom[key] = vnode.props[key];
}
}
// 3. 处理 props.children
let prevChild = null;
vnode.props.children.forEach((child, index) => {
if (index === 0) {
vnode.child = child;
} else {
prevChild.sibling = child;
}
child.parent = vnode;
prevChild = child;
});
// 4. 挂载
let parentFiber = vnode.parent;
if (parentFiber) {
while (!parentFiber.dom) {
parentFiber = parentFiber.parent;
}
parentFiber.dom.appendChild(vnode.dom);
}
}
/**
* 一次只处理一个任务,并返回下一个任务
* @param {object} fiber
*/
function performUnitOfWork(fiber) {
const isFunctionComponent = fiber.type instanceof Function;
if (isFunctionComponent) {
updateFunctionComponent(fiber);
} else {
updateHostComponent(fiber);
}
/* 先序遍历 */
// 1. return child
if (fiber.child) return fiber.child;
// 2. return sibling
if (fiber.sibling) return fiber.sibling;
// 3. return 叔叔/祖叔叔/太叔叔...
let fiberParent = fiber.parent;
while (fiberParent && !fiberParent.sibling) fiberParent = fiberParent.parent;
return fiberParent?.sibling;
}
function workLoop(deadline) {
let shouldYield = false;
// nextUnitOfWork 不存在时,恭喜,完成所有 dom 创建与挂载任务。
if (!shouldYield && nextUnitOfWork) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
shouldYield = deadline.timeRemaining() < 1;
}
requestIdleCallback(workLoop);
}
requestIdleCallback(workLoop);
为验证该代码是否正确,App 的代码修改如下
import React from './core/React';
const App = function () {
return (
<div id="app">
<div>
<h2>
歌手:<span>容祖儿</span>
</h2>
</div>
<ul>
<li>东京人寿</li>
<li>再见我的初恋</li>
<li>隆重登场</li>
<li>华丽邂逅</li>
</ul>
</div>
);
};
export default App;
3.4 代码抽取与改进
- 处理 children 的步骤一致,可抽取。
- 对
props.children的处理中,修改了原有 child,不太合适,改为复制一个新的对象作为 child 使用。
/**
* 函数组件
* @param {Object} vnode
*/
function updateFunctionComponent(vnode) {
const child = vnode.type();
initChildren(vnode, [child]);
}
/**
* 普通组件
* @param {Object} vnode
*/
function updateHostComponent(vnode) {
if (!vnode.dom) {
// 1. 创建 DOM 节点
const dom = (vnode.dom =
vnode.type === 'ELEMENT_TEXT'
? document.createTextNode('')
: document.createElement(vnode.type));
// 2. 赋值 props
for (const key in vnode.props) {
if (key === 'children') continue;
dom[key] = vnode.props[key];
}
}
// 3. 处理 props.children
initChildren(vnode, vnode.props.children);
// 4. 挂载
let parentFiber = vnode.parent;
if (parentFiber) {
while (!parentFiber.dom) {
parentFiber = parentFiber.parent;
}
parentFiber.dom.appendChild(vnode.dom);
}
}
function initChildren(fiber, children) {
let prevChild = null;
children.forEach((child, index) => {
// 用新的对象,不改变原有对象
let newChild = {
...child,
dom: null,
child: null,
sibling: null,
parent: null
};
if (index === 0) {
fiber.child = newChild;
} else {
prevChild.sibling = newChild;
}
newChild.parent = fiber;
prevChild = newChild;
});
}
3.5 重命名
自行将 vnode 改为 fiber —— 毕竟是 fiber 架构[手动狗头]