接下来我们需要解决上文《深入理解react------1. jsx与虚拟dom》留下来的问题。
一 Fiber
比较正式的fiber架构介绍的文章各种各样,我这里就不再重复,我只用比较简单接地气的方式描述一下我们怎么解决上面的问题,以及我们接下来应该怎么做。相信做完后大家自己能够去总结,到底什么是fiber。
1.1 问题分析以及解决策略
1.1.1问题
这是我们将虚拟dom渲染到界面的代码,很容易可以看出在第十五行有一个递归调用,当界面节点过多时js主线程就会一直卡在这里,这就造成了页面的卡顿。
js
function render(element, container) {
const dom =
element.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(element.type)
const isProperty = key => key !== "children"
Object.keys(element.props)
.filter(isProperty)
.forEach(name => {
dom[name] = element.props[name]
})
element.props.children.forEach(child =>
render(child, dom)
)
container.appendChild(dom)
}
1.1.2 解决方案
要说明方案,我们先澄清几个概念,虚拟dom,工作单元(nextUnitOfWork)。
这里给出的概念不是官方概念,而是我自己理解给出的内容
-
虚拟dom,一个纯js的对象,也就是我们使用createElement 得到的js对象
-
工作单元(nextUnitOfWork),根据虚拟dom进行的一系列操作最小单元。
- 打上标记,以便于分辨下次更新是增删还是改
- 判断是否还有剩余时间进行下一个工作单元的计算
- 返回下一个工作单元
- 判断是否全部计算完成,能够在界面上进行渲染(commit阶段)
我们假设界面上有如上图的结构,我们对A进行修改。
- 首先我们需要能够感受到A的变化
- 其次我们从A开始处理,并返回下一个工作单元,直至整个树计算完成
- 最后在合适的时机进行提交(渲染到界面上)
二 代码实现
2.1 workLoop
js
// 全局变量
// nextUnitOfWork: 下一个需要处理的工作单元(Fiber节点)
let nextUnitOfWork = null;
// wipRoot: 当前正在构建的Fiber树的根节点
let wipRoot = null;
// currentRoot: 上一次提交到DOM的Fiber树的根节点
let currentRoot = null;
let deletions = null;
// 启动Fiber树的构建循环
requestIdleCallback(workLoop);
/**
* workLoop 函数
* 浏览器空闲时执行的主循环,用于协调Fiber树的构建
* @param {IdleDeadline} deadline - 浏览器提供的空闲时间对象
*/
export function workLoop(deadline) {
let shouldYield = false; // 是否需要暂停工作
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork); // 执行当前工作单元
shouldYield = deadline.timeRemaining() < 1; // 如果剩余时间不足,暂停工作
}
if (!nextUnitOfWork && wipRoot) {
commitRoot(); // 如果没有工作单元且Fiber树构建完成,提交更新
}
requestIdleCallback(workLoop); // 继续下一次工作
}
此处使用requestIdleCallback模仿真实react的行为,利用浏览器空闲时间进行计算,避免阻塞优先级更高的任务。真实的react使用MessageChannel 实现。
从以上代码可以看到,workLoop一直在被递归调用,直到没有下一个工作单元,并且wipRoot构建完成,则进行更新。
2.2 render
那结合上面的例子,当我们有了新的nextUnitOfWork 并且浏览器有足够的空闲时间,就会开启新一轮的渲染。所以我们需要将render函数进行一些修改。render将不再操作dom更新,只是赋值变量,方便下一次的workLoop开启计算。
js
/**
* render 函数
* 用于初始化Fiber树并设置根节点的属性
* @param {Object} element - React元素对象
* @param {HTMLElement} container - 容器DOM节点
*/
export function render(element, container) {
wipRoot = {
dom: container, // 容器DOM节点
props: { children: [element] }, // 根节点的子元素
alternate: currentRoot, // 保存上一次的Fiber树,用于比较
};
console.log("wipRoot:", wipRoot);
deletions = [];
nextUnitOfWork = wipRoot; // 设置下一个工作单元为根节点
}
2.3 performUnitOfWork
接下来是最为核心的部分,最小工作单元的计算工作,且返回下一个工作单元。其实也就是我们常常说的"调和"(reconcile)
每一个fiber打上不同的标记effectTag,用于判断增删改。
js
/**
* performUnitOfWork 函数
* 执行当前Fiber节点的工作,并返回下一个工作单元
* @param {Object} fiber - 当前的Fiber节点
* @returns {Object|null} - 下一个需要处理的Fiber节点
*/
export function performUnitOfWork(fiber) {
// 1. 创建当前Fiber节点对应的DOM节点
if (!fiber.dom) {
fiber.dom = createDom(fiber);
}
// 2. 为当前Fiber节点的子元素创建Fiber节点
const elements = fiber.props.children;
reconcileChildren(fiber, elements);
// 3. 返回下一个工作单元
if (fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.parent;
}
}
export function reconcileChildren(wipFiber, elements) {
// TODO 调和变更
let index = 0;
let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
let prevSibling = null;
// TODO 思考:为什么不能用 oldFiber !== null
// oldFiber一直为undefined,会造成死循环
while (index < elements.length || oldFiber != null) {
const element = elements[index];
let newFiber = null;
const sameType = oldFiber && element && element.type == oldFiber.type;
// 更新
if (sameType) {
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: "UPDATE",
};
}
// 重新创建
if (element && !sameType) {
newFiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: "PLACEMENT",
};
}
// 删除
if (oldFiber && !sameType) {
oldFiber.effectTag = "DELETION";
deletions.push(oldFiber);
}
// 同时遍历旧fiber树
if (oldFiber) {
oldFiber = oldFiber.sibling;
}
// 父fiber的child指向第一个子fiber
if (index === 0) {
wipFiber.child = newFiber;
} else if (element) {
/* 当oldFiber != null时,需要判断element存在才设置sibling */
// 如果存在兄弟节点,通过sibling关联
prevSibling.sibling = newFiber;
}
// 暂存上一个兄弟节点
prevSibling = newFiber;
index++;
}
}
对于我们上面举的例子,它的遍历顺序是这样的。A->B->D->C->E。
2.4 commitRoot
让我们来恭喜自己,已经完成了最核心也是最困难的部分。只需要最后最简单的一步,将计算过后的fiber树提交,变成真实的dom渲染到界面上就好了。
scss
/**
* commitWork 函数
* 递归提交Fiber节点的DOM更新
* @param {Object} fiber - 当前的Fiber节点
*/
export function commitWork(fiber) {
if (!fiber) {
return;
}
const domParent = fiber.parent.dom; // 获取父DOM节点
if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
domParent.appendChild(fiber.dom);
}
if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
updateDom(fiber.dom, fiber.alternate.props, fiber.props);
}
if (fiber.effectTag === "DELETION") {
domParent.removeChild(fiber.dom);
return;
}
commitWork(fiber.child); // 递归提交子节点
commitWork(fiber.sibling); // 递归提交兄弟节点
}
const isEvent = (key) => key.startsWith("on");
const isProperty = (key) => key !== "children" && !isEvent(key);
const isNew = (prev, next) => (key) => prev[key] !== next[key];
const isGone = (next) => (key) => !(key in next);
export function updateDom(dom, prevProps, nextProps) {
// 移除旧事件
Object.keys(prevProps)
.filter(isEvent)
.filter((key) => !(key in nextProps) || isNew(prevProps, nextProps)(key))
.forEach((name) => {
const eventType = name.toLowerCase().substring(2);
dom.removeEventListener(eventType, prevProps[name]);
});
// 删除旧属性
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(nextProps))
.forEach((name) => {
dom[name] = "";
});
// 设置新属性
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))
.forEach((name) => {
dom[name] = nextProps[name];
});
// 添加新事件
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(prevProps, nextProps))
.forEach((name) => {
const eventType = name.toLowerCase().substring(2);
dom.addEventListener(eventType, nextProps[name]);
});
}
三,测试
大功告成,接下来我们做点小测试。
ini
const childrenElements = [
React.createElement("div", { key: "B" }, "B", React.createElement("div", { key: "D" }, "D")),
React.createElement("div", { key: "C" }, "C", React.createElement("div", { key: "E" }, "E")),
];
const element_0 = React.createElement("h1", "A", "A1", ...childrenElements);
const element_1 = React.createElement("h1", "A", "A2", ...childrenElements);
ReactDom.render(element_0, rootDOM);
setTimeout(() => {
ReactDom.render(element_1, rootDOM);
}, 1000);

四,后续内容
如果这里做完,晚上吃饭可以狠狠奖励一下自己了。大概的架子和核心的部分已经完成,接下来会依次添加函数式组件的支持,以及useState,useEffect。