极简的React 实现一

处理JSX

JSX优点

JSX 本质是 JavaScript 的语法糖,让写 UI 更自然。

我们知道,JSX并不是一种全新的语言,而是允许你在 JavaScript 代码中直接书写类似 HTML 的标签。

而这种设计消除了传统开发中"在 JS 里拼接 HTML 字符串"或"频繁调用 DOM API"的繁琐,让你能用最熟悉的标签语法来构建界面,大大降低了程序员的负担。

提供了直观且声明式的开发体验。

传统的命令式编程需要你一步步告诉浏览器"创建这个元素"、"添加那个属性"、"把它插到那里"。

而 JSX 是声明式的,你只需要描述"界面最终应该长什么样"。

代码的结构直接对应了 DOM 的树形结构,让你一眼就能看出最终的渲染结果,极大地提升了代码的可读性和可维护性。

动态数据的渲染变得极其简洁。

在 JSX 中嵌入动态内容只需一对花括号 {}。无论是渲染列表(user.map)、显示变量(${user.name})还是执行条件判断,都像是在写普通的 JavaScript 代码一样自然。这种无缝融合避免了传统模板引擎中特殊的语法指令,让开发者能完全专注于业务逻辑本身。

源码的第一阶段:The CreateElement Function

意识到 JSX 只是表象,真正的起点是 Babel 的转译。

首先先来看看这个命令:

bash 复制代码
pnpm i @babel/core @babel/preset-react @babel/cli

浏览器原生只认识标准的 JavaScript(ES5/ES6+),

完全看不懂 JSX 语法(即 <div> Hello </div> 这种写在 JS 里的 HTML 标签)。如果直接运行,浏览器会报错 SyntaxError。

Babel 的作用:它在代码运行前(编译阶段),扫描你的代码,把所有 JSX 标签"翻译"成普通的 JavaScript 函数调用

bash 复制代码
@babel/preset-react

Babel 核心本身不知道如何翻译 JSX,它需要插件。这个包提供了一套标准的 React 翻译规则。 当我写下 <div>Hello</div> 这样的 JSX 语法时,浏览器其实是无法直接理解的。

在代码运行前,Babel 已经默默工作,将其"翻译"成了标准的 JavaScript 函数调用:React.createElement('div', null, 'Hello')。这一步让我明白,JSX 本质上就是 createElement 函数的语法糖,而我的 Mini React 核心任务之一,就是实现这个函数。

接着,我们定义了 createElement 函数的职责:接收参数并构建虚拟节点。

这个函数非常简单却至关重要,它主要接收三个参数:

  1. type :代表元素的类型,可以是字符串(如 'div'),也可以是一个组件函数。
  2. props :包含所有的属性配置(如 className, id 等)。
  3. children :元素内部嵌套的子节点。
    任务是将这些参数打包,返回一个描述 UI 结构的普通 JavaScript 对象,也就是我们常说的 VDOM (Virtual DOM) Element

递归逻辑,确保所有节点(包括文本)格式统一。

在构建 VDOM 树时,我发现子节点可能是复杂的组件,也可能是简单的文本字符串。为了后续渲染(Render)和对比(Diff)算法的统一处理,我必须对它们进行标准化:

  • 如果子节点是对象,说明它已经是 VDOM 节点,直接使用。
  • 如果子节点是字符串或数字(叶子节点),我需要手动将其包装成一个特殊的 VDOM 对象,通常标记为 TEXT_ELEMENT
    通过这种递归处理,无论我的组件树有多深、多复杂,最终得到的都是一棵结构完全一致的纯 JSON 树。

最终,我们得到了一个标准化的 VDOM 对象结构。

经过上述处理,createElement 返回的对象长这样:经过上述处理,createElement 返回的对象长这样:

javascript 复制代码
{
  type: 'TEXT_ELEMENT' | 'div' | ComponentFunction, // 节点类型
  props: {
    className: '...', // 原始属性
    children: [       // 统一处理后的子节点数组
      { 
      type: 'TEXT_ELEMENT', 
      props: { nodeValue: 'Hello', children: [] 
      } 
    \},
      // ...其他子节点
    ]
  }
}

开发者只需关心数据和业务逻辑,而繁琐的 DOM 创建、属性挂载、以及未来可能发生的重绘重排优化,都将被 React(或我的 Mini React)基于这棵 VDOM 树自动接管。这就是声明式编程的魅力所在。

源码的第二阶段:The Render Function

意识到 VDOM 只是蓝图,真正的落地是真实 DOM 的构建与挂载。

首先,让我们明确 render 函数的使命:它接收上一阶段生成的 VDOM 对象 和一个真实的 DOM 容器(container)

浏览器虽然能执行 JS,但它无法直接"显示"一个 JavaScript 对象。如果只停留在 VDOM 层面,用户看到的将是一片空白。

render 的作用:它在代码运行期(执行阶段),遍历 VDOM 树,将抽象的 JSON 节点"翻译"成浏览器能理解的真实 DOM 节点,并最终插入到页面中。

javascript 复制代码
function render(element, container) {
  // 开始构建真实世界
}

