React 作为现代前端框架的代表,其核心思想包括组件化、响应式、虚拟 DOM 等。想要真正理解 React 的底层原理,最好的方式就是手写一个 Mini React。本文将从 JSX 转译到虚拟 DOM 渲染,带你一步步实现 React 的核心功能。
JSX 的本质
JSX 是 React 最具特色的语法糖,它允许我们在 JavaScript 中直接编写类似 HTML 的标签。这种声明式的写法让代码更加直观:
jsx
javascript
let userList = (
<div>
<h2>用户列表</h2>
{users.map(user => <p key={user.id}>{user.name}</p>)}
</div>
)
但浏览器并不认识 JSX,它需要通过 Babel 转译成 React.createElement 函数调用。这就是我们要实现的第一个核心函数。
实现 createElement 函数
createElement 是整个 React 渲染流程的起点,它接收三个参数:
type: 元素类型(如 'div'、'h1' 或组件)props: 元素属性对象children: 子元素(可变参数)
核心实现如下:
javascript
typescript
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map(child =>
typeof child === 'object'
? child
: createTextElement(child)
)
}
}
}
这个函数的关键在于处理 children。我们需要遍历所有子元素,如果子元素已经是对象(虚拟 DOM),则直接使用;如果是字符串或数字,则调用 createTextElement 转换为文本节点。
为什么要统一处理文本节点?
在 React 中,为了统一渲染逻辑,文本节点也需要包装成虚拟 DOM 对象:
javascript
arduino
function createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: []
}
}
}
这样做的好处是,无论是元素节点还是文本节点,都遵循相同的数据结构,render 函数可以用统一的方式处理。
虚拟 DOM 的数据结构
经过 createElement 处理后,我们得到的虚拟 DOM 结构是这样的:
javascript
less
{
type: 'div',
props: {
style: 'background:salmon',
children: [
{
type: 'h1',
props: {
children: [
{ type: 'TEXT_ELEMENT', props: { nodeValue: 'Hello, world!', children: [] } }
]
}
},
{
type: 'h2',
props: {
style: 'text-align:right',
children: [
{ type: 'TEXT_ELEMENT', props: { nodeValue: 'from Didact', children: [] } }
]
}
}
]
}
}
这个树形结构完整描述了 DOM 的层级关系和属性,但它只是 JavaScript 对象,还没有真正渲染到页面上。
实现 render 函数
render 函数负责将虚拟 DOM 转换为真实 DOM 并挂载到页面。它的实现分为几个步骤:
1. 创建 DOM 节点
根据虚拟 DOM 的 type 创建对应的真实节点:
javascript
go
const dom = element.type === 'TEXT_ELEMENT'
? document.createTextNode('')
: document.createElement(element.type);
文本节点使用 createTextNode,普通元素使用 createElement。
2. 添加属性
遍历 props 对象,将属性赋值给 DOM 节点。这里需要过滤掉 children 属性,因为它不是 DOM 的原生属性:
javascript
ini
const isProperty = key => key !== 'children';
Object.keys(element.props)
.filter(isProperty)
.forEach(name => {
dom[name] = element.props[name];
});
这段代码会将 style、className 等属性直接赋值到 DOM 对象上。例如 dom['style'] = 'background:salmon' 等同于 dom.style = 'background:salmon'。
3. 递归渲染子元素
对每个 children,递归调用 render 函数:
javascript
ini
element.props.children.forEach(child => render(child, dom));
这里体现了递归的"递"过程------自顶向下创建节点。
4. 挂载到父容器
最后将创建好的 DOM 节点挂载到父容器:
javascript
ini
container.appendChild(dom);
这是递归的"归"过程------自底向上完成挂载。
完整的渲染流程
将所有代码整合,我们就得到了一个完整的 Mini React:
javascript
ini
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map(child =>
typeof child === 'object'
? child
: createTextElement(child)
)
}
}
}
function createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: []
}
}
}
function render(element, container) {
const dom = element.type === 'TEXT_ELEMENT'
? document.createTextNode('')
: document.createElement(element.type);
const isProperty = key => key !== 'children';
Object.keys(element.props)
.filter(isProperty)
.forEach(name => {
dom[name] = element.props[name];
});
element.props.children.forEach(child => render(child, dom));
container.appendChild(dom);
}
window.Didact = {
createElement,
render,
}
使用示例
配置 JSX 转译:
javascript
javascript
/** @jsxRuntime classic */
/** @jsx Didact.createElement */
const element = (
<div style="background:salmon">
<h1>Hello, world!</h1>
<h2 style="text-align:right">from Didact</h2>
</div>
)
const container = document.getElementById('root');
Didact.render(element, container);
通过注释 /** @jsx Didact.createElement */,我们告诉 Babel 将 JSX 转译为 Didact.createElement 调用,而不是默认的 React.createElement。
React 做了什么?
通过手写这个 Mini React,我们可以更清晰地理解 React 的价值:
- 声明式 UI: 开发者只需描述"想要什么样的界面",而不用关心"如何操作 DOM"
- 虚拟 DOM: 将 UI 抽象为 JavaScript 对象,为后续的 diff 算法和性能优化打下基础
- 统一的数据结构 : 通过
createElement将所有节点统一处理,简化了渲染逻辑
React 帮我们处理了繁琐的 DOM 操作(如重绘、重排),让开发者可以专注于业务逻辑和数据流转。
总结
本文通过实现 createElement 和 render 两个核心函数,揭示了 React 从 JSX 到真实 DOM 的完整流程:
- JSX 经 Babel 转译为
createElement调用 createElement生成虚拟 DOM 树render递归遍历虚拟 DOM,创建真实节点并挂载
这只是 React 原理的冰山一角。真正的 React 还包括 Fiber 架构、调度器、Hooks、diff 算法等复杂机制。但理解这个 Mini 版本,是深入 React 源码的第一步。
如果你想继续深入,可以尝试实现:
- 函数组件和类组件的支持
- 事件系统
- 简单的 diff 算法
- 基础的 Hooks
学习框架最好的方式,就是动手实现一个。