手写 Mini React 核心原理解析

1. "手写一个 React 框架"是什么意思?

"手写"不是让你从零发明一个 React,而是用纯 JavaScript 自己实现 React 最核心的两个功能

  • createElement --- 把 JSX 转成虚拟 DOM(VDOM)对象
  • render --- 把虚拟 DOM 渲染成真实 DOM

在这个项目中,用 ~60 行代码实现了一个迷你框架 Didact (挂载在 window.Didact 下),模拟 React 的核心流程。

scss 复制代码
整个流程只有两步:
JSX  →  Babel转译  →  Didact.createElement()  →  VDOM对象  →  Didact.render()  →  真实DOM
  • 第一步 createElement :接收标签名、属性、子元素,返回一个普通 JS 对象(VDOM),对文本子节点统一包装成 TEXT_ELEMENT 类型。
  • 第二步 render :根据 VDOM 的 type 创建真实 DOM 节点,挂上属性,递归处理子节点,最后 appendChild 到容器。

React 的真实源码当然更复杂(Fiber 调度、Diff 算法、并发模式等),但对于理解核心原理,这两个函数就够了。React 的本质就是 JavaScript 函数。

2. 框架还能手写吗?

当然可以。React 本质上就是 JavaScript 函数,核心机制并不神秘:

js 复制代码
// 这就是 "手写" 的 createElement,只是一个接收参数、处理数据、返回对象的纯函数
function createElement(type, props, ...children) {
    return {
        type,
        props: {
            ...props,
            children: children.map(child =>
                typeof child === 'object' ? child : createTextElement(child)
            )
        }
    }
}

整个 Didact 框架只有 60 行代码,就实现了 JSX → VDOM → 真实 DOM 的完整链路。React 框架的起点,就是一个简单的纯函数。

3. 什么是"统一的命名空间"?

指框架的所有 API 挂在一个对象下,例如:

js 复制代码
window.Didact = {
    createElement,  // 创建虚拟DOM
    render          // 渲染虚拟DOM
}

这和 React 的做法一样 --- React 也是一个统一的命名空间,所有 API 都是 React.createElementReact.renderReact.useState 这样统一使用的。

4. JS 对象是什么?VDOM 如何用 JS 对象描述界面?

JS 对象 就是 JavaScript 里用 {} 包起来的一组键值对 ,用 . 来读写属性值:

js 复制代码
// 这就是一个 JS 对象 ------------ 描述一个"人"
const person = {
    name: "张三",
    age: 28,
    city: "上海"
}
console.log(person.name);  // "张三"
person.age = 29;           // 改数据

VDOM 就是一个 JS 对象 ,用 typeprops 来描述 DOM 结构:

js 复制代码
// 这段 JSX:
<div style="background:salmon">
    <h1>Hello</h1>
</div>

// 经过 Babel 转译 + createElement 之后,变成一个 JS 对象:
const vdom = {
    type: "div",
    props: {
        style: "background:salmon",
        children: [
            {
                type: "h1",
                props: {
                    children: [
                        { type: "TEXT_ELEMENT", props: { nodeValue: "Hello", children: [] } }
                    ]
                }
            }
        ]
    }
}

多个 element 对象通过 children 数组嵌套,就构成了 VDOM 树。

5. VDOM → 真实 DOM 是什么意思?render 函数具体做了什么?

  • 虚拟 DOM:内存里的普通 JS 对象,描述"页面上应该有什么"。用户看不见。
  • 真实 DOM:浏览器里真正显示的 HTML 元素。用户能看见。
  • render:把 JS 对象描述的页面结构,变成浏览器真正能显示的 HTML 元素。
less 复制代码
虚拟DOM对象 (内存里)          真实DOM (页面上)
─────────────────────         ─────────────────
{                              <div>
  type: "div",        render     <h1>Hello</h1>
  props: {            ──────>    <h2>World</h2>
    children: [...]             </div>
  }
}

render 函数拆解为三步(对应项目源码):

第一步 :根据 type 创建真实 DOM 节点

js 复制代码
const dom = element.type === 'TEXT_ELEMENT'
    ? document.createTextNode('')       // 文本节点
    : document.createElement(element.type);  // 普通标签

第二步 :把属性挂上去(过滤掉 children 属性)

js 复制代码
const isProperty = key => key !== "children";
Object.keys(element.props).filter(isProperty).forEach(name => {
    dom[name] = element.props[name];  // 如 style="background:salmon"
});

第三步:递归处理子节点,塞进父节点

js 复制代码
element.props.children.forEach(child => render(child, dom));  // 递归
container.appendChild(dom);  // 最终插入页面

