render & commit
分析
前面我们已经把 render 从递归修改成了 fiber 架构。
每次我们处理一个 fiber 时,我们都会向 DOM 添加一个新节点。也就是说,在我们完成渲染整棵树之前,浏览器随时会中断我们的工作。在这种情况下,用户将看到一个不完整的 UI。这对于用户体验是极不友好的。
因此,我们把 dom 添加到 parent 这部分内容单独提取出来,等工作单元执行结束时,再统一添加。这样浏览器就会一次性把所有 UI 绘制出来,而不是不完整的 UI。
然后,让我们整理下需要做的工作:
function render(element, container) {
// todo: 使用wipRoot保存根fiber wipRoot: workInProgressRoot
nextWorkUnit = {
dom: container,
props: { children: [element] }
};
}
// ...
function commitRoot() {
// todo: 将根fiber下的所有fiber的dom 递归 添加到其父fiber的dom中去
}
// ...
function workLoop(deadline) {
while (nextWorkUnit && deadline.timeRemaining() > 1) {
nextWorkUnit = performUnitOfWork(nextWorkUnit);
}
// todo: 当工作单元为空时,执行commitRoot
requestIdleCallback(workLoop);
}
实现
接下来,我们完善下代码:
function render(element, container) {
// 使用wipRoot保存根fiber wipRoot: workInProgressRoot
wipRoot = {
dom: container,
props: { children: [element] }
};
nextWorkUnit = wipRoot;
}
function commitRoot() {
commitWork(wipRoot.child);
wipRoot = null;
}
// 将fiber的dom 递归 添加到其父fiber的dom中去
function commitWork(fiber) {
if (!fiber) {
return;
}
const domParent = fiber.parent.dom;
domParent.appendChild(fiber.dom);
commitWork(fiber.child);
commitWork(fiber.sibling);
}
function workLoop(deadline) {
while (nextWorkUnit && deadline.timeRemaining() > 1) {
nextWorkUnit = performUnitOfWork(nextWorkUnit);
}
// 当工作单元为空时,执行commitRoot
if (!nextWorkUnit && wipRoot) {
commitRoot();
}
requestIdleCallback(workLoop);
}
我们再修改下 src/index.js 里的代码,把 jsx 写复杂一些。
import miniReact from './mini-react/react';
import miniReactDOM from './mini-react/react-dom';
/** @jsx miniReact.createElement */
const element = (
<div title='hello'>
<h1>hello</h1>
<p>hello world!</p>
</div>
);
const container = document.getElementById('root');
miniReactDOM.render(element, container);
不出意外,正常显示 hello 与 hello world!
疑问
这里,可能有人会疑问:前面因为递归渲染慢的问题,我们拆分成了一个个工作单元,怎么这里又递归添加 dom 了呢?
实际上,浏览器执行 createElement、appendChild 等函数是非常快的。比如下面这个例子:
const root = document.getElementById('root');
console.time('createElement');
for (let i = 0; i < 1000; i++) {
const node = document.createElement('div');
const textNode = document.createTextNode('');
textNode.nodeValue = 'hello';
node.appendChild(textNode);
root.appendChild(node);
}
console.timeEnd('createElement');
我们创建了 1000 个 div
节点,每个 div
节点里添加上文本节点,每个文本节点的 nodeValue
是'hello'字符串。其实就是模拟我们 commitRoot
的过程, 只不过这里多了 createElement、createTextNode 操作。
createElement: 2.30126953125 ms
结果仅用了 2.3ms。
因此,我们大可不必担心 commitRoot
会带来浏览器卡顿。真正耗时的是 React 的 render 操作。不过已经被我们拆分成一个个工作单元了。