原文链接:Build your own React
- Step I: The createElement Function ✅
- Step II: The render Function ✅
- Step III: Concurrent Mode 并发模式 ✅
- Step IV: Fibers ✅
- Step V: Render and Commit Phases
- Step VI: Reconciliation
- Step VII: Function Components
- Step VIII: Hooks
** To avoid confusion, I’ll use “element” to refer to React elements and “node” for DOM elements.*
前言
上文中我们完成了基本的createElement以及render函数,但是还遗留了一些问题,这篇文章将会给出解决方案。
Concurrent Mode(并发模式)
在我们继续添加功能之前,我们需要对已有的代码进行下重构。问题出在render函数中递归渲染的方式,一旦我们开始rendering,直到整棵树渲染成功后才会停止,如果DOM树比较大,可能会阻塞主线程太久。如果浏览器需要处理高优先级的事情,比如处理用户输入或者保持动画流畅,那么主线程必须要等到渲染完成才可以处理其他事件。
所以我们要把工作分解成一个个的小部分,在完成每个小部分后,如果有其他事情不得不处理,我们会让浏览器中断渲染。这里我们主要会使用requestIdleCallback来重构我们的代码。关于此api,可以在我的另一篇文章查看(强烈建议先阅读下,否则理解下面的代码会有些费力)。
重构后需添加如下代码:
// react/render.js // 待处理的任务 let nextUnitOfWork = null; function workLoop(deadline) { // 剩余时间是否足够执行任务的标识 let shouldYield = false; while (nextUnitOfWork && !shouldYield) { // performUnitOfWork处理当前任务并返回下一个待执行的任务 nextUnitOfWork = performUnitOfWork(nextUnitOfWork); shouldYield = deadline.timeRemaining() < 1; } // 本次没有足够空闲时间 请求下一次浏览器空闲的时候执行 requestIdleCallback(workLoop); } // 初始化执行workLoop requestIdleCallback(workLoop); function performUnitOfWork(nextUnitOfWork) { // TODO }
接下来我们需要初始化nextUnitOfWork,并且补充performUnitOfWork函数的逻辑,此函数不仅要负责处理当前任务,还需要返回下一个待处理的任务,如果有的话。
Fiber
终于迎来了React中一个十分重要的概念,如果你已经阅读了上面提及的文章初识Fiber以及RIC,那么此节对你来说很轻松就可以理解。
我们将会为每一个元素创建其对应的fiber,每一个fiber就是一个小的工作单元,举例说明:
如果我们想渲染如下DOM:
render( <div> <h1> <p /> <a /> </h1> <h2 /> </div>, container )首先需要在render函数中初始化nextUnitOfwork,剩余的工作将发生在上节提到的performUnitOfWork函数中,此函数会对每个fiber做三件事:
- 添加元素至DOM中
- 为每个子元素创建fiber
- 选择下一个工作单元并返回
逐步构建出如下的Fiber树:
show code
首先将render函数中的代码分离至createDOM函数中:
// react/render.js
function createDOM(fiber) {
// 创建元素
const dom =
fiber.type === "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(fiber.type);
// 赋予属性
Object.keys(fiber.props)
.filter((key) => key !== "children")
.forEach((name) => (dom[name] = fiber.props[name]));
return dom;
}
function render(element, container) {
// 初始化下一个工作单元(根节点)
nextUnitOfWork = {
dom: container,
props: {
children: [element],
},
sibling: null,
child: null,
parent: null,
};
}
当浏览器就绪时,将会调用我们的workLoop函数并在初始化的工作单元(根节点)上工作。
接下来补充performUnitOfWork函数:
function performUnitOfWork(fiber) {
// debugger
// 生成DOM
if (!fiber.dom) {
fiber.dom = createDOM(fiber);
}
// 如果当前节点存在父节点,将其挂载到父节点下
if (fiber.parent) {
fiber.parent.dom.append(fiber.dom);
}
const elements = fiber.props.children;
// 记录上一个fiber
let prevSibling = null;
// 为每个子节点创建fiber
for (let i = 0; i < elements.length; i++) {
const newFiber = {
// 记录元素标签类型
type: elements[i].type,
// 记录元素属性
props: elements[i].props,
// 与父节点创建链接
parent: fiber,
// 此时DOM还未生成 初始化null
// 下一次进入performUnitOfWork时
// 通过createDOM函数生成
dom: null,
// 与第一个子节点关联 初始化null
child: null,
// 与兄弟节点创建链接 初始化null
sibling: null,
};
if (i === 0) {
// 与第一个子节点成功关联
fiber.child = newFiber;
} else {
/**
* 根据fiber的架构设计,只有第一个子节点关联至child属性
* 其他子节点将关联到第一个子节点的兄弟节点 通过fiber.child.sibling访问到
* 此处的prevSibling实际上就是上一个子节点生成的fiber
* 这段代码也可以理解为 fiber.child.sibling = newFiber
* (此处有些不易理解,涉及到js引用类型的浅拷贝,大家可以多想一想)
*/
prevSibling.sibling = newFiber;
}
// 保存当前fiber
prevSibling = newFiber;
}
// 按照Fiber架构渲染顺序
// 有子节点 优先处理子节点
if (fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while (nextFiber) {
// 没有子节点的时候 优先处理兄弟节点
if (nextFiber.sibling) {
return nextFiber.sibling;
}
// 当此层的兄弟节点也处理完时
// 返回其父节点 继续处理其父节点所在层的兄弟节点
nextFiber = nextFiber.parent;
}
}
tips:第一次看这段代码可能会有些难以理解,建议大家一定要动手debugger下(在performUnitOfWork函数第一行debugger),单步调试观察下每次是如何处理fiber,如何生成,如何渲染,如何返回下一次工作单元。
总结
每篇文章都不会过长,从这篇文章开始会没那么容易理解,希望大家可以亲身动手尝试下。当然目前这段代码设计还是存在问题的,我们每次处理一个元素时,都会向DOM添加一个新节点,但是记住,浏览器可能会在我们完成渲染整个树之前中断我们的工作。在这种情况下,用户将看到一个不完整的 UI。这个问题将在下个文章解决。