不只是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(时间分片)

  1. 首次调用requestIdleCallback(workLoop),浏览器会在空闲时执行workLoop函数。
  2. workLoop函数会执行任务,并在每次执行任务时检查剩余空闲时间。
  3. 如果空闲时间不足 1 毫秒(timeRemaining() < 1),shouldYield会被设置为true,从而暂停当前任务。
  4. 然后,requestIdleCallback(workLoop)会被调用来请求下一次空闲时间,从而在下次空闲时继续执行未完成的任务。
  5. 每次任务执行时,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

执行顺序:

jsxbabel等工具编译createElement()render()workLoop()performWorkOfUnit()initChildren()树形结构转链表结构

1.通过createElement创建虚拟DOM树

createElement函数负责创建一个虚拟 DOM 对象。它的参数包括:type(元素类型,如divspan等),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 元素、propschildren等信息。这样 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 的typeprops更新实际 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;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/893618.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MongoDB实训:电子商务日志存储任务

一、实验目的 1. 理解如何通过Java API连接MongoDB数据库。 2. 学习在Java中使用MongoDB进行数据库操作&#xff0c;包括插入数据、查询数据以及数据统计等。 3. 掌握电子商务日志数据在MongoDB中的存储和操作方法。 二、实验环境准备 1. JAVA环境准备&#xff1a;确保…

计算机网络 (59)无线个人区域网WPAN

前言 无线个人区域网&#xff08;WPAN&#xff0c;Wireless Personal Area Network&#xff09;是一种以个人为中心&#xff0c;采用无线连接方式的个人局域网。 一、定义与特点 定义&#xff1a;WPAN是以个人为中心&#xff0c;实现活动半径小、业务类型丰富、面向特定群体的无…

从spec到iso的koji使用

了解一下Linux发行版流程&#xff1a;:从spec到iso的koji使用 for Fedora 41。 Fedora 41有24235个包&#xff0c;我们选择 minimal 的几十个源码包&#xff0c;百多个rpm包构建。 配3台服务器 40C64G 44C64G 80C128G&#xff0c;有点大材小用&#xff0c;一台就够了 &#xf…

20250124-注意力机制(5-7)【3/3完结】 ——已复现

Attention Is All You Need&#xff08;注意力就是你所需要的一切&#xff09;&#xff08;5-7&#xff09;【3/3完结】 ——已复现 20250124-注意力机制&#xff08;1-2&#xff09;【1/3】 ——已复现-CSDN博客 20250124-注意力机制&#xff08;3-4&#xff09;【2/3】 ——已…

22_解析XML配置文件_List列表

解析XML文件 需要先 1.【加载XML文件】 而 【加载XML】文件有两种方式 【第一种 —— 使用Unity资源系统加载文件】 TextAsset xml Resources.Load<TextAsset>(filePath); XmlDocument doc new XmlDocument(); doc.LoadXml(xml.text); 【第二种 —— 在C#文件IO…

[JavaScript] ES6及以后版本的新特性

文章目录 箭头函数&#xff08;Arrow Functions&#xff09;为什么需要箭头函数&#xff1f;箭头函数的完整语法箭头函数中的 this实用场景 解构赋值&#xff08;Destructuring Assignment&#xff09;为什么需要解构赋值&#xff1f;数组解构赋值的完整用法对象解构赋值的完整…

C语言进阶——3字符函数和字符串函数(2)

8 strsrt char * strstr ( const char *str1, const char * str2);查找子字符串 返回指向 str1 中第一次出现的 str2 的指针&#xff0c;如果 str2 不是 str1 的一部分&#xff0c;则返回 null 指针。匹配过程不包括终止 null 字符&#xff0c;但会在此处停止。 8.1 库函数s…

ThinkPHP 8请求处理-获取请求对象与请求上下文

【图书介绍】《ThinkPHP 8高效构建Web应用》-CSDN博客 《2025新书 ThinkPHP 8高效构建Web应用 编程与应用开发丛书 夏磊 清华大学出版社教材书籍 9787302678236 ThinkPHP 8高效构建Web应用》【摘要 书评 试读】- 京东图书 使用Composer初始化ThinkPHP 8应用_thinkphp8 compos…

飞行器半实物联合仿真:技术解析与应用实践

1.背景介绍 当前&#xff0c;飞行器已成为大国博弈复杂场景中的重要角色&#xff0c;其技术经过多次实践不断发展&#xff0c;性能持续提升&#xff0c;整体效能显著增强。随着计算机技术和系统仿真技术的发展&#xff0c;利用计算机模拟和仿真构造一个虚拟飞行器的飞行控制系…