接着,我们定义了节点创建的逻辑:区分元素节点与文本节点。

这个判断至关重要,因为浏览器 API 对两者的创建方式完全不同:

  1. 元素节点(Element Node) :如果 element.type 不是 'TEXT_ELEMENT'(例如 'div', 'h1'),我们需要调用 document.createElement(element.type)
  2. 文本节点(Text Node) :如果 element.type'TEXT_ELEMENT',说明这是一个叶子节点,我们需要调用 document.createTextNode('')

通过这一步,我们将 VDOM 中的 type 属性映射成了真实的 DOM 实例。

属性过滤逻辑,确保 children 不被误当作 DOM 属性处理。

在将 VDOM 的 props 应用到真实 DOM 时,我发现 props 对象里混杂了普通属性(如 className, style)和子节点(children)。

  • 核心规则 :除了 children 以外,props 里的其他键值对都应该被当作 DOM 的属性处理。
  • 为什么? 因为 DOM API 中没有 setAttribute('children', ...) 这种方法。children 需要通过递归渲染,作为子节点挂载到父节点内部,而不是作为属性挂在标签上。
javascript 复制代码
function render(element,container){
    //console.log(element,container);
    const dom = 
    element.type === 'TEXT_ELEMENT'
    ? document.createTextNode('')
    : document.createElement(element.type);
    // 处理 props
    const isProperty = key => key !== 'children';
    Object.keys(element.props)
    .filter(isProperty)
    .forEach(name =>{
        console.log(name,'///');
        dom[name] = element.props[name];
    })
    element.props.children.forEach(child => render(child,dom));
    container.appendChild(dom);
}

属性添加与事件绑定,赋予节点生命。

遍历过滤后的属性,将它们一一"画"到真实 DOM 上:

  • 普通属性 :直接赋值,如 dom.className = valuedom.id = value
  • 特殊处理 :对于 style 对象,需要遍历其内部键值对设置 dom.style[key];对于以 on 开头的事件(如 onClick),则需要使用 addEventListener 进行绑定,而不是直接赋值。

这一步让原本苍白的 DOM 节点拥有了样式、ID 和交互能力。

递归挂载,完成从虚拟到现实的最后一公里。

这是 render 函数最精妙的地方。处理完当前节点的属性和类型后,我们需要处理它的 children

  • 取出 element.props.children
  • 遍历每一个子节点(无论它是文本还是复杂的组件 VDOM)。
  • 递归调用 render(child, dom)。注意,这里的第二个参数不再是根容器 container,而是当前刚刚创建好的父节点 dom

这种递归结构确保了无论组件树有多深,所有子节点都会准确地挂载到其对应的父节点下。最后,将处理完备的 dom 节点 appendChild 到传入的 container 中。

最终,我们得到了一棵与 VDOM 结构完全一致的真实 DOM 树。

经过上述处理,页面上呈现出的结构与开发者编写的 JSX 完全一致:

html 复制代码
<!-- 开发者写的 JSX -->
<div className="card">
  <h1>Hello</h1>
</div>

<!-- Render 函数生成的真实 DOM -->
<div class="card">
  <h1>Hello</h1>
</div>

开发者只需描述 UI 应该是什么样子 ,而繁琐的 document.createElementsetAttribute、递归遍历以及节点挂载,都被 render 函数自动接管。

这就是声明式编程的闭环:createElement 负责在内存中构建蓝图(VDOM),render 负责在浏览器中按图施工(Real DOM)。 至此,我们的 Mini React 已经具备了最基础的"渲染"能力。

实现二前瞻及总结

✅ 我们做到了什么?

  1. JSX 祛魅 :理解了 JSX 只是 createElement 的语法糖,本质是构建 JS 对象树。
  2. 标准化 VDOM :实现了将字符串、数字等"叶子节点"统一包装为 TEXT_ELEMENT 对象,确保了树结构的一致性。
  3. 递归渲染:通过简单的递归逻辑,将虚拟 DOM 树完整映射为真实 DOM 树,并挂载到页面。
  4. 属性处理 :学会了区分 children 和普通属性,正确地将 props 应用到 DOM 节点上。

现在的 Mini React 已经可以完美渲染静态页面了!🎉


⚠️ 核心痛点:当前 render 函数的致命缺陷

虽然我们的代码能跑,但它存在一个架构级的致命问题,这也是 React 在 v15 及之前版本面临的真实困境:

🛑 缺陷一:同步阻塞(Synchronous & Blocking)

当前的 render 函数是同步递归执行的。

javascript

编辑

javascript 复制代码
1function render(element, container) {
2  // ...创建 DOM...
3  element.props.children.forEach(child => {
4    render(child, dom); // 👈 递归调用,必须等子节点全部渲染完才能返回
5  });
6  // ...挂载...
7}

后果

  • 如果组件树很深(例如 1000 层嵌套)或者节点很多(例如渲染一个包含 10,000 条数据的列表),render 函数会一直执行,直到整棵树全部构建完毕。
  • 主线程被独占 :在此期间,浏览器的主线程(Main Thread) 被完全占用。
  • 用户感知 :页面会出现明显的卡顿(Jank) ,输入框无法响应点击,动画停止,甚至浏览器标签页提示"页面无响应"。

