自己造轮子系列——React框架fiber树实战应用
写在前面
前一篇我们讲React为了递归更新DOM树不阻塞主线程,而引入了并发模型,把整个递归渲染DOM操作拆分成若干个任务单元执行,这节我们就来讲讲React是怎么拆分任务单元的。
原理剖析
为了组织任务单元,我们需要学习一个新的数据结构——fiber
树。每个组件对应一个fiber
,而每个fiber节点对应一个任务单元。
为了加深理解,我们来看一个简单的例子:
Didact.render(
<div>
<h1>
<p />
<a />
</h1>
<h2 />
</div>,
container
)
发挥想象:在
render
函数中,我们将创建根fiber
并将其赋值给nextunitofwork
。剩下的任务在performUnitOfWork函数中执行。
实际上,我们将为每个fiber
做下面三件事:
- 将元素添加到DOM中
- 为元素的子元素创建Fiber
- 返回下一个任务单元
其实,fiber
这种数据结构的目标之一,就是方便查找下一个任务单元。所以每个fiber
节点都同时指向它的第一个子节点(child)、下一个兄弟节点(sibling)和它的父节点(parent)的。
而当一个fiber
节点任务单元完成时:
-
如果它有子节点,那子节点就是下一个执行单元。在我们的示例中,当div这个任务单元完成时,下一个任务执行单元就是H1节点。
-
如果一个
fiber
节点没有子节点,就会执行下一个兄弟节点的任务单元。示例中的p任务单元执行完成,a是下一个任务执行单元。 -
如果一个
fiber
节点没有子节点也没有下一个兄弟节点,则会指向父节点的兄弟节点(uncle节点),比如说上面示例中的a
任务单元的下一个任务单元是h2
任务单元。
如果父 parent
节点没有兄弟节点silbing
,就继续找父节点的父节点,直到该节点有兄弟节点sibling
,或者直到达到根节点。到达根节点意味着完成了整个树的 render
。
综上,每个fiber
节点都可以指向第一个子节点(child)、下一个兄弟节点(sibling)和它的父节点(parent),并且至少有其中一个指向:
代码实现
旧递归逻辑
在前面的章节中,我们讲了普通的递归来实现render
方法,具体代码如下:
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)
}
fiber改造
接下来我们应用前面原理剖析的理论知识,来把DOM递归渲染的render
方法改造成fiber
渲染的方式:
1. 将 render 中创建 DOM 节点的部分抽离为 creactDOM 函数;
/**
* createDom 创建 DOM 节点
* @param {fiber} fiber 节点
* @return {dom} dom 节点
*/
function createDom (fiber) {
// 如果是文本类型,创建空的文本节点,如果不是文本类型,按 type 类型创建节点
const dom = fiber.type === 'TEXT_ELEMENT'
? document.createTextNode("")
: document.createElement(fiber.type)
// isProperty 表示不是 children 的属性
const isProperty = key => key !== "children"
// 遍历 props,为 dom 添加属性
Object.keys(fiber.props)
.filter(isProperty)
.forEach(name => {
dom[name] = fiber.props[name]
})
// 返回 dom
return dom
}
在createDom
中,创建DOM节点,该函数接收一个fiber
作为参数,处理不同类型的节点,生成新的DOM结构,并返回该DOM节点。
2. 在 render 中设置第一个任务单元为 fiber 根节点;
fiber
根节点仅包含 children
属性,值为长度为1的fiber
数组。
// 下一个任务单元
let nextUnitOfWork = null
/**
* 将 fiber 添加至真实 DOM
* @param {element} fiber
* @param {container} 真实 DOM
*/
function render (element, container) {
nextUnitOfWork = {
dom: container,
props: {
children: [element]
}
}
}
3. 通过 requestIdleCallback 在浏览器空闲时,渲染 fiber;
/**
* workLoop 工作循环函数
* @param {deadline} 截止时间
*/
function workLoop(deadline) {
// 是否应该停止工作循环函数
let shouldYield = false
// 如果存在下一个任务单元,且没有优先级更高的其他工作时,循环执行
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
// 如果截止时间快到了,停止工作循环函数
shouldYield = deadline.timeRemaining() < 1
}
// 通知浏览器,空闲时间应该执行 workLoop
requestIdleCallback(workLoop)
}
// 通知浏览器,空闲时间应该执行 workLoop
requestIdleCallback(workLoop)
workLoop
类似任务栈,不停地处理一个又一个任务单元,并且添加了是否中止的判断,同时还有继续下一个任务单元的逻辑。这样可以有效地避免主线程被阻塞,这也是React采用fiber
数据结构的最重要的目的。
4. 渲染 fiber 的函数 performUnitOfWork;
performUnitOfWork
主要实现前面提到的三个功能:
- 添加 dom 节点
- 如果 fiber 没有 dom 节点,为它创建一个 dom 节点;
- 如果 fiber 有父节点,将 fiber.dom 添加至父节点;
- 新建 filber
- 遍历子节点;
- 创建 fiber;
- 将第一个子节点设置为 fiber 的子节点;
- 第一个之外的子节点设置为该节点的兄弟节点;
- 返回下一个任务单元(fiber)
- 如果有子节点,返回子节点;
- 如果有兄弟节点,返回兄弟节点;
- 否则继续走 while 循环,直到找到 root;
/**
* performUnitOfWork 处理任务单元
* @param {fiber} fiber
* @return {nextUnitOfWork} 下一个任务单元
*/
function performUnitOfWork(fiber) {
//----------------------1. 添加 dom 节点----------------------
// 如果 fiber 没有 dom 节点,为它创建一个 dom 节点
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
// 如果 fiber 有父节点,将 fiber.dom 添加至父节点
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)
}
//----------------------2 新建 filber----------------------
// 子节点
const elements = fiber.props.children
// 索引
let index = 0
// 上一个兄弟节点
let prevSibling = null
// 遍历子节点
while (index < elements.length) {
const element = elements[index]
// 创建 fiber
const newFiber = {
type: element.type,
props: element.props,
parent: fiber,
dom: null,
}
// 将第一个子节点设置为 fiber 的子节点
if (index === 0) {
fiber.child = newFiber
} else if (element) {
// 第一个之外的子节点设置为该节点的兄弟节点
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
//----------------------3 返回下一个任务单元(fiber)----------------------
// 如果有子节点,返回子节点
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
// 如果有兄弟节点,返回兄弟节点
if (nextFiber.sibling) {
return nextFiber.sibling
}
// 否则继续走 while 循环,直到找到 root。
nextFiber = nextFiber.parent
}
}
在performUnitOfWork
函数,我们接受一个fiber
作为入参,然后处理当前任务单元,并返回下一个任务单元。
以上我们实现了将 fiber
树渲染到页面的功能,并且渲染过程是可中断的。
写在后面
今天就到这里,fiber
的内容还是挺多的,由于代码内容比较多,功能集中,所以多以注释的方式,解释了实现细节,可以慢慢消化。
不过重要的是懂得原理,然后再学习下具体的实现细节。
转载自:https://juejin.cn/post/7249286903207804986