从 0 到 1 手写 React(part1):揭秘 MVVM 框架的“心脏”跳动原理

从 0 到 1 手写 React(part1):揭秘 MVVM 框架的"心脏"跳动原理

导语:在 2026 年的今天,React 依然是前端领域的绝对霸主。Fiber 架构、并发模式、Server Components......这些高级概念让 React 强大无比,但也让许多开发者望而却步。

很多时候,我们沉迷于调用 API,却忘记了框架的本质。最好的学习方式,永远是亲手造一个轮子。

本文将带你剥离 React 复杂的外壳,回归最原始的 createElementrender,用一个名为 Didact(致敬 React 的教育意义)的微型框架,带你彻底吃透 JSX、虚拟 DOM 和渲染机制。


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

在稀土掘金和各大技术社区,关于 React 源码分析的文章汗牛充栋。但大多数文章直接切入 Fiber 链表或调度算法,让初学者极易劝退。

其实,React 的核心哲学非常简单:UI 是状态的函数 (UI = f(State))

手写一个 Mini React(我们称之为 Didact),能帮你解决三个核心困惑:

  1. JSX 到底是什么? 它真的是 HTML 吗?
  2. 虚拟 DOM (VDOM) 长什么样? 为什么需要它?
  3. 渲染是如何发生的? 从 JS 对象到真实页面,中间经历了什么?

💡 命名彩蛋 :我们将这个微型框架命名为 Didact。在希腊语中,"Didact" 意为"教学",寓意通过构建它来学习 React 的真谛。


二、第一块基石:JSX 与 createElement

2.1 JSX:不仅仅是语法糖

很多新手看到 JSX 会误以为是在 JS 里写 HTML。实际上,JSX 是 JavaScript 的语法扩展

看这段熟悉的代码:

jsx 复制代码
const element = (
  <div id="app">
    <h1>Hello, World!</h1>
  </div>
);

在没有 Babel 转译之前,浏览器是无法识别 <div> 标签的。Babel 会将上述代码"翻译"成标准的 JavaScript 函数调用:

javascript 复制代码
const element = createElement(
  'div',
  { id: 'app' },
  createElement('h1', null, 'Hello, World!')
);

JSX 的核心优势 在于它的声明式直观性

  • 逻辑与视图统一:不像 Vue 早期版本需要将 template、script、style 分离(三段式),React 通过 JSX 将数据逻辑直接嵌入 UI 结构中。
  • 所见即所得 :你可以直接在 JS 中看到最终的 DOM 树结构,利用 JS 的强大能力(如 mapfilter)动态生成节点。
jsx 复制代码
// 在 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 中,我们这样实现它:

javascript 复制代码
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 容器)。

它的核心逻辑是递归遍历

javascript 复制代码
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);
}

流程拆解

  1. 节点类型判断 :如果是 TEXT_ELEMENT,调用 createTextNode;否则调用 createElement
  2. 属性挂载 :遍历 props,将 idclassName 等属性赋值给真实 DOM。注意children 不是 DOM 属性,需要过滤掉,因为它会在下一步作为子节点处理。
  3. 递归下降 :对每个子节点再次调用 render,并将当前的 dom 作为新的 container 传入。这是典型的深度优先遍历 (DFS)。
  4. 插入文档流 :当子节点处理完毕,将当前 dom 追加到父容器中。

四、整合:Didact 的诞生

现在,我们将 createElementrender 暴露在一个统一的命名空间下,形成我们的微型框架。

javascript 复制代码
// 统一导出
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+,我们缺失了什么?

  1. 更新机制 (Re-render) :目前的 render 每次都是全量重新创建 DOM。如果状态变了,如何只更新变化的部分?这就需要 Diff 算法
  2. 性能瓶颈 :一旦组件树变大,递归渲染会阻塞主线程,导致页面卡顿。React 引入 Fiber 架构,将渲染任务拆分成可中断的时间切片,解决了这个问题。
  3. 事件系统 :原生 DOM 事件性能较差,React 实现了合成事件事件委托
  4. Hooks 原理:函数组件如何"记住"状态?这需要闭包和链表的支持。

学习路线建议

如果你想继续深入,建议按照以下路径进阶:

  • 阶段一(本文) :理解 createElement 和同步 render
  • 阶段二 :实现简单的 setState 和 Diff 算法,支持局部更新。
  • 阶段三 :引入 Fiber 架构,实现 requestIdleCallback 进行时间分片。
  • 阶段四 :实现 Hooks (useState, useEffect)。

结语

手写框架的意义不在于替代官方库,而在于祛魅

当你亲手写下那几十行代码,看着 JSX 变成对象,对象变成 DOM,你会对"虚拟 DOM"、"声明式 UI"这些概念有肌肉记忆般的理解。下次再遇到 React 的性能问题或奇怪 Bug 时,你的脑海中浮现的不再是黑盒,而是那棵正在被递归遍历的树。

代码已开源 :欢迎在评论区交流你的 Didact 实现,或者提出你想看到的下一个功能(比如 Hooks 实现)!


相关推荐
随逸1772 小时前
《从零手写Mini React》
react.js
www_stdio2 小时前
手搓一个 Mini React:从 JSX 到虚拟 DOM 的完整实现
前端·react.js·面试
@大迁世界3 小时前
01.什么是 ReactJS?
前端·javascript·react.js·前端框架·ecmascript
默 语3 小时前
TypeScrip+React 全栈生态实战:从架构选型到工程落地,告别开发踩坑
前端·react.js·架构
zhengzhengwang4 小时前
react18升级新特性
前端·javascript·react.js
炒毛豆4 小时前
微前端框架 qiankun 简明指南
前端·javascript·react.js
我命由我123456 小时前
React - 验证 Diffing 算法、key 的作用
javascript·算法·react.js·前端框架·html·html5·js
湛海不过深蓝13 小时前
【procomponents】根据表单查询表格数据的两种写法
前端·javascript·react.js
Beth_Chan13 小时前
Stock Trading - React
javascript·react.js