render 做完 appendChild 之后就结束了,此时页面上有了真实的 DOM 节点。底层调用的就是浏览器原生 API:document.createElementdocument.createTextNodeappendChild,和你在 HTML 原生 JS 里用的完全一模一样。

VDOM 不是"另一种 DOM" ,可以将 VDOM 理解为内存里的"设计图纸",render 就是施工队按图纸造真实的房子。React/Didact 底层调用的就是浏览器原生 DOM API,最终产物都是标准的 DOM 节点(HTMLDivElement 等),和你在 HTML 里手写的没有区别。

6. "开发者只需要操作 JS 对象(改数据)"是什么意思?

这句话容易产生误解,不是说开发者去手动操作 VDOM 那个 JS 对象,而是说 React 内部用 JS 对象(VDOM)来代替直接操作真实 DOM,开发者只需要改变普通的状态数据即可。

对比两种写法:

没有 React 时,操作的是真实 DOM(性能差、代码繁琐):

js 复制代码
document.getElementById('title').innerText = 'Hello';
document.getElementById('title').style.color = 'red';
// 每次改东西都要这样写一大串

有 React 时,开发者只需改 state:

jsx 复制代码
function App() {
    const [text, setText] = useState("Hello");  // 改这个数据就行
    return <h1>{text}</h1>                       // React 自动同步到页面
}

整个流程是 :你改数据(setText) → React 内部用 createElement 生成新的 VDOM(JS 对象)→ React 拿新旧 VDOM 对比,算出差异 → React 只更新变化的那部分真实 DOM。作为开发者你根本不需要碰 DOM,只需要改 text 这种普通数据就够了。"操作 JS 对象"发生在 React 内部,React 用 JS 对象做高效对比后再更新页面,你不需要手动写。

7. React 的 DOM 节点和 HTML 里的 DOM 节点一样吗?

完全一样。 React / Didact 底层调用的就是浏览器原生 API:

js 复制代码
// Didact render 函数的源码,用的就是原生 API
const dom = element.type == 'TEXT_ELEMENT'
    ? document.createTextNode('')           // ← 原生 Web API
    : document.createElement(element.type); // ← 还是原生 Web API

React/Didact 最终调用 document.createElementdocument.createTextNodeappendChild,和你在 HTML 原生 JS 里用的完全一模一样 。最终产物都是浏览器标准的 DOM 节点(HTMLDivElement 等),挂到页面上之后你用 document.getElementById 也能查到。

可以这样理解:两张图纸(新旧 VDOM)在内存里对比,算出哪里需要改,施工队(render)只修有变化的地方。但最终造出来的"房子"(DOM 节点),和你在 HTML 里手写的没有任何区别。

8. Vite/脚手架 和 React 的分工:它们各干各的

Vite 和 React 做的是完全不相干的两件事:

Vite / 脚手架(构建时) React(运行时)
干什么 JSX 编译成 JS、模块化打包、热更新(HMR)、CSS处理、TypeScript编译、生产打包压缩 VDOM 生成、Diff 对比、DOM 更新
什么时候干活 你写代码 / 保存文件时 用户打开网页、点击按钮、交互时
类比 翻译:把 JSX 语法翻译成浏览器能懂的 JS 厨师:把食材(数据)做成菜(页面)

整个时间线:

css 复制代码
┌───── 写代码/保存(构建时)─────┐        ┌───── 浏览器运行(运行时)─────────────────┐
│                                │        │                                          │
│ Vite 干活:                      │        │ React 干活:                               │
│ <h1>Hello</h1>                  │        │ Didact.createElement(...)                  │
│      ↓ Babel 编译                │        │      ↓                                   │
│ Didact.createElement('h1', ...) │   →    │ { type: "h1", props: {...} }  (VDOM)      │
│                                │        │      ↓                                   │
│ 然后 Vite 退出,不参与了           │        │ Didact.render(element, container)         │
│                                │        │      ↓                                   │
└────────────────────────────────┘        │ <h1>Hello</h1>  (真实DOM)                  │
                                          └──────────────────────────────────────────┘

关键点 :在 setText → 新 VDOM → render 这个过程中,Vite 完全不在场。Vite 只参与"写代码 → 能运行"这一步(把 JSX 编译成 JS),不参与"运行后 → 渲染"这一步。渲染完全是 React 的事。

另外,Vite 不仅仅做 Babel 编译,它还包括:import/export 模块化处理、热更新(HMR)、CSS 预处理器编译、TypeScript 编译、生产环境打包压缩等功能。

9. document.createElementReact.createElement 是两个完全不同的函数

名字像但完全不同,这是最容易混淆的地方:

