不只是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 taskconsole.log(`taskId:${taskId} run task`);// domshouldYield = 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节点,创建DOMif (!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节点的属性{ <divclassName: '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,};// 第一个子节点设为childif (index === 0) {fiber.child = newFiber;} else {// 其他子节点设为上一个节点的siblingprevChild.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. 处理propsupdateProps(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;
相关文章:
不只是mini-react第二节:实现最简fiber
省流|总结 首先,我们编写JSX文件,并通过Babel等转换工具将其转化为createElement()函数的调用,最终生成虚拟 DOM(Vdom)格式。举个例子: // 原始 JSX const App <div>hi-mini-react</div>;//…...
python 使用Whisper模型进行语音翻译
目录 一、Whisper 是什么? 二、Whisper 的基本命令行用法 三、代码实践 四、是否保留Token标记 五、翻译长度问题 六、性能分析 一、Whisper 是什么? Whisper 是由 OpenAI 开源的一个自动语音识别(Automatic Speech Recognition, ASR)系统。它的主要特点是: 多语言…...
priority_queue的创建_结构体类型(重载小于运算符)c++
当优先级队列里面存的是一个自定义(结构体)类型,我们有两种方式,一个是用内置类型的方式,在priority_queue<>里写三个参数,比如int, vector<int>, less<int>,把int改成结构体…...
数据结构实战之线性表(一)
一.线性表的定义和特点 线性表的定义 线性表是一种数据结构,它包含了一系列具有相同特性的数据元素,数据元素之间存在着顺序关系。例如,26个英文字母的字符表 ( (A, B, C, ....., Z) ) 就是一个线性表,其中每个字母就是一个数据…...
Python学习之旅:进阶阶段(七)数据结构-计数器(collections.Counter)
在 Python 编程的进阶学习中,数据处理是一项重要的任务。collections.Counter作为 Python 标准库collections模块中的一员,为我们提供了一种高效且便捷的方式来统计数据出现的次数。接下来,就让我们一起深入了解这个强大的计数器。 一、什么是计数器 collections.Counter本…...
Spring Boot项目如何使用MyBatis实现分页查询及其相关原理
写在前面:大家好!我是晴空๓。如果博客中有不足或者的错误的地方欢迎在评论区或者私信我指正,感谢大家的不吝赐教。我的唯一博客更新地址是:https://ac-fun.blog.csdn.net/。非常感谢大家的支持。一起加油,冲鸭&#x…...
【项目初始化】
项目初始化 使用脚手架创建项目Vite创建项目推荐拓展 使用脚手架创建项目 Vite Vite 是一个现代的前端构建工具,它提供了极速的更新和开发体验,支持多种前端框架,如 Vue、React 等创建项目 pnpm create vuelatest推荐拓展...
LeetCode热题100(八)—— 438.找到字符串中所有字母异位词
LeetCode热题100(八)—— 438.找到字符串中所有字母异位词 题目描述代码实现思路解析 你好,我是杨十一,一名热爱健身的程序员在Coding的征程中,不断探索与成长LeetCode热题100——刷题记录(不定期更新&…...
26.Word:创新产品展示说明会【9】
目录 NO1.2.3 NO4.5.6.7 NO1.2.3 另存为/F12:考生文件夹点亮显示和隐藏标记选中→插入→表格→文字转化成表格→✔制表符→确定布局→自动调整→设计→随便一种保存至“表格”部件库:选中表格→插入→文档部件→使用“表格”部件库:插入→…...
python 之 zip 和 * 解包操作
文章目录 1. zip 函数语法:示例:特点:应用场景: 2. * 操作符语法:示例:应用场景: 3. zip 和 * 的结合使用示例:转置二维列表 4. zip 和 * 的其他用法示例 1:合并多个列表…...
反向代理模块jmh
1 概念 1.1 反向代理概念 反向代理是指以代理服务器来接收客户端的请求,然后将请求转发给内部网络上的服务器,将从服务器上得到的结果返回给客户端,此时代理服务器对外表现为一个反向代理服务器。 对于客户端来说,反向代理就相当…...
AI应用部署——streamlit
如何把项目部署到一个具有公网ip地址的服务器上,让他人看到? 可以利用 streamlit 的社区云免费部署 1、生成requirements.txt文件 终端输入pip freeze > requirements.txt即可 requirements.txt里既包括自己安装过的库,也包括这些库的…...
文明的基因:在传承中破茧重生
敦煌莫高窟的壁画历经千年风雨,至今仍在向世界讲述着东方美学的密码。那些斑驳的壁画上,既有北魏时期的天竺梵音,也留存着盛唐气象的长安余韵。文明的基因从未停止生长,就像莫高窟的壁画师们在临摹前朝壁画时,总会在衣…...
全国31省空间权重矩阵(地理相邻空间、公路铁路地理距离空间、经济空间)权重矩阵数据-社科数据
中国31个省份空间权重矩阵-社科数据https://download.csdn.net/download/paofuluolijiang/90028597 https://download.csdn.net/download/paofuluolijiang/90028597 空间权重矩阵是反映个体在空间中依赖关系的矩阵,本数据计算全国31个省三种标准化处理的空间权重矩…...
MySQL数据类型转换应注意什么?
文章目录 1. **隐式转换**2. **显式转换**3. **数据截断**4. **字符集与排序规则**5. **日期和时间转换**6. **数值转换**7. **NULL 处理**8. **性能影响**9. **错误处理**10. **函数选择**示例总结 在 MySQL 中进行数据类型转换时,需要注意以下几个关键点ÿ…...
前端开发之jsencrypt加密解密的使用方法和使用示例
目录 RSA密钥生成选项简介 jsencrypt 使用教程 一、安装 jsencrypt 二、使用 jsencrypt 进行加密和解密 1. 创建密钥对 2. 加密数据 3. 解密数据 三、实际应用示例 加密数据并存储到 localStorage 中: 从 localStorage 中读取加密数据并解密: …...
ESP32和STM32在处理中断方面的区别
为了通俗地讲解ESP32和STM32在处理中断方面的区别,我们可以把它们想象成两个不同的“智能管家”系统,各自负责管理一个家庭(即嵌入式项目)的各种任务。我们将重点放在如何处理突发事件(即中断)上。 ESP32 …...
98.1 AI量化开发:长文本AI金融智能体(Qwen-Long)对金融研报大批量处理与智能分析的实战应用
目录 0. 承前1. 简介1.1 通义千问(Qwen-Long)的长文本处理能力 2. 基础功能实现2.1 文件上传2.2 单文件分析2.3 多文件分析 3. 汇总代码&运行3.1 封装的工具函数3.2 主要功能特点3.3 使用示例3.4 首次运行3.5 运行结果展示 4. 注意事项4.1 文件要求4.2 错误处理机制4.3 最佳…...
PPT演示设置:插入音频同步切换播放时长计算
PPT中插入音频&同步切换&放时长计算 一、 插入音频及音频设置二、设置页面切换和音频同步三、播放时长计算 一、 插入音频及音频设置 1.插入音频:点击菜单栏插入-音频-选择PC上的音频(已存在的音频)或者录制音频(现场录制…...
链表的简单介绍
申明: 我们的链表可以写在类中或者接口中(接口中更好),这里我们是写在类当中。 1.节点的构造是由当前数据和指向下一个结点的地址组成,那么我们在当前这个链表的类中需要实现一个节点那么此时就需要用到内部类(当一个…...
Cocoa和Cocoa Touch是什么语言写成的?什么是Cocoa?编程语言中什么是框架?为什么苹果公司Cocoa类库有不少NS前缀?Swift编程语言?
Cocoa和Cocoa Touch是什么语言写成的? 二者主要都是用Objective-C语言编写而成的。 什么是Cocoa? Cocoa是苹果操作系统macOS和iOS上的应用程序开发框架集合,核心语言是Objective-C编程语言,在移动平台被称为Cocoa Touch,Cocoa包含多个子框架…...
AI-System 学习
《AI系统原理与架构》ZOMI https://github.com/chenzomi12/AISystem CPU、GPU、NPU 芯片基础 华为 Ascend 产品 NVLink的发展 & 结构 NVLink 拓扑、DGX 硬件渲染图...
基于聚类与相关性分析对马来西亚房价数据进行分析
碎碎念:由于最近太忙了,更新的比较慢,提前祝大家新春快乐,万事如意!本数据集的下载地址,读者可以自行下载。 1.项目背景 本项目旨在对马来西亚房地产市场进行初步的数据分析,探索各州的房产市…...
ARM嵌入式学习--第十一天(中断处理 , ADC)
--中断的概念 中断是指计算机运行过程中,出现某些意外情况需主机干预时,机器能自动停止正在运行的程序并转入处理新情况的程序,处理完毕后又返回被暂停的程序继续运行 --CPU处理事情的方式 -轮询方式 不断查询是否有事情需要处理,…...
消息队列篇--通信协议篇--网络通信模型(OSI7层参考模型,TCP/IP分层模型)
一、OSI参考模型(Open Systems Interconnection Model) OSI参考模型是一个用于描述和标准化网络通信功能的七层框架。它由国际标准化组织(ISO)提出,旨在为不同的网络设备和协议提供一个通用的语言和结构,以…...
“新月之智”智能战术头盔系统(CITHS)
新月人物传记:人物传记之新月篇-CSDN博客 相关文章链接(更新): 星际战争模拟系统:新月的编程之道-CSDN博客 新月智能护甲系统CMIA--未来战场的守护者-CSDN博客 目录 一、引言 二、智能头盔控制系统概述 三、系统架…...
Go Fx 框架使用指南:深入理解 Provide 和 Invoke 的区别
1. 什么是 Fx 框架? Fx 是一个基于 Go 语言的依赖注入框架,专注于简化应用程序的生命周期管理和依赖的构建。在复杂的应用程序中,Fx 通过模块化的设计方式将组件连接起来,使开发者能够更高效地管理依赖关系。 Fx 的核心理念是&a…...
实验七 JSP内置对象II
实验七 JSP内置对象II 目的: 1、掌握JSP内置对象的使用。 2、理解JSP的作用域 3、掌握session,application对象的使用 实验要求: 1、完成实验题目 2、要求提交实验报告,将代码和实验结果页面截图放入报告中 实验过程:…...
OpenCV:Harris、Shi-Tomasi角点检测
简述 在计算机视觉和图像处理领域,角点是一种重要的特征点,通常是图像中梯度变化剧烈的区域,例如建筑物的拐角、棋盘的交点等。角点检测广泛应用于目标跟踪、运动检测、拼接全景图 等任务。 本文将介绍 Harris 角点检测 和 Shi-Tomasi 角点…...
RK3568 opencv播放视频
文章目录 一、opencv相关视频播放类1. cv::VideoCapture 类主要构造方法:主要方法: 2. 视频播放基本流程代码示例: 3. 获取和设置视频属性4. 结合 FFmpeg 使用5. OpenCV 视频播放的局限性6. 结合 Qt 实现更高级的视频播放总结 二、QT中的代码…...
