深入解答React是否必须依赖JSX,详细解析React.createElement底层原理、不用JSX编写原生组件的具体步骤与代码示例,以及HTM零构建替代方案与技术选型指南。
React必须用JSX写吗?不用JSX怎么写出同样组件?
时间:2026-11-13
浏览:210
文章目录
React.createElement 函数调用。即使脱离 Babel、Vite 或 Webpack 等打包工具,开发者也完全可以直接手写纯 JavaScript 函数调用,实现一模一样的组件逻辑、生命周期与状态响应。
一、底层揭秘:JSX 到底被编译成了什么?
初学者容易产生一个误解:认为 React 框架自身能直接“看懂”尖括号语法。实际上,React 官方团队在最初设计 React 时,核心目标是构建一套面向“虚拟 DOM(Virtual DOM)”的函数式组件系统,而虚拟 DOM 的实体不过是一个普通的 JavaScript 对象。
当我们在代码中写下一段看似 HTML 的 JSX 标签时:
// 开发者书写的 JSX
const element = <h1 className="title">你好,React</h1>;
打包工具(如 Babel 或 SWC)在编译构建时,会将其机械式地替换为以下原生 JavaScript 调用:
// 经典运行时(Classic Runtime)转换后的纯 JavaScript
const element = React.createElement('h1', { className: 'title' }, '你好,React');
React.createElement 的函数签名非常规整:
React.createElement(type, [props], [...children])
type:表示元素的类型。如果传字符串(如'div'、'h1'、'button'),代表原生 DOM 标签;如果传函数或类(如UserCard、App),则代表自定义 React 组件。props:传递给该元素的属性对象,包含id、className、样式对象、自定义参数以及onClick等事件处理函数。若无属性,传入null或空对象{}。...children:后续的所有参数都会作为该元素的子节点。子节点可以是普通文本字符串、数字,也可以是另一个React.createElement返回的子虚拟节点对象。
函数执行完毕后,返回的内存对象大致结构如下:
// 内存中实际生成的 React Element 节点对象
{
$$typeof: Symbol(react.element),
type: 'h1',
key: null,
ref: null,
props: {
className: 'title',
children: '你好,React'
}
}
React 内部的协调引擎(Reconciler)只关心这个对象里的 type 和 props,完全不在乎这个对象是由 Babel 翻译出来的,还是由开发者直接调用函数构造出来的。
React 17 引入了自动 JSX 运行时(Automatic Runtime),编译器会将 JSX 转译为从
react/jsx-runtime 导出的 _jsx 或 _jsxs 函数,从而免去手动 import React from 'react'。但这一改动的本质依然是语法糖到纯 JS 函数调用的映射,对组件本身的运行模型和虚拟 DOM 结构没有任何改变。
二、不使用 JSX 编写组件的四个关键步骤
理解了函数调用的对应关系后,编写纯 JavaScript 组件就像拼装常规函数一样清晰。我们可以按照由浅入深的步骤逐步构建完整功能的组件。
步骤 1:创建单级原生 DOM 元素
对于没有子层级或仅有纯文本内容的单标签,直接传入标签名与属性即可:
// 渲染一个带样式和ID的标题
const header = React.createElement(
'h2',
{ id: 'greeting', className: 'text-highlight' },
'欢迎访问控制台'
);
步骤 2:组织多层树形嵌套
当元素包含子元素时,只需要把子元素的 React.createElement 作为父函数的第三、第四个参数连续传入,或者直接传入一个子元素数组:
// 构建一个包含标题与正文段落的卡片结构
const card = React.createElement(
'div',
{ className: 'user-card' },
React.createElement('h3', null, '用户详情'),
React.createElement('p', null, '这是第一行介绍文字。'),
React.createElement('p', null, '这是第二行补充信息。')
);
步骤 3:定义与渲染自定义函数组件
在 React 中,函数组件本质上就是一个“接收 props 并返回虚拟 DOM 对象”的纯 JavaScript 函数。如果不用 JSX,只需要让该函数返回 React.createElement 的结果:
// 定义一个通用的徽标组件 Badge
function Badge(props) {
const style = {
backgroundColor: props.color || '#687D8D',
color: '#FFFFFF',
padding: '3px 8px',
borderRadius: '4px',
fontSize: '12px'
};
return React.createElement('span', { style: style }, props.text);
}
// 渲染自定义组件:将函数名 Badge 作为第一个参数
const myBadge = React.createElement(Badge, {
color: '#879B88',
text: '已审核'
});
步骤 4:处理状态管理、Hooks 与交互事件
有些开发者会疑惑:“不用 JSX,还能用 useState、useEffect 吗?”答案是完全不受任何影响。Hooks 是运行在函数组件执行上下文里的 JavaScript 函数,它与渲染模板语法毫无耦合:
function Counter() {
// 正常使用 Hook
const [count, setCount] = React.useState(0);
// 绑定事件处理器
return React.createElement(
'div',
{ className: 'counter-wrapper' },
React.createElement('p', null, `当前累计数值:${count}`),
React.createElement(
'button',
{
className: 'btn-primary',
onClick: function() {
setCount(count + 1);
}
},
'增加计数'
)
);
}
在渲染数组时,
key 必须写在 props 对象中,而不是作为后续参数传递。例如:items.map(item => React.createElement('li', { key: item.id }, item.name))。React 协调引擎会在该层级直接读取 props.key。
三、完整可运行代码:在零构建的纯 HTML 中运行 React
为了直观验证这种写法的可用性,下面给出一个无需安装 Node.js、无需 npm 安装任何包、也不依赖 Babel 的完整 HTML 页面。读者可以将以下代码保存为一个单独的 .html 文件,在任意现代浏览器中双击即可直接运行并交互。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>纯 JS 运行 React 示例</title>
<!-- 1. 直接通过 CDN 引入官方预编译好的 React 与 ReactDOM 生产包 -->
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<style>
body { font-family: sans-serif; padding: 24px; }
.todo-card { border: 1px solid #ccc; padding: 16px; border-radius: 8px; max-width: 320px; }
.todo-list { margin: 12px 0; padding-left: 20px; }
button { padding: 6px 12px; cursor: pointer; }
</style>
</head>
<body>
<!-- 页面挂载容器 -->
<div id="root"></div>
<script>
// 2. 为了书写简洁,可以为 createElement 声明短别名
const h = React.createElement;
// 3. 定义子组件 TodoItem
function TodoItem(props) {
return h('li', { className: 'todo-item' }, props.title);
}
// 4. 定义包含状态管理的 TodoApp 组件
function TodoApp() {
const [todos, setTodos] = React.useState(['学习 React 原理', '掌握 createElement']);
const [inputVal, setInputVal] = React.useState('');
function handleAdd() {
if (!inputVal.trim()) return;
setTodos([...todos, inputVal]);
setInputVal('');
}
return h(
'div',
{ className: 'todo-card' },
h('h3', null, '待办清单(零构建运行)'),
h(
'ul',
{ className: 'todo-list' },
todos.map(function(item, index) {
return h(TodoItem, { key: index, title: item });
})
),
h('input', {
type: 'text',
value: inputVal,
placeholder: '输入新任务...',
onChange: function(e) { setInputVal(e.target.value); }
}),
h('button', { onClick: handleAdd, style: { marginLeft: '8px' } }, '添加')
);
}
// 5. 挂载到 DOM
const container = document.getElementById('root');
const root = ReactDOM.createRoot(container);
root.render(h(TodoApp));
</script>
</body>
</html>
运行效果说明:双击在浏览器打开此网页后,页面会立即渲染出一个交互式任务卡片。输入文本点击“添加”,列表会正常利用 Virtual DOM Diff 完成局部重绘。整个过程没有任何转译器介入,源码即是浏览器直接运行的执行码。
四、深度对比:JSX 与纯 createElement 的差异与取舍
既然纯 JavaScript 完全可以写出同样的组件,为什么业界绝大多数项目仍然选择 JSX?为了让读者建立客观的认知,我们在一致条件下,从四个工程维度进行深入对比:
1. 工具链依赖与环境成本
JSX 无法直接被浏览器解释,这意味着项目必须引入打包工具(如 Webpack、Vite、Rollup)以及转译插件(Babel、SWC 或 esbuild)。对于现代中大型单页应用(SPA),由于本身就需要打包静态资源,这一成本是可以接受的;但在一些轻量脚本、单页面小工具、或者受限无法安装 Node 环境的内网开发中,原生 createElement 具备随开随用的绝对优势。
2. UI 结构直观性与维护成本
UI 界面具有天然的树状层级关系。JSX 的 HTML 标签语法让“父子、兄弟”层级一目了然,属性与内容的归属也非常贴近视觉排版习惯;相比之下,随着 DOM 树深度的增加,连续嵌套的 React.createElement 会产生大量的括号和缩进层级,阅读体验和心智负担明显上升。
3. 动态元编程与数据驱动构建
这是一个 React.createElement 反向优于 JSX 的典型场景。在低代码引擎、表单生成器或根据后端 JSON 接口动态渲染组件时,节点类型往往是以变量或字符串形式下发的。例如:
// 根据后端返回的数据结构动态生成组件
const schema = [
{ tag: 'h4', text: '模块一' },
{ tag: 'p', text: '描述一' },
{ tag: 'button', text: '确认' }
];
// 使用 createElement 一行 map 即可优雅完成动态构造
const elements = schema.map((item, index) =>
React.createElement(item.tag, { key: index }, item.text)
);
如果用 JSX 来写这段动态逻辑,则必须提前将动态标签转换为大写变量名或者写大量的 switch/case 判断,灵活性远不如直接调用函数。
4. 调试直观度与 Sourcemap 依赖
在 JSX 模式下,开发者看到的代码和浏览器调试器运行的代码并不一致,排查深层执行问题必须依赖 Sourcemap 映射文件。如果映射失效或配置有误,堆栈信息就会指向难以阅读的转译代码;而原生 createElement 代码就是实际运行代码,控制台断点直接落在函数参数上,传值逻辑非常透明。
五、实用进阶方案:使用 HTM 兼顾“零构建”与“类 JSX 体验”
如果不想配置繁重的 Webpack 或 Vite 构建流,但又觉得 React.createElement 写多层嵌套过于繁琐,社区提供了一个非常成熟的折中方案——HTM(Hyperscript Tagged Markup)。
HTM 利用了现代 JavaScript 原生支持的“标签模板字符串(Tagged Template Literals)”特性。它在浏览器中直接解析模板字符串,并在底层自动将其编译为 React.createElement 的调用:
<!-- 引入 htm 绑定库 -->
<script src="https://unpkg.com/htm@3.1.1/dist/htm.umd.js"></script>
<script>
// 将 htm 绑定到 React.createElement 上
const html = htm.bind(React.createElement);
function Banner(props) {
// 书写风格与 JSX 99% 一致,且纯原生运行,不需要 Babel
return html`
<div class="banner-box">
<h2>${props.title}</h2>
<p>这是通过原生 ES6 模板字符串渲染出的 React 组件!</p>
</div>
`;
}
</script>
对于想要轻量内嵌 React、制作教学 Demo 或编写浏览器油猴脚本(Tampermonkey)的场景,HTM 是目前体验最好的零编译方案之一。
六、总结与技术选型建议
回到文章最初的问题:React 必须用 JSX 写吗?
答案是清晰而确定的:不是必须。JSX 只是一层提升开发体验的外壳,React 的真正生命线是其背后的虚拟 DOM 数据结构与组件渲染逻辑。不用 JSX,调用 React.createElement 可以写出功能、性能与交互完全一致的组件。
在实际开发中,可以参考以下决策依据:
- 首选 JSX:面对绝大多数常规 Web 商业项目、团队协作项目和具有成熟脚手架(如 Vite、Next.js)的环境,JSX 拥有最强的语法直观度和社区生态,依然是效率最高的首选。
- 选用 React.createElement:当开发低代码渲染器、根据配置规则动态生成 UI 树,或者在无编译环境的单 HTML 页面中进行轻量概念验证时,直接使用原生函数调用更加轻巧、精确且没有黑盒。
- 选用 HTM:在不希望引入 Node 构建工具链,但又需要编写较复杂、嵌套较深的组件页面时,HTM 提供了兼顾简洁排版与零构建运行的最佳平衡点。
实用工具推荐
绿化、压缩、噪点和虚拟水印,自由调节做旧程度。
选择画质和输出格式,预览压缩前后的大小。