从 0 到 1 手写 React(part1):揭秘 MVVM 框架的“心脏”跳动原理
导语:在 2026 年的今天,React 依然是前端领域的绝对霸主。Fiber 架构、并发模式、Server Components……这些高级概念让 React 强大无比,但也让许多开发者望而却步。
很多时候,我们沉迷于调用 API,却忘记了框架的本质。最好的学习方式,永远是亲手造一个轮子。
本文将带你剥离 React 复杂的外壳,回归最原始的
createElement和render,用一个名为 Didact(致敬 React 的教育意义)的微型框架,带你彻底吃透 JSX、虚拟 DOM 和渲染机制。
一、为什么我们要手写 React?
在稀土掘金和各大技术社区,关于 React 源码分析的文章汗牛充栋。但大多数文章直接切入 Fiber 链表或调度算法,让初学者极易劝退。
其实,React 的核心哲学非常简单:UI 是状态的函数 (UI = f(State))。
手写一个 Mini React(我们称之为 Didact),能帮你解决三个核心困惑:
- JSX 到底是什么? 它真的是 HTML 吗?
- 虚拟 DOM (VDOM) 长什么样? 为什么需要它?
- 渲染是如何发生的? 从 JS 对象到真实页面,中间经历了什么?
💡 命名彩蛋:我们将这个微型框架命名为
Didact。在希腊语中,"Didact" 意为“教学”,寓意通过构建它来学习 React 的真谛。
二、第一块基石:JSX 与 createElement
2.1 JSX:不仅仅是语法糖
很多新手看到 JSX 会误以为是在 JS 里写 HTML。实际上,JSX 是 JavaScript 的语法扩展。
看这段熟悉的代码:
const element = (
<div id="app">
<h1>Hello, World!</h1>
</div>
);
在没有 Babel 转译之前,浏览器是无法识别 <div> 标签的。Babel 会将上述代码“翻译”成标准的 JavaScript 函数调用:
const element = createElement(
'div',
{ id: 'app' },
createElement('h1', null, 'Hello, World!')
);
JSX 的核心优势在于它的声明式和直观性:
- 逻辑与视图统一:不像 Vue 早期版本需要将 template、script、style 分离(三段式),React 通过 JSX 将数据逻辑直接嵌入 UI 结构中。
- 所见即所得:你可以直接在 JS 中看到最终的 DOM 树结构,利用 JS 的强大能力(如
map、filter)动态生成节点。
// 在 JSX 中直接使用 JS 逻辑
<div>
<h2>用户列表</h2>
{users.map(user => <p key={user.id}>{user.name}</p>)}
</div>
2.2 实现 createElement:构建虚拟 DOM
既然 JSX 最终会变成 createElement 的调用,那这个函数到底做了什么?
它的任务很简单:接收参数,返回一个描述 UI 结构的普通 JavaScript 对象(即虚拟 DOM)。
在 Didact 中,我们这样实现它:
function createElement(type, props, ...children) {
// 1. 处理 children:将原始值(字符串/数字)转换为文本节点对象
const flattenedChildren = children.flat().map(child =>
typeof child === 'object' ? child : createTextElement(child)
);
// 2. 返回 VDOM 对象
return {
type, // 标签名 (如 'div') 或 组件函数
props: {
...props,
children: flattenedChildren
}
};
}
// 辅助函数:创建文本节点的特殊 VDOM 结构
function createTextElement(text) {
return {
type: 'TEXT_ELEMENT',
props: {
nodeValue: text,
children: []
}
};
}
关键点解析:
- 统一数据结构:无论是元素节点 (
div) 还是文本节点 (hello),我们都将其转化为对象。文本节点被特殊标记为TEXT_ELEMENT,方便后续渲染时区分。 - 递归处理:
children中可能包含其他createElement的返回值(子元素),也可能只是字符串。我们通过map统一处理,确保props.children是一个标准的 VDOM 数组。 - 结果:执行完这一步,我们得到了一棵纯 JS 对象构成的树,它内存占用小,且易于比较和修改。
三、第二块基石:Render 机制
有了虚拟 DOM 树,下一步就是把它“画”到屏幕上。这就是 render 函数的职责。
3.1 从 VDOM 到 Real DOM
render 函数接收两个参数:element (VDOM 根节点) 和 container (真实 DOM 容器)。
它的核心逻辑是递归遍历:
function render(element, container) {
// 1. 创建真实 DOM 节点
const dom =
element.type === 'TEXT_ELEMENT'
? document.createTextNode(element.props.nodeValue ?? '')
: document.createElement(element.type);
// 2. 添加属性 (排除 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. 挂载到父节点
container.appendChild(dom);
}
流程拆解:
- 节点类型判断:如果是
TEXT_ELEMENT,调用createTextNode;否则调用createElement。 - 属性挂载:遍历
props,将id、className等属性赋值给真实 DOM。注意:children不是 DOM 属性,需要过滤掉,因为它会在下一步作为子节点处理。 - 递归下降:对每个子节点再次调用
render,并将当前的dom作为新的container传入。这是典型的深度优先遍历 (DFS)。 - 插入文档流:当子节点处理完毕,将当前
dom追加到父容器中。
四、整合:Didact 的诞生
现在,我们将 createElement 和 render 暴露在一个统一的命名空间下,形成我们的微型框架。
// 统一导出
window.Didact = {
createElement,
render
};
// 配置 Babel (在 HTML 中)
// <script type="text/babel" data-presets="react" data-type-module>
// @jsxRuntime classic
// @jsxFactory Didact.createElement
const App = () => (
<div id="app">
<h1>你好,Didact!</h1>
<p>这是手写的 React 核心原理。</p>
</div>
);
Didact.render(<App />, document.getElementById('root'));
// </script>
当你运行这段代码,浏览器控制台会打印出创建的 DOM 结构,页面上也会显示出内容。恭喜!你已经实现了 React 最核心的“渲染链路”。 在页面上会显示:
五、深入思考:这仅仅是开始
虽然我们成功渲染了页面,但这个 Didact 还很粗糙。对比 2026 年成熟的 React 19+,我们缺失了什么?
- 更新机制 (Re-render):目前的
render每次都是全量重新创建 DOM。如果状态变了,如何只更新变化的部分?这就需要 Diff 算法。 - 性能瓶颈:一旦组件树变大,递归渲染会阻塞主线程,导致页面卡顿。React 引入 Fiber 架构,将渲染任务拆分成可中断的时间切片,解决了这个问题。
- 事件系统:原生 DOM 事件性能较差,React 实现了合成事件和事件委托。
- Hooks 原理:函数组件如何“记住”状态?这需要闭包和链表的支持。
学习路线建议
如果你想继续深入,建议按照以下路径进阶:
- 阶段一(本文):理解
createElement和同步render。 - 阶段二:实现简单的
setState和 Diff 算法,支持局部更新。 - 阶段三:引入 Fiber 架构,实现
requestIdleCallback进行时间分片。 - 阶段四:实现 Hooks (
useState,useEffect)。
结语
手写框架的意义不在于替代官方库,而在于祛魅。
当你亲手写下那几十行代码,看着 JSX 变成对象,对象变成 DOM,你会对“虚拟 DOM”、“声明式 UI”这些概念有肌肉记忆般的理解。下次再遇到 React 的性能问题或奇怪 Bug 时,你的脑海中浮现的不再是黑盒,而是那棵正在被递归遍历的树。
代码已开源:欢迎在评论区交流你的 Didact 实现,或者提出你想看到的下一个功能(比如 Hooks 实现)!