不只是mini-react第二节:实现最简fiber
省流|总结
首先,我们编写JSX
文件,并通过Babel
等转换工具将其转化为createElement()
函数的调用,最终生成虚拟 DOM(Vdom)格式。举个例子:
// 原始 JSX
const App = <div>hi-mini-react</div>;
// Babel 编译后
const App = React.createElement(
"div",
null,
"hi-mini-react"
);
// createElement 返回的虚拟 DOM 结构
const App = {
type: "div",
props: {
children: ["hi-mini-react"]
}
};
接下来,将转换后的虚拟 DOM 格式传入render
函数,启动fiber
流程,调用代码如下:
ReactDOM.createRoot(document.querySelector("#root")).render(App);
这段代码的作用是将整个App
组件渲染到root
容器中。
然后,fiber
流程启动。render
函数接收传入的虚拟 DOM,并构建root fiber
节点,即fiber
树的根节点,也是第一个工作单元nextWorkOfUnit
。此时,fiber
树中仅包含根节点的真实 DOM 引用(容器),其他节点尚未创建。root fiber
节点具有如下特点:
- 它的
dom
属性指向容器节点 - 它的
props.children
包含整个应用的虚拟 DOM 结构 - 它是整个
fiber
树的起点
nextWorkOfUnit = {
dom: container, // 真实 DOM 元素
props: {
children: [el], // el 是传入的 App 虚拟 DOM
},
};
接下来,启用workLoop
任务调度系统。该系统通过requestIdleCallback
在浏览器空闲时执行任务。当检测到剩余时间不足(小于 1ms)时,系统会主动让出主线程,从而实现任务的分片处理。
每次workLoop
循环都会调用performWorkOfUnit
,确保每次只处理一个fiber
节点,实现任务的分片执行,这些任务可以随时中断与恢复。如果当前fiber
节点的真实 DOM 引用不存在,performWorkOfUnit
会在此fiber
节点上添加dom
引用,并将该fiber
节点指向父级fiber
节点,同时将其真实 DOM 引用传递给父级节点,形成如下结构:
//可视化展示
fiber节点 真实DOM节点
┌─────────┐ ┌─────────┐
│div1 fiber│ dom引用 │ <div> │
├─────────┤ ────────────► ├─────────┤
│parent │ │ │
│dom │ │ │
└─────────┘ └─────────┘
▲ ▲
│ │
│ parent │ append(h1)
│ │
┌─────────┐ ┌─────────┐
│h1 fiber │ dom引用 │ <h1> │
├─────────┤ ────────────► ├─────────┤
│parent │ │ │
│dom │ │ │
└─────────┘ └─────────┘
//具体代码
const dom = (fiber.dom = createDom(fiber.type));
fiber.parent.dom.append(dom);
接着,performWorkOfUnit
会调用两个函数:
updateProps
:读取fiber
节点中的虚拟 DOM 属性,并将其应用到对应的真实 DOM 上。initChildren
:将当前fiber
节点传入,通过深度优先遍历的方式,将当前树形结构的fiber
节点转化为链表结构。这样,大的fiber
节点被拆分成一个个小的fiber
节点,最终形成自上而下的树形结构。这种方式将整个渲染过程分割成一个个小任务,每个任务处理一个节点,从而实现任务的分片,避免了长任务阻塞主线程。
最后,遍历由initChildren
构建的链表结构。需要注意的是,这里并不是先遍历再执行,而是边遍历边执行。
最简任务调度器workLoop
(时间分片)
- 首次调用
requestIdleCallback(workLoop)
,浏览器会在空闲时执行workLoop
函数。 workLoop
函数会执行任务,并在每次执行任务时检查剩余空闲时间。- 如果空闲时间不足 1 毫秒(
timeRemaining() < 1
),shouldYield
会被设置为true
,从而暂停当前任务。 - 然后,
requestIdleCallback(workLoop)
会被调用来请求下一次空闲时间,从而在下次空闲时继续执行未完成的任务。 - 每次任务执行时,
taskId
都会递增,用于标记任务的顺序。
什么是下次空闲时?这里的下次空闲时是指当前可能有高优先级任务需要处理,那么浏览器会暂停当前相对低优先级的任务而去处理这个高优先级任务,等这个高优先级任务处理完成后再回过头来执行低优先级任务,这就是时间分片。
let taskId = 1;
function workLoop(deadline) {
taskId++;
let shouldYield = false;
while (!shouldYield) {
// run task
console.log(`taskId:${taskId} run task`);
// dom
shouldYield = deadline.timeRemaining() < 1;
}
requestIdleCallback(workLoop);// 再次调用
}
requestIdleCallback(workLoop);//首次调用
实现最简fiber
执行顺序:
jsx
→babel等工具编译
→createElement()
→render()
→workLoop()
→performWorkOfUnit()
→initChildren()
→树形结构转链表结构
1.通过createElement
创建虚拟DOM树
createElement
函数负责创建一个虚拟 DOM 对象。它的参数包括:type
(元素类型,如div
、span
等),props
(元素属性),children
(元素的子节点)。在 React 中,children
的处理方式非常重要,因为它决定了如何渲染子元素。createElement
会递归处理children
,如果children
是文本节点,则直接将其转换为文本节点。如果是其他 React 元素或组件,它会再次调用createElement
来生成对应的虚拟 DOM。
// 1. 首先通过createElement创建React元素
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map((child) => {
// 如果子元素是字符串,则创建文本节点
return typeof child === "string" ? createTextNode(child) : child;
}),
},
};
}
2.调用render函数,设置工作单元(fiber节点)nextWorkOfUnit
由前一节博客可以知道:
el
为编写的jsx
文件
container
为传入的真实dom
节点(在这里是root节点)
render
函数是 React 渲染流程的起点。它会将传入的虚拟 DOM(el
)渲染到指定的容器(container
)中。在这个过程中,React 会为每个虚拟 DOM 元素创建一个 fiber 对象,并构建一个 fiber 树。每个 fiber 节点包含对应的 DOM 元素、props
、children
等信息。这样 React 就能根据这个 fiber 树来管理和更新实际的 DOM。
// 2. 调用render函数开始渲染
function render(el, container) {
// 创建第一个工作单元(root fiber)
nextWorkOfUnit = {
dom: container,//传入的真实root节点
//虚拟dom元素
props: {
children: [el],
},
};
}
3.工作循环workLoop
启动
workLoop
是 React 渲染任务的调度器。它会根据浏览器的空闲时间进行任务调度,并决定什么时候更新哪些 fiber 节点。当浏览器空闲时,workLoop
会通过requestIdleCallback
触发任务执行。如果当前任务无法在空闲时间内完成,workLoop
会暂停任务,等待下一次空闲时间。通过这种异步调度方式,React 可以避免阻塞主线程,确保渲染操作的流畅性。
// 3. 程序启动时就开始监听空闲时间
let nextWorkOfUnit = null;
function workLoop(deadline) {
// deadline对象包含:
// timeRemaining(): 返回当前空闲期剩余的毫秒数
// didTimeout: 表示任务是否超时
let shouldYield = false;
while (!shouldYield && nextWorkOfUnit) {
// 1. 条件判断:
// - 是否需要让出主线程(shouldYield)
// - 是否还有工作要做(nextWorkOfUnit)
// 2. 处理当前工作单元并重新分配工作单元
nextWorkOfUnit = performWorkOfUnit(nextWorkOfUnit);
// 3. 检查是否需要让出主线程
shouldYield = deadline.timeRemaining() < 1;
}
// 4. 无论是否完成所有工作,都继续监听下一个空闲时间
requestIdleCallback(workLoop);
}
4.performWorkOfUnit
重新分配工作单元
performWorkOfUnit
是处理每个 fiber 节点的函数。在执行过程中,它会根据虚拟 DOM 的type
和props
更新实际 DOM。当 fiber 节点包含子节点时,performWorkOfUnit
会递归处理子节点。每处理完一个节点,performWorkOfUnit
会返回该节点的下一个待处理工作单元,从而继续构建 DOM 树。
function performWorkOfUnit(fiber) {
// 4 如果没有DOM节点,创建DOM
if (!fiber.dom) {
const dom = (fiber.dom = createDom(fiber.type));
fiber.parent.dom.append(dom);
//5 属性协调
updateProps(dom, fiber.props);
}
//---------------------------------------只关注前面即可
// 6 处理子节点,构建fiber树
initChildren(fiber)
// 7 返回下一个工作单元,按照以下优先级:
// 先找子节点
if (fiber.child) {
return fiber.child;
}
// 没有子节点找兄弟节点
if (fiber.sibling) {
return fiber.sibling;
}
// 都没有就回到父节点的兄弟节点
return fiber.parent?.sibling;
}
5.updateProps
属性协调
updateProps
函数的核心目的是将 React
元素的属性(props
)设置到实际的 DOM
节点上,而这里传入的dom
实际上就是fiber.dom
function updateProps(dom, props) {
Object.keys(props).forEach((key) => {
if (key !== "children") {
dom[key] = props[key];
}
});
}
举个简单的例子:
// 假设我们写了这样一个React元素
<div className="box" id="main">Hello</div>
// React会将其转换为这样的对象
{
type: 'div',
props: {
className: 'box',
id: 'main',
children: ['Hello']
}
}
// 然后在performWorkOfUnit中:
// 1. 首先创建DOM节点
const dom = document.createElement('div') // <div></div>
// 2. 调用updateProps设置属性
updateProps(dom, props)
// 结果:<div class="box" id="main"></div>
可视化展示:
React元素的属性 → 真实DOM节点的属性
{ <div
className: 'box' → class="box"
id: 'main' → id="main"
children: [...] >
} </div>
React属性世界 DOM属性世界
┌──────────┐ ┌──────────┐
│ props │ ═══> │ DOM │
└──────────┘ └──────────┘
│ │
className: 'box' class="box"
onClick: fn addEventListener
6.initChildren
子节点处理
initChildren
函数的任务是初始化虚拟 DOM 中的子节点。它会遍历子节点,并为每个子节点创建一个新的 fiber 节点。每个新创建的 fiber 节点将被添加到父节点的children
数组中,从而形成树形结构。React 会继续递归处理每个子节点,直到所有子节点都被处理为止。
function initChildren(fiber) {
const children = fiber.props.children;
let prevChild = null;
// 遍历所有子节点,建立fiber链接关系
children.forEach((child, index) => {
const newFiber = {
type: child.type,
props: child.props,
child: null,//子级
parent: fiber,//父级
sibling: null,//兄弟级
dom: null,
};
// 第一个子节点设为child
if (index === 0) {
fiber.child = newFiber;
} else {
// 其他子节点设为上一个节点的sibling
prevChild.sibling = newFiber;
}
prevChild = newFiber;
});
}
7.performWorkOfUnit
将树形结构通过深度遍历转化为链表结构
在performWorkOfUnit
函数中,React 通过深度优先遍历将树形结构转化为链表结构。这种链表结构可以帮助 React 更高效地遍历并更新每个节点。每个 fiber 节点不仅包含当前节点的信息,还持有对父节点、兄弟节点的引用,这使得 React 可以灵活地在渲染过程中调整工作单元。
function performWorkOfUnit(fiber) {
// ... 前面的代码省略 ...
// 这三行代码决定了下一个工作单元,实际上就是在构建链表
if (fiber.child) {
return fiber.child; // 1️⃣ 优先返回子节点
}
if (fiber.sibling) {
return fiber.sibling; // 2️⃣ 没有子节点就返回兄弟节点
}
return fiber.parent?.sibling; // 3️⃣ 都没有就返回父节点的兄弟节点
}
源代码
// 创建文本节点
function createTextNode(text) {
console.log("heiheihei!!!!!!!");
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [],
},
};
}
// 创建React元素
// type: 元素类型
// props: 元素属性
// children: 子元素
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map((child) => {
return typeof child === "string" ? createTextNode(child) : child;
}),
},
};
}
// 渲染函数:将React元素渲染到容器中
function render(el, container) {
nextWorkOfUnit = {
dom: container,
props: {
children: [el],
},
};
}
// 下一个工作单元
let nextWorkOfUnit = null;
// 工作循环:利用浏览器空闲时间处理任务
function workLoop(deadline) {
let shouldYield = false;
while (!shouldYield && nextWorkOfUnit) {
nextWorkOfUnit = performWorkOfUnit(nextWorkOfUnit);
// 当剩余时间小于1ms时,让出主线程
shouldYield = deadline.timeRemaining() < 1;
}
requestIdleCallback(workLoop);
}
// 根据类型创建DOM节点
function createDom(type) {
return type === "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(type);
}
// 更新DOM节点的属性
function updateProps(dom, props) {
Object.keys(props).forEach((key) => {
if (key !== "children") {
dom[key] = props[key];
}
});
}
// 初始化fiber节点的子节点
// 构建fiber树的链表结构:child(第一个子节点)和sibling(兄弟节点)
function initChildren(fiber) {
const children = fiber.props.children;
let prevChild = null;
children.forEach((child, index) => {
const newFiber = {
type: child.type,
props: child.props,
child: null,
parent: fiber,
sibling: null,
dom: null,
};
if (index === 0) {
fiber.child = newFiber;
} else {
prevChild.sibling = newFiber;
}
prevChild = newFiber;
});
}
// 处理工作单元
// 主要完成三件事:
// 1. 创建DOM节点
// 2. 处理props
// 3. 构建fiber树
// 4. 返回下一个工作单元
function performWorkOfUnit(fiber) {
// 1. 创建DOM节点,保证一次只处理一个fiber节点(任务分片机制)
if (!fiber.dom) {
const dom = (fiber.dom = createDom(fiber.type));
// 将DOM节点添加到父节点
fiber.parent.dom.append(dom);
// 2. 处理props
updateProps(dom, fiber.props);
}
// 3. 构建fiber树
initChildren(fiber)
// 4. 返回下一个要执行的任务
// 遍历顺序:先子节点,然后兄弟节点,最后回到父节点的兄弟节点
if (fiber.child) {
return fiber.child;
}
if (fiber.sibling) {
return fiber.sibling;
}
return fiber.parent?.sibling;
}
// 启动工作循环
requestIdleCallback(workLoop);
// React对象
const React = {
render,
createElement,
};
export default React;