手搓一个 Mini React:从 JSX 到虚拟 DOM 的完整实现

0 阅读5分钟

手搓一个 Mini React:从 JSX 到虚拟 DOM 的完整实现

前言:很多人对 React 的印象停留在“组件化”、“声明式”、“虚拟 DOM”这些高大上的名词上。但 React 到底是如何将我们写的 JSX 转换成浏览器能理解的 DOM 节点的?今天,我们不依赖任何框架,只用原生 JavaScript,手写一个迷你版的 React(我们叫它 Didact),带你揭开 React 底层原理的神秘面纱。


一、为什么我们要手写 React?

在深入阅读源码之前,最好的学习方式就是自己实现一遍

React 的核心流程其实非常清晰:

  1. JSX 编译:通过 Babel 将 JSX 语法转换为 React.createElement 调用。
  2. 创建虚拟 DOMcreateElement 返回一个描述 UI 结构的普通 JavaScript 对象。
  3. 渲染真实 DOMrender 函数遍历虚拟 DOM 树,创建真实的 DOM 节点并挂载到页面。

我们将实现这三个核心步骤,构建一个名为 Didact 的微型库。


二、第一步:实现 createElement —— 构建虚拟 DOM

JSX 只是语法糖,它的本质是函数调用。例如:

jsx

预览

<div className="container">
  <h1>Hello</h1>
</div>

会被 Babel 转译为:

javascript

编辑

Didact.createElement(
  'div',
  { className: 'container' },
  Didact.createElement('h1', null, 'Hello')
);

我们需要实现 createElement 函数,它接收 type(标签名或组件)、props(属性)和 children(子节点),返回一个虚拟 DOM 对象

核心代码实现

javascript

编辑

function createElement(type, props, ...children) {
    return {
        type,
        props: {
            ...props,
            // 处理 children:如果是对象则直接保留(嵌套的虚拟DOM),如果是文本则创建文本节点对象
            children: children.map(child => 
                typeof child === 'object' 
                ? child 
                : createTextElement(child)
            )
        }
    };
}

// 专门处理文本节点,统一数据结构
function createTextElement(text) {
    return {
        type: "TEXT_ELEMENT",
        props: {
            nodeValue: text,
            children: []
        }
    };
}

关键点解析

  1. 统一数据结构:无论是 HTML 标签还是纯文本,最终都变成具有 type 和 props 的对象。文本节点被特殊标记为 TEXT_ELEMENT
  2. 递归处理children 中的每个元素都会经过判断,如果是对象(即另一个虚拟 DOM),直接保留;如果是字符串/数字,则包裹成文本节点对象。这保证了整棵树结构的一致性。

三、第二步:实现 render —— 将虚拟 DOM 变为真实 DOM

有了虚拟 DOM 树,下一步就是把它“画”到页面上。render 函数接收两个参数:虚拟 DOM 根节点 (element) 和 容器节点 (container)。

核心代码实现

javascript

编辑

function render(element, container) {
    // 1. 根据 type 创建真实的 DOM 节点
    const dom = element.type === 'TEXT_ELEMENT'
        ? document.createTextNode('')
        : document.createElement(element.type);

    // 2. 将 props 赋值给 DOM 节点 (排除 children)
    const isProperty = key => key !== 'children';
    
    Object.keys(element.props)
        .filter(isProperty)
        .forEach(name => {
            dom[name] = element.props[name];
        });

    // 3. 递归渲染子节点
    element.props.children.forEach(child => {
        render(child, dom);
    });

    // 4. 将生成的 DOM 挂载到父节点
    container.appendChild(dom);
}

关键点解析

  1. 节点类型判断:通过 element.type 判断是创建 Text 节点还是普通的 Element 节点。

  2. 属性挂载:遍历 props,过滤掉 children 属性,将其余属性(如 styleclassNameid 等)直接赋值给 DOM 对象。

    • 注:实际生产中需要更严谨的属性处理逻辑(如 className 映射),这里为了演示简化为直接赋值。
  3. 递归渲染:这是最关键的一步。对每个子节点再次调用 render,并将当前生成的 dom 作为新的容器传入。这正是深度优先遍历(DFS)的过程。


四、整合与测试:让 Didact 跑起来

我们将上述函数暴露在一个全局命名空间 Didact 下,并配置 Babel 以识别我们的 JSX。

1. 定义命名空间

javascript

编辑

window.Didact = {
    createElement, 
    render  
};

2. 配置 Babel (在 HTML 中)

如果你使用在线编辑器或本地构建工具,需要告诉 Babel 使用我们的 Didact.createElement 而不是默认的 React.createElement

html

预览

<!-- 在 script 标签上方添加注释配置 -->
<!-- /** @jsxRuntime classic */ -->
<!-- /** @jsx Didact.createElement */ -->

3. 编写 JSX 并渲染

javascript

编辑

// 这里的 JSX 会被 Babel 转换为 Didact.createElement 调用
const element = (
    <div style="background:salmon; padding: 20px;">
        <h1>Hello, world!</h1>
        <h2 style="text-align:right">from Didact</h2>
        <p>这是一个手搓的 Mini React 示例。</p>
    </div>
);

const container = document.getElementById('root');
Didact.render(element, container);

运行结果

当代码执行后,你会在页面上看到一个粉色背景的盒子,里面包含标题和段落。这一切没有依赖任何庞大的框架,只有几十行原生 JavaScript 代码。


五、深入思考:我们实现了什么?还缺什么?

通过这个 Mini React,我们理解了 React 最核心的协调(Reconciliation) 雏形:

  1. 数据驱动视图:我们只需描述 UI 应该长什么样(JSX -> Virtual DOM),具体的 DOM 操作由 render 完成。
  2. 声明式编程:不需要手动 document.createElement 或 appendChild,逻辑更加清晰。

真正的 React 做了什么优化?

虽然我们实现了功能,但上面的 render 函数每次调用都会销毁并重建整个 DOM 树。如果状态改变,整个页面会闪烁重绘。真正的 React 引入了以下机制来解决这个问题:

  • Diff 算法:对比新旧虚拟 DOM 树,只更新变化的部分(最小化重绘)。
  • Fiber 架构:将渲染任务拆分为可中断的小单元,避免长任务阻塞主线程,实现并发渲染。
  • 调度系统 (Scheduler) :根据优先级处理更新(如用户输入优先于数据加载)。

六、总结

手写 Mini React 是理解现代前端框架的最佳途径。通过 Didact,我们看到了:

  • JSX 只是生成对象的语法糖。
  • 虚拟 DOM 是描述 UI 的普通 JS 对象。
  • Render 是一个递归遍历并创建真实 DOM 的过程。

虽然这只是 React 庞大冰山的一角,但它涵盖了最本质的思想:UI 是状态的函数。掌握了这些,再去阅读 React 源码或学习 Vue、Solid 等框架,你会发现它们的内核惊人地相似。

下一步挑战:尝试为 Didact 添加 useState 和 Diff 算法,让它支持状态更新而不重绘整个页面!