当前位置: 首页 > article >正文

不只是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 task
    console.log(`taskId:${taskId} run task`);
    // dom

    shouldYield = 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节点,创建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;

http://www.kler.cn/a/517403.html

相关文章:

  • 9【如何面对他人学习和生活中的刁难】
  • 将Deepseek接入本地Vscode
  • CASAIM与友达光电达成深度合作,CASAIM IS自动化蓝光测量技术为创新显示技术发展注入新的活力
  • 批量创建ES索引
  • react antd点击table单元格文字下载指定的excel路径
  • 「 机器人 」扑翼飞行器的数据驱动建模核心方法
  • 状态模式
  • OpenAI模块重构
  • 43 继承
  • 【统计的思想】假设检验(二)
  • 对神经网络基础的理解
  • MATLAB支持的概率分布
  • Hive 知识点八股文记录 ——(三)区别和原理
  • Unity自学之旅05
  • mysql-023.增删查改进阶-表的设计,查询进阶
  • (算法竞赛)DFS深搜4——迷宫第一条路问题解析与代码实现
  • 2025数学建模美赛|赛题评析|难度对比|选题建议
  • SpringBoot开发(二)Spring Boot项目构建、Bootstrap基础知识
  • Linux主机密钥验证失败的解决方法
  • YOLOv5训练自己的数据及rknn部署
  • vscode下poetry管理项目的debug配置
  • 本地大模型编程实战(01)实现翻译功能
  • 详细介绍:持续集成与持续部署(CI/CD)技术细节(关键实践、CI/CD管道、优势与挑战)
  • leetcode 3090. 每个字符最多出现两次的最长子字符串
  • 深度学习-96-大语言模型LLM之基于langchain的ConversationBufferMemory缓冲记忆
  • 2025年数学建模美赛 A题分析(3)楼梯使用方向偏好模型