从零手写Mini React:吃透JSX与虚拟DOM,彻底搞懂React底层渲染逻辑
引言
在前端开发领域,React 早已成为主流的 MVVM 框架,组件化、虚拟 DOM、JSX 语法这些特性我们天天在用,但抛开框架的封装,你是否真的理解:为什么写 JSX 就能渲染出页面?虚拟 DOM 到底是什么?React 渲染的核心流程又是什么?
死记硬背源码概念远不如动手实现来得深刻,今天我们就抛开繁杂的 React 源码,从零实现一个极简版 Mini React(命名为 Didact),只聚焦JSX 编译、虚拟 DOM 生成、真实 DOM 渲染三大核心环节,用几十行代码,撕开 React 底层的神秘面纱,让前端原理学习不再晦涩难懂。
一、先搞懂:React 渲染的核心本质
很多刚接触 React 的同学,都会陷入一个误区:把 JSX 当成 HTML,把虚拟 DOM 当成"优化手段"。其实 React 的核心渲染逻辑,总结起来就一句话:开发者写 JSX 描述 UI → 编译生成虚拟 DOM → 虚拟 DOM 转为真实 DOM 挂载到页面。
1.1 关键概念拆解
- JSX :不是模板语法,更不是 HTML,而是 JavaScript 的语法糖,最终会被 Babel 编译成可执行的 JS 函数调用,让我们能用更直观的方式编写 UI 结构。
- 虚拟 DOM(VDOM) :一个普通的 JS 对象,用来描述真实 DOM 的结构、属性和子节点,是 React 操作 DOM 的中间层,避免直接操作原生 DOM 导致的性能损耗。
- 渲染流程 :从 JSX 到页面可见元素,本质是
JSX → createElement → VDOM → render → 真实 DOM的转化过程。
1.2 为什么要手写 Mini React?
- 剥离 React 源码中冗余的边界处理、兼容逻辑,直击核心原理,学习效率翻倍;
- 亲手实现每一步逻辑,彻底理解虚拟 DOM 和 JSX 的底层作用,告别"只会用不懂原理"的困境;
- 夯实前端基础,应对面试中 React 原理类问题得心应手,同时提升源码阅读能力。
二、极简环境搭建:零配置快速上手
我们不需要 create-react-app、Vite 这类复杂脚手架,也不用配置繁琐的 Babel 工程化环境,采用原生 HTML + 在线 Babel的方式,零配置就能运行,全程专注于逻辑实现,不被工程化琐事干扰。
核心准备:新建一个 HTML 文件,引入 Babel 在线编译脚本,用于实时编译 JSX 语法,后续所有核心代码都写在页面内,打开浏览器即可查看效果。
三、Mini React 核心代码实现(逐行精讲)
我们的 Mini React 只实现两大核心函数:createElement(生成虚拟 DOM)和 render(渲染真实 DOM),再搭配文本节点处理函数,完成完整渲染流程。
3.1 第一步:处理文本节点,统一 DOM 格式
真实 DOM 分为普通元素节点(div、h1 等)和文本节点,二者创建方式不同,为了让后续渲染逻辑统一处理,我们需要把纯文本也封装成格式一致的虚拟 DOM 对象,单独封装处理函数。
php
/**
* 创建文本类型的虚拟DOM
* @param {string} text - 文本内容
* @return {object} 标准化文本虚拟DOM
*/
function createTextElement(text) {
return {
// 标记为文本节点,区分普通元素
type: 'TEXT_ELEMENT',
props: {
// 文本节点的内容,对应原生DOM的nodeValue
nodeValue: text,
// 文本节点无子节点,固定为空数组
children: []
}
}
}
这一步的核心作用:抹平元素节点和文本节点的差异,让后续递归渲染无需区分处理,简化代码逻辑。
3.2 第二步:实现 createElement,生成虚拟 DOM
Babel 编译 JSX 后,会自动调用 React.createElement 方法,我们手写 Mini React,只需实现同名逻辑的 Didact.createElement,就能承接编译后的 JSX。
该函数接收三个参数:节点类型(type)、节点属性(props)、子节点(children) ,最终返回标准化的虚拟 DOM 对象。
typescript
/**
* 创建虚拟DOM
* @param {string|function} type - 节点类型(标签名/组件)
* @param {object} props - 节点属性(style、class等)
* @param {...any} children - 子节点
* @return {object} 虚拟DOM对象
*/
function createElement(type, props, ...children) {
return {
type,
props: {
// 展开传入的属性
...props,
// 处理子节点:递归格式化,保证所有子节点都是虚拟DOM对象
children: children.map(child => {
// 如果是对象,说明已是虚拟DOM,直接返回
// 如果是基础类型(文本),调用函数封装为虚拟DOM
return typeof child === 'object' ? child : createTextElement(child)
})
}
}
}
生成的虚拟 DOM 结构
经过该函数处理后,无论元素还是文本,都会生成结构一致的 JS 对象,这就是虚拟 DOM,后续渲染全靠它:
go
{
type: 'div' | 'h1' | 'TEXT_ELEMENT', // 节点类型
props: {
style: 'xxx', // 节点属性
children: [] // 子虚拟DOM数组
}
}
3.3 第三步:实现 render 函数,虚拟 DOM 转真实 DOM
render 是 Mini React 的渲染入口函数 ,负责把虚拟 DOM 转化为浏览器可识别的真实 DOM,并挂载到指定容器中,核心逻辑是递归遍历虚拟 DOM 树,逐个创建真实节点、绑定属性、拼接子节点。
javascript
/**
* 虚拟DOM渲染为真实DOM并挂载
* @param {object} element - 虚拟DOM对象
* @param {HTMLElement} container - 真实DOM挂载容器
*/
function render(element, container) {
// 1. 根据虚拟DOM类型,创建对应的真实DOM节点
const dom = element.type === 'TEXT_ELEMENT'
? document.createTextNode('') // 创建文本节点
: document.createElement(element.type) // 创建普通元素节点
// 2. 为真实DOM绑定属性,过滤掉children(非原生DOM属性)
const isNormalProp = key => key !== 'children'
Object.keys(element.props)
.filter(isNormalProp)
.forEach(propName => {
dom[propName] = element.props[propName]
})
// 3. 递归渲染所有子节点,挂载到当前DOM
element.props.children.forEach(child => render(child, dom))
// 4. 将构建好的真实DOM,挂载到页面指定容器
container.appendChild(dom)
}
3.4 第四步:全局挂载 + Babel 配置
想要让 Babel 识别我们自定义的 Mini React,需要将核心函数挂载到 window 全局,同时通过注释配置 Babel 编译规则,告诉 Babel:JSX 要编译成 Didact.createElement 而非默认的 React.createElement。
php
// 挂载到全局,命名为Didact,统一Mini React命名空间
window.Didact = {
createElement,
render
}
// Babel编译配置:经典JSX运行时 + 指定createElement函数
/** @jsxRuntime classic */
/** @jsx Didact.createElement */
四、完整可运行代码(复制即用)
将以下代码复制到 HTML 文件中,直接用浏览器打开,即可看到渲染效果,无需任何额外配置,新手也能轻松上手实操。
xml
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>手写Mini React - Didact</title>
<!-- 引入在线Babel,实时编译JSX -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<!-- 渲染挂载节点 -->
<div id="root"></div>
<!-- type="text/babel" 标识该脚本需要Babel编译 -->
<script type="text/babel">
// 1. 文本虚拟DOM创建
function createTextElement(text) {
return {
type: 'TEXT_ELEMENT',
props: {
nodeValue: text,
children: []
}
}
}
// 2. 核心:创建虚拟DOM
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map(child =>
typeof child === 'object' ? child : createTextElement(child)
)
}
}
}
// 3. 核心:渲染真实DOM
function render(element, container) {
const dom = element.type === 'TEXT_ELEMENT'
? document.createTextNode('')
: document.createElement(element.type)
// 绑定属性
Object.keys(element.props)
.filter(key => key !== 'children')
.forEach(prop => dom[prop] = element.props[prop])
// 递归渲染子节点
element.props.children.forEach(child => render(child, dom))
container.appendChild(dom)
}
// 全局挂载Didact
window.Didact = { createElement, render }
// Babel配置
/** @jsxRuntime classic */
/** @jsx Didact.createElement */
// 测试UI:编写JSX
const demoElement = (
<div style="width: 400px; margin: 50px auto; padding: 24px; background: #ff9e7b; border-radius: 12px;">
<h1 style="color: #fff; margin: 0 0 12px;">Hello Mini React👋</h1>
<p style="color: #fff; font-size: 16px; text-align: right; margin: 0;">我是手写的 Didact</p>
</div>
)
// 获取挂载容器,执行渲染
const root = document.getElementById('root')
Didact.render(demoElement, root)
</script>
</body>
</html>
实操小贴士:直接打开 HTML 文件可能出现跨域问题,推荐使用 VSCode 的 Live Server 插件启动本地服务,一键运行无报错。
五、核心原理复盘:吃透就等于懂了 React 根基
- JSX 不是魔法 :我们写的
<div>xxx</div>,最终会被 Babel 编译成Didact.createElement('div', {}, ...children),所谓的 JSX 语法,只是让 UI 编写更便捷的语法糖。 - 虚拟 DOM 不神秘 :就是一个普通 JS 对象,作用是用 JS 描述 DOM,统一节点格式,避免频繁操作原生 DOM 引发的重绘重排,同时方便后续做 DOM 差异化更新。
- 渲染核心是递归:虚拟 DOM 是树形结构,想要渲染完整 UI,必须通过递归遍历,逐层创建真实节点,最终拼接成完整 DOM 树挂载到页面。
- React 底层极简 :抛开进阶特性,React 最核心的就是虚拟 DOM 生成 + 递归渲染,我们手写的 Mini React,已经涵盖了这一核心逻辑。
六、进阶方向:如何完善 Mini React?
本次实现的是极简版 Mini React,仅完成基础渲染,想要更贴近真实 React,还可以在此基础上扩展:
- 实现 diff 算法:对比新旧虚拟 DOM,只更新变化的节点,而非全量重新渲染,提升性能;
- 实现 类组件/函数组件:支持自定义组件,贴合实际开发场景;
- 实现 setState 状态更新:完成数据驱动视图的核心响应式逻辑;
- 实现 Fiber 调度机制:解决递归渲染的性能瓶颈,实现可中断渲染。
结语
学习前端框架原理,动手实践永远是最好的方式。这几十行代码,没有复杂的封装和晦涩的逻辑,却能让你彻底吃透 React 最核心的渲染流程,看懂虚拟 DOM 和 JSX 的本质。