📊 数据事实 :浏览器通常需要在 16ms 内完成一帧的绘制才能达到 60FPS 的流畅度。如果我们的 render 执行了 200ms,用户就要干等 200ms 才能看到画面或进行交互。

🛑 缺陷二:无法中断与恢复(Non-Interruptible)

一旦 render 开始,它就停不下来。

  • 即使此时有一个更高优先级的任务(比如用户突然点击了一个紧急按钮,或者输入框需要更新),JavaScript 引擎也必须等当前的 render 递归彻底结束后,才能去处理那个高优先级任务。
  • 缺乏调度能力:我们无法说"先渲染一部分,让出主线程处理用户输入,剩下的等浏览器空闲了再渲染"。

🛑 缺陷三:全量重绘(No Diffing Yet)

目前的 render 每次调用都是推倒重来

  • 如果状态改变(例如只修改了一个字),我们会销毁整个 DOM 树,重新创建所有节点。
  • 这不仅性能低下,还会导致焦点丢失 (input 框失去焦点)和状态重置(视频播放暂停、滚动条回到顶部)。

🔮 下一篇前瞻:引入 Fiber 架构,实现"可中断"渲染

为了解决上述"同步阻塞"的灾难,React 团队在 v16 引入了革命性的 Fiber 架构。这也是我们 Mini React 进化的下一个终极目标。

🚀 第三阶段目标:Mini React Fiber

我们将重构 render 函数,不再使用递归,而是采用链表遍历 + 时间切片(Time Slicing)

1. 核心变革:从"递归栈"到"工作单元(Work Unit)"

  • 旧模式:函数调用栈(Call Stack),深不可测,无法中断。
  • 新模式 :将渲染任务拆分成一个个小的 Fiber 节点。每个 Fiber 代表一个工作单元。
  • 数据结构升级 :VDOM 对象将增加 return (父), child (子), sibling (兄弟) 指针,形成一棵可遍历的链表树

2. 实现"时间切片"(Time Slicing)

我们将利用 requestIdleCallback (或 setTimeout 模拟) 来实现:

javascript

编辑

scss 复制代码
1function workLoop(deadline) {
2  // 只要还有剩余时间,就继续工作
3  while (deadline.timeRemaining() > 0 && nextUnitOfWork) {
4    // 执行一个小任务
5    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
6  }
7  // 时间用完了?主动让出主线程,等下一帧空闲再继续
8  if (nextUnitOfWork) {
9    requestIdleCallback(workLoop);
10  } else {
11    // 所有任务完成,提交阶段
12    commitRoot();
13  }
14}

效果 :渲染过程变得可中断、可恢复、可优先級调度。高优先级任务(如用户输入)可以插队,低优先级任务(如渲染长列表)可以利用碎片时间慢慢完成。

3. 双缓冲机制(Double Buffering)

  • 引入 Current Tree (屏幕上显示的) 和 WorkInProgress Tree (内存中构建的)。
  • 所有的修改都在 WIP 树上进行,互不干扰。
  • 只有当整棵树构建完成后,才一次性替换指针,瞬间更新 UI,避免用户看到中间状态。

📅 预告内容

在下一篇文章中,我们将:

  1. 重构数据结构 :定义 Fiber 节点结构。
  2. 实现调度器 :编写 workLoop,让渲染"动起来"且"可暂停"。
  3. 分离阶段 :明确区分 Render 阶段 (可中断,构建 Fiber 树)和 Commit 阶段(同步,一次性更新 DOM)。
  4. 解决痛点:演示如何在渲染 10,000 个节点时,依然保持输入框流畅响应。

从"同步阻塞"到"并发可调度的 Fiber",这是现代前端框架性能优化的分水岭。准备好了吗?让我们进入 React 最核心的深水区! 🌊

相关推荐
小曹要微笑1 小时前
委托(Delegate)在C#中的概念与应用
前端·javascript·c#
GISer_Jing1 小时前
前端职业发展进阶指南:从技术深耕到能力破界,向资深工程师稳步迈进
前端·javascript·架构·typescript
weiwx831 小时前
【前端】Node.js使用教程
前端·node.js·vim
K姐研究社1 小时前
Nano Banana 2 国内使用教程:LiblibAI 免翻墙使用
前端·javascript·html
松小白song2 小时前
机器人路径规划算法之Dijkstra算法详解+MATLAB代码实现
前端·javascript·算法
SuperEugene2 小时前
Vue3 中后台实战:VXE Table 从基础表格到复杂业务表格全攻略 | Vue生态精选篇
前端·vue.js·状态模式·vue3·vxetable
打小就很皮...2 小时前
实现可交互的泳道图组件(React)
前端·react.js·泳道图
optimistic_chen2 小时前
【Vue3 入门】掌握这些才能优雅上手
前端·javascript·vue.js·前端框架·visual studio code
认真的小羽❅2 小时前
JavaScript完全指南:从入门到精通
开发语言·javascript·ecmascript