c#配置config文件

1&#xff0c;引用命名空间 Configuration 及配置信息

【机器学习】机器学习引领数学难题攻克:迈向未知数学领域的新突破

我的个人主页 我的领域&#xff1a;人工智能篇&#xff0c;希望能帮助到大家&#xff01;&#xff01;&#xff01;&#x1f44d;点赞 收藏❤ 一、引言 在数学的浩瀚领域中&#xff0c;存在着诸多长期未解的难题&#xff0c;这些难题犹如高耸的山峰&#xff0c;吸引着无数数…

OS Copilot功能测评:智能助手的炫彩魔法

简介&#xff1a; OS Copilot 是一款融合了人工智能技术的智能助手&#xff0c;专为Linux系统设计&#xff0c;旨在提升系统管理和运维效率。本文详细介绍了在阿里云ECS实例上安装和体验OS Copilot的过程&#xff0c;重点评测了其三个核心参数&#xff1a;-t&#xff08;模式…

计算机网络 (55)流失存储音频/视频

一、定义与特点 定义&#xff1a;流式存储音频/视频是指经过压缩并存储在服务器上的多媒体文件&#xff0c;客户端可以通过互联网边下载边播放这些文件&#xff0c;也称为音频/视频点播。 特点&#xff1a; 边下载边播放&#xff1a;用户无需等待整个文件下载完成即可开始播放…

Oracle存储过程语法详解

简介 存储过程是一系列SQL语句的集合&#xff0c;可以封装复杂的逻辑&#xff0c;实现特定的功能&#xff0c;可以提高执行速度和代码的复用性&#xff0c;预先编译后存储在数据库中&#xff0c;可以通过指定存储过程的名称对其进行调用。 本文主要讲解Oracle存储过程语法&am…

推箱子游戏

java小游戏2 一游戏介绍 二图像准备 墙、箱子、人、箱子目的地&#xff0c;人左边、人右边、人上边、人下边 三结构准备 地图是什么&#xff0c;我们把地图想象成一个网格&#xff0c;每个格子就是工人每次移动的步长&#xff0c;也是箱子移动的距离&#xff0c;设置一个二维数…

如何分辨ddos攻击和cc攻击?

DDoS&#xff08;分布式拒绝服务&#xff09;攻击和 CC&#xff08;Challenge Collapsar&#xff09;攻击都属于网络攻击手段&#xff0c;主要通过消耗目标服务器资源使其无法正常提供服务&#xff0c;但它们在攻击原理、攻击特征等方面存在区别&#xff1a; 攻击原理 DDoS 攻…

期权帮|如何利用股指期货进行对冲套利?

锦鲤三三每日分享期权知识&#xff0c;帮助期权新手及时有效地掌握即市趋势与新资讯&#xff01; 如何利用股指期货进行对冲套利&#xff1f; 对冲就是通过股指期货来平衡投资组合的风险。它分为正向与反向两种策略&#xff1a; &#xff08;1&#xff09;正向对冲&#xff…

软件质量与测试报告5-压力测试 JMeter 与 Badboy

A&#xff0e;百度搜索引擎压力测试 通过在Badboy下执行如下的测试场景来生成压力测试的脚本&#xff1a; a) 在Badboy的地址栏里面输入www.baidu.com&#xff0c;回车&#xff1b; b) 在右下区域打开的百度的主页上输入搜索关键字JMeter&#xff0c;回车&#xff1b; c) 在…

Mybatis多条件查询:Map传参与对象传参解析

Mybatis 多条件查询常见且关键&#xff0c;本文探讨两种方法——Map 传参和 Java Bean 对象传参&#xff0c;展示用法及区别&#xff0c;总结应用场景和优缺点。 1. Map传参方式 原理&#xff1a;Mybatis允许我们通过一个Map对象来传递动态SQL中的参数。Map的键对应于SQL语句中…

wangEditor富文本编辑器,Laravel上传图片配置和使用

文章目录 前言步骤1. 构造好前端模版2. 搭建后端存储3. 调试 前言 由于最近写项目需要使用富文本编辑器&#xff0c;使用的是VUE3.0版本所以很多不兼容&#xff0c;实际测试以后推荐使用wangEditor 步骤 构造好前端模版搭建后端存储调试 1. 构造好前端模版 安装模版 模版安…