手搓一个 Mini React:从 JSX 到虚拟 DOM 的完整实现
前言 :很多人对 React 的印象停留在"组件化"、"声明式"、"虚拟 DOM"这些高大上的名词上。但 React 到底是如何将我们写的 JSX 转换成浏览器能理解的 DOM 节点的?今天,我们不依赖任何框架,只用原生 JavaScript,手写一个迷你版的 React(我们叫它 Didact),带你揭开 React 底层原理的神秘面纱。
一、为什么我们要手写 React?
在深入阅读源码之前,最好的学习方式就是自己实现一遍。
React 的核心流程其实非常清晰:
- JSX 编译 :通过 Babel 将 JSX 语法转换为
React.createElement调用。 - 创建虚拟 DOM :
createElement返回一个描述 UI 结构的普通 JavaScript 对象。 - 渲染真实 DOM :
render函数遍历虚拟 DOM 树,创建真实的 DOM 节点并挂载到页面。
我们将实现这三个核心步骤,构建一个名为 Didact 的微型库。
二、第一步:实现 createElement ------ 构建虚拟 DOM
JSX 只是语法糖,它的本质是函数调用。例如:
jsx
预览
xml
<div className="container">
<h1>Hello</h1>
</div>
会被 Babel 转译为:
javascript
编辑
php
Didact.createElement(
'div',
{ className: 'container' },
Didact.createElement('h1', null, 'Hello')
);
我们需要实现 createElement 函数,它接收 type(标签名或组件)、props(属性)和 children(子节点),返回一个虚拟 DOM 对象。
核心代码实现
javascript
编辑
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: []
}
};
}
关键点解析
- 统一数据结构 :无论是 HTML 标签还是纯文本,最终都变成具有
type和props的对象。文本节点被特殊标记为TEXT_ELEMENT。 - 递归处理 :
children中的每个元素都会经过判断,如果是对象(即另一个虚拟 DOM),直接保留;如果是字符串/数字,则包裹成文本节点对象。这保证了整棵树结构的一致性。
三、第二步:实现 render ------ 将虚拟 DOM 变为真实 DOM
有了虚拟 DOM 树,下一步就是把它"画"到页面上。render 函数接收两个参数:虚拟 DOM 根节点 (element) 和 容器节点 (container)。
核心代码实现
javascript
编辑
ini
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);
}
关键点解析
-
节点类型判断 :通过
element.type判断是创建Text节点还是普通的Element节点。 -
属性挂载 :遍历
props,过滤掉children属性,将其余属性(如style,className,id等)直接赋值给 DOM 对象。- 注:实际生产中需要更严谨的属性处理逻辑(如
className映射),这里为了演示简化为直接赋值。
- 注:实际生产中需要更严谨的属性处理逻辑(如
-
递归渲染 :这是最关键的一步。对每个子节点再次调用
render,并将当前生成的dom作为新的容器传入。这正是深度优先遍历(DFS)的过程。
四、整合与测试:让 Didact 跑起来
我们将上述函数暴露在一个全局命名空间 Didact 下,并配置 Babel 以识别我们的 JSX。
1. 定义命名空间
javascript
编辑
ini
window.Didact = {
createElement,
render
};
2. 配置 Babel (在 HTML 中)
如果你使用在线编辑器或本地构建工具,需要告诉 Babel 使用我们的 Didact.createElement 而不是默认的 React.createElement。
html
预览
xml
<!-- 在 script 标签上方添加注释配置 -->
<!-- /** @jsxRuntime classic */ -->
<!-- /** @jsx Didact.createElement */ -->
3. 编写 JSX 并渲染
javascript
编辑
css
// 这里的 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) 雏形:
- 数据驱动视图 :我们只需描述 UI 应该长什么样(JSX -> Virtual DOM),具体的 DOM 操作由
render完成。 - 声明式编程 :不需要手动
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 算法,让它支持状态更新而不重绘整个页面!