document.createElement React.createElement(Didact.createElement)
谁提供的 浏览器原生 API,内置在浏览器里 React 源码自己写的,是自定义 JS 函数
返回值 真实 DOM 节点(HTMLDivElement 等) 普通 JS 对象(VDOM){ type, props }
用途 创建页面上的 HTML 元素 描述页面结构
函数内部 浏览器内核实现,你改不了 React 团队自己设计参数格式和返回结构
js 复制代码
// 浏览器原生 API ------------ 创建真实 DOM 元素
document.createElement('div')   // → <div></div>(真正的 HTML 元素对象)

// React 自己写的函数 ------------ 创建 VDOM 对象(普通 JS 对象)
React.createElement('div', null) // → { type: "div", props: { children: [] } }

React 的 createElement 是 React 团队自己设计并实现的函数,接收 (type, props, children) 三个参数,返回固定结构的 JS 对象 { type, props: { ...children } }。浏览器完全不知道这个函数的存在。

React 做的事 :自己写 createElement 函数 + 自己写 render 函数 + 运行时调用它们。不是调浏览器现成的 API,而是 React 自己造了一套规则和函数,浏览器只负责最后那一步(document.createElementappendChild),而且也是 React 在 render 函数里替你去调用的。

10. createElement 内部实现详解

当 Babel 把 <h1>Hello</h1> 编译成 Didact.createElement('h1', null, 'Hello') 后,执行过程如下:

js 复制代码
function createElement(type, props, ...children) {
    return {
        type,                     // 'h1' 直接存进 type
        props: {
            ...props,             // null 展开就是空(无属性时)
            children: children.map(child =>
                typeof child === 'object'
                    ? child                // 已经是 VDOM 对象,直接使用
                    : createTextElement(child)  // 普通字符串 → 包装成文本节点对象
            )
        }
    }
}

function createTextElement(text) {
    return {
        type: "TEXT_ELEMENT",
        props: {
            nodeValue: text,    // 文本内容
            children: []        // 文本节点没有子节点
        }
    }
}

执行过程拆解 (以 <h1>Hello</h1> 为例):

rust 复制代码
调用:createElement('h1', null, 'Hello')

参数:
  type = 'h1'
  props = null
  children[0] = 'Hello'  (普通字符串)

执行:
  1. 'Hello' 是字符串,typeof child !== 'object'
  2. 调用 createTextElement('Hello')
     → 返回 { type: "TEXT_ELEMENT", props: { nodeValue: "Hello", children: [] } }
  3. 最终返回:

{
    type: "h1",
    props: {
        children: [
            {
                type: "TEXT_ELEMENT",
                props: { nodeValue: "Hello", children: [] }
            }
        ]
    }
}

为什么要统一处理文本节点? 普通字符串 'Hello' 和 VDOM 对象结构不同,如果 render 时要分别判断,代码会很乱。通过 createTextElement 把文本也包装成 { type: "TEXT_ELEMENT", props: { nodeValue: "...", children: [] } } 统一格式,所有节点结构一致,render 时无需区分类型,直接递归处理。

11. React 的 render ≠ 浏览器的渲染

两个"渲染"不是一回事,经常被混淆:

React 的 render 浏览器的渲染
做什么 把 VDOM(JS对象)变成真实 DOM 节点并 appendChild 插入页面 浏览器内核把 DOM + CSS 画成屏幕上的像素
产物 HTML 元素节点(<div><h1> 等) 你眼睛看到的画面(像素)
谁在做 React / Didact 代码 浏览器渲染引擎(内核)

完整渲染流水线

scss 复制代码
React render 结束                  浏览器渲染开始
──────────────                    ──────────────
DOM 节点已在页面上    →    构建渲染树 → 布局(计算位置大小) → 绘制(画像素) → 你看到了画面

做完 appendChild 之后,React 的 render 就结束了 。此时页面上有了真实的 DOM 节点,浏览器检测到 DOM 变了,才自动触发自己的渲染流程。React 只负责产出 DOM 节点,至于怎么把 DOM 画到屏幕上,那是浏览器内核(渲染引擎)的事,React 完全不管。

12. VDOM 有什么好处?没有 VDOM 会怎么样?

没有 VDOM 时(原生 JS),每次改数据都直接操作真实 DOM:

js 复制代码
// 用户点击按钮,想把 "Hello" 改成 "你好"
document.getElementById('title').innerText = '你好';   // 改文字 → 触发重绘
document.getElementById('title').style.color = 'red';  // 改颜色 → 又触发重绘
// 每个改动都直接打在真实 DOM 上,每个改动都可能触发浏览器的重绘重排

VDOM 的三大好处:

  1. 批量对比,一次性更新:VDOM 是普通 JS 对象,创建和对比都非常快。React 先在 JS 对象层面算完所有差异,再一次性操作真实 DOM,避免频繁触发重绘重排。

    没有 VDOM:改文字 → 操作DOM → 改颜色 → 操作DOM → 改大小 → 操作DOM (3 次 DOM 操作)
    有 VDOM: 先在 JS 对象里算完所有差异 → 一次性更新 DOM (1 次 DOM 操作)

  2. 跨平台:VDOM 就是一个纯 JS 对象,不依赖浏览器。同一个 VDOM 结构可以渲染到不同平台:

css 复制代码
VDOM对象  →  render →  浏览器 DOM
          →  render →  React Native(iOS/Android 原生组件)
          →  render →  Canvas
          →  render →  PDF
          →  render →  ...
  1. 解耦数据与 DOM:开发者只关心数据(state),React 负责把数据高效同步到页面。你可以把 VDOM 理解为"数据"和"页面"之间的中间层。

13. 一个 JSX 片段 = 一个 element 对象

JSX 只是写法上的语法糖 ,它等于 createElement 的调用,然后返回一个 element 对象(VDOM 树上的一个节点):

js 复制代码
// 这两行是等价的:
const el1 = <div>Hi</div>                                     // JSX 写法
const el2 = Didact.createElement('div', null, 'Hi')            // 编译后写法
// el2 的值 = { type: "div", props: { children: [TEXT_ELEMENT] } }  ← element 对象

复杂嵌套示例

jsx 复制代码
const element = (
    <div style="background:salmon">
        <h1>Hello</h1>
        <h2>World</h2>
    </div>
)

// 编译后变成一个嵌套的 element 对象:
{
    type: "div",
    props: {
        style: "background:salmon",
        children: [
            { type: "h1", props: { children: [{ type: "TEXT_ELEMENT", props: { nodeValue: "Hello" } }] } },
            { type: "h2", props: { children: [{ type: "TEXT_ELEMENT", props: { nodeValue: "World" } }] } }
        ]
    }
}

每个 element = VDOM 树上的一个节点 ,节点之间通过 children 数组串联,最终形成一棵树。JSX 就是一层糖衣,剥开后全是普通的 JS 函数调用和对象。

14. 挂载点是什么?和 render 有什么关系?

挂载点 是 HTML 里预先放好的一个空容器(通常是 <div id="root"></div>),亲手写的或脚手架模板生成的。

html 复制代码
<!-- index.html -->
<body>
    <div id="root"></div>   <!-- ← 挂载点,一开始是空的,等着 React 往里填内容 -->
</body>

JS 中通过 getElementById 拿到它,传给 render:

js 复制代码
const container = document.getElementById('root');  // 获取挂载点
Didact.render(element, container);                   // 把 VDOM 渲染的内容塞进去

render 前后对比

xml 复制代码
render 之前:                      render 之后:

<div id="root"></div>              <div id="root">
                                       <div style="background:salmon">
                                           <h1>Hello, world!</h1>
                                           <h2>from Didact</h2>
                                       </div>
                                   </div>
       ↑                                    ↑
    空壳子                              VDOM 渲染的内容被塞进去了

在 render 函数最后,通过 container.appendChild(dom) 把生成的 DOM 塞进挂载点。

不是每个标签都是挂载点 。挂载点是 React 接管页面的唯一入口,整个应用通常只有一个。挂载点之外的内容(如果有)不受 React 控制。React 从这个入口进去之后,里面的所有内容都是 React 自己管的。

相关推荐
触底反弹1 小时前
💡 React 父子组件通信:一个进度条教会我的 5 件事
前端·react.js·面试
码农学院21 小时前
React公众号开发:母婴用品会员积分体系技术方案
前端·react.js·前端框架
mONESY21 小时前
React TodoList 通俗讲解|彻底搞懂父子组件通信与状态管理
react.js
kisshyshy1 天前
《端侧大模型(三)“精装修”日记:进度条整容、聊天框首秀》
react.js·typescript·代码规范
lymboy1 天前
Spring AI的工具调用循环,本质就是ReAct:一次源码级拆解
人工智能·spring·react.js
Darling噜啦啦2 天前
React 组件化实战:从 TodoList 吃透父子通信与状态管理
react.js
bloglin999992 天前
langchain 和 langgraph 和 react
javascript·react.js·langchain
触底反弹2 天前
🚀 删了数据刷新又回来?3 组件 × 4 回调 × 3 坑讲透 React 父子通信
前端·javascript·react.js
sugar__salt2 天前
useState 深入浅出:React 状态管理的基石
前端·react.js·前端框架