为了组织工作单元,我们需要一个数据结构:fiber树。
每个元素都是一个 fiber,每个 fiber 都是一个工作单元。
让我们来看一个例子:
假设我们想要渲染一个这样的元素树
js
Didact.render(
<div>
<h1>
<p />
<a />
</h1>
<h2 />
</div>,
container
)
这张图下面会用到。
在渲染过程中,我们将创建一个 root fiber 对象,并将其赋值给 nextUnitOfWork。
剩下的工作将发生在 performUnitOfWork 函数上,我们将为每个 fiber 做三件事:
- 将元素添加到 DOM
- 为当前元素的每个孩子创建 fiber
- 选择下一个工作单元
这种结构的目标之一是便于查找下一个工作单元。这也是为什么每个 fiber 都有一个连接到它第一个 child、下一个 sibling 和它的 parent 的链接。(当然,没有的话链接指向 null)
child -> 孩子,sibling -> 兄弟姐妹, parent -> 父母。在这里不翻译这几个单词。因为在 fiber 的结构中有这三个 key。
当我们在一个 fiber 上完成了需要执行的工作,如果它还有一个 child,那么这个 child 就是下一个工作单元。在上面 Fiber Tree 的图中,当我们完成了对 div fiber 的处理时,下一个工作单元就成了 h1。
但如果当前 fiber 没有 child,我们会将它的 sibling 作为下一个工作单元。例如,p fiber 没有 child,因此我们将 a fiber 作为下一个工作单元。
如果这个 fiber 既没有 child,也没有 sibling,我们将会去寻找它的 uncle,例如图中的 a 和 h2。
如果这个 parent 没有 sibling,那我们就通过 parent 一直往上找,直到找到一个 sibling,或是找到最终的 root 。如果我们已经到达了 root,这意味着我们已经完成了这个渲染的所有工作。
整个过程其实就是进行深度优先遍历。
这也和我们古代的君位传承的方式一致,一是父死子继,二是兄终弟及。
现在让我们写一下代码。
首先让我们先从 render 函数里移除代码。
js
function createDOM(fiber) {
const dom =
element.type === "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(element.type);
// 将属性分配至生成的 DOM 节点
const isProperty = (key) => key !== "children";
Object.keys(element.props)
.filter(isProperty)
.forEach((key) => {
dom[key] = element.props[key];
});
return dom;
}
function render(element, container) {
// TODO set next unit of work
}
let nextUnitOfWork = null;
我们将创建 DOM 节点的部分保留在函数里,我们之后会使用它。
js
function render(element, container) {
// TODO set next unit of work
nextUnitOfWork = {
dom: container,
props: {
children: [element],
},
};
}
let nextUnitOfWork = null;
在 render 函数里,我们将 nextUnitOfWork 设为 fiber 树的根。
然后,当浏览器准备好的时候,它将调用我们的 workLoop,我们将开始处理 root。
js
let nextUnitOfWork = null;
function workLoop(deadline) {
let shouldYield = false;
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
shouldYield = deadline.timeRemaining() < 1;
}
requestIdleCallback(workLoop);
}
function performUnitOfWork(fiber) {
// TODO add dom node
// TODO create new fibers
// TODO return next unit of work
}
首先,我们先判断传入的工作单元(其实就是 createElement 函数返回的对象)是否有创建好的 DOM 节点,没有的话为它创建 DOM 节点(根 fiber 是有 DOM节点的,就是 render 函数传入的 container )。
然后判断当前 fiber 是否有 parent,有的话就将当前 fiber 中的 dom(刚才创建的),追加到( append )它父节点的 dom 中。(这一步就是在一步步地构建 dom 节点树)
js
function performUnitOfWork(fiber) {
// TODO add dom node
if (!fiber.dom) {
fiber.dom = createDOM(fiber);
}
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom);
}
// TODO create new fibers
// TODO return next unit of work
}
然后,对于每一个子元素(其实就是 createElement 函数返回的对象),我们都会创建一个新的 fiber。
具体的实现方式就是遍历当前 fiber 的子元素列表。
js
function performUnitOfWork(fiber) {
// TODO add dom node
if (!fiber.dom) {
fiber.dom = createDOM(fiber);
}
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom);
}
// TODO create new fibers
const elements = fiber.props.children;
let index = 0;
let prevSibling = null;
while (index < elements.length) {
const element = elements[index];
const newFiber = {
type: element.type,
props: element.props,
parent: fiber,
dom: null,
};
}
// TODO return next unit of work
}
此外,我们还需要将它添加到 fiber 树中,根据它是否是第一个孩子,将它设置为 child 或是 sibling。
在遍历的过程中,如果被遍历到的元素是第一个孩子,就将它设为传入 performUnitOfWork 的 fiber 的 child,如果不是,就将它设为长子的 sibling,并且还要将这个它存到提前声明的 prevSibling 中,这一步主要是为了设置 sibling。最后形成这样的 sibling 链:长子(数组第一个元素) -> 次子 -> ... -> 最小的孩子(数组最后一个元素)。
js
function performUnitOfWork(fiber) {
// TODO add dom node
if (!fiber.dom) {
fiber.dom = createDOM(fiber);
}
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom);
}
// TODO create new fibers
const elements = fiber.props.children;
let index = 0;
let prevSibling = null;
while (index < elements.length) {
const element = elements[index];
const newFiber = {
type: element.type,
props: element.props,
parent: fiber,
dom: null,
};
if (index === 0) {
fiber.child = newFiber;
} else {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
index++;
}
// TODO return next unit of work
}
那么最后一步就是返回下一个工作单元了。
按照我们之前所讲的,下一个工作单元的第一继承人就是嫡长子(第一个子元素)。如果没有孩子,那么就只能按照兄终弟及,让自己的弟弟成为下一个工作单元。如果既没有孩子也没有兄弟,那就只能让父亲的兄弟来做下一个工作单元了。
当 nextFiber 为空,也就是说 parent 指向为空的时候,树的回溯已经回溯到树根(root)了,也就表明此时整棵树的处理工作已经结束了。
js
function performUnitOfWork(fiber) {
// TODO add dom node
if (!fiber.dom) {
fiber.dom = createDOM(fiber);
}
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom);
}
// TODO create new fibers
const elements = fiber.props.children;
let index = 0;
let prevSibling = null;
while (index < elements.length) {
const element = elements[index];
const newFiber = {
type: element.type,
props: element.props,
parent: fiber,
dom: null,
};
if (index === 0) {
fiber.child = newFiber;
} else {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
index++;
}
// TODO return next unit of work
if (fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.parent;
}
}
完整代码
js
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map((child) =>
typeof child === "object" ? child : createTextElement(child)
),
},
};
}
function createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [],
},
};
}
function createDOM(fiber) {
const dom =
element.type === "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(element.type);
// 将属性分配至生成的 DOM 节点
const isProperty = (key) => key !== "children";
Object.keys(element.props)
.filter(isProperty)
.forEach((key) => {
dom[key] = element.props[key];
});
return dom;
}
function render(element, container) {
// TODO set next unit of work
nextUnitOfWork = {
dom: container,
props: {
children: [element],
},
};
}
let nextUnitOfWork = null;
function workLoop(deadline) {
let shouldYield = false;
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
shouldYield = deadline.timeRemaining() < 1;
}
requestIdleCallback(workLoop);
}
function performUnitOfWork(fiber) {
// TODO add dom node
if (!fiber.dom) {
fiber.dom = createDOM(fiber);
}
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom);
}
// TODO create new fibers
const elements = fiber.props.children;
let index = 0;
let prevSibling = null;
while (index < elements.length) {
const element = elements[index];
const newFiber = {
type: element.type,
props: element.props,
parent: fiber,
dom: null,
};
if (index === 0) {
fiber.child = newFiber;
} else {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
index++;
}
// TODO return next unit of work
if (fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.parent;
}
}
const Didact = {
createElement,
render,
};
/** @jsx Didact.createElement */
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
);
const container = document.getElementById("root");
Didact.render(element, container);
简单总结下,fiber 就是一个数据结构、一个 JavaScript 对象,它是由 createElement 函数的返回值扩充过来的。这个扩充的过程就是 fiber 树形成的过程,也是 dom 树形成的过程。在这个过程中,是以深度优先搜索的方式来寻找下一个工作单元。