React必须用JSX写吗?不用JSX怎么写出同样组件?

作者:晚风写码时间:2026-11-13 22:52:32浏览:209

深入解答React是否必须依赖JSX,详细解析React.createElement底层原理、不用JSX编写原生组件的具体步骤与代码示例,以及HTM零构建替代方案与技术选型指南。

核心答案:React 完全不强制要求使用 JSX。JSX 本质上只是为了方便开发者书写 UI 树而设计的语法糖,任何现代浏览器都无法直接解析 JSX。在实际执行前,编译工具会将所有的 JSX 语法展开为纯 JavaScript 的 React.createElement 函数调用。即使脱离 Babel、Vite 或 Webpack 等打包工具,开发者也完全可以直接手写纯 JavaScript 函数调用,实现一模一样的组件逻辑、生命周期与状态响应。
React必须用JSX写吗?不用JSX怎么写出同样组件?
缩略导读:JSX 语法糖转译为 React 原生函数调用全流程

一、底层揭秘: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必须用JSX写吗?不用JSX怎么写出同样组件?
图一:JSX 从源码经过编译解析最终生成内存虚拟 DOM 对象的执行链路
关于 React 17+ 新 JSX 转换运行时的说明:
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);
        }
      },
      '增加计数'
    )
  );
}
React必须用JSX写吗?不用JSX怎么写出同样组件?
图二:不用 JSX 编写组件的四个递进式操作步骤
避坑提醒:动态列表中的 key 属性位置
在渲染数组时,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?为了让读者建立客观的认知,我们在一致条件下,从四个工程维度进行深入对比:

React必须用JSX写吗?不用JSX怎么写出同样组件?
图三:JSX 模式与原生 createElement 模式核心维度对照

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 可以写出功能、性能与交互完全一致的组件。

React必须用JSX写吗?不用JSX怎么写出同样组件?
图四:React 开发模式与场景选型决策指南

在实际开发中,可以参考以下决策依据:

  • 首选 JSX:面对绝大多数常规 Web 商业项目、团队协作项目和具有成熟脚手架(如 Vite、Next.js)的环境,JSX 拥有最强的语法直观度和社区生态,依然是效率最高的首选。
  • 选用 React.createElement:当开发低代码渲染器、根据配置规则动态生成 UI 树,或者在无编译环境的单 HTML 页面中进行轻量概念验证时,直接使用原生函数调用更加轻巧、精确且没有黑盒。
  • 选用 HTM:在不希望引入 Node 构建工具链,但又需要编写较复杂、嵌套较深的组件页面时,HTM 提供了兼顾简洁排版与零构建运行的最佳平衡点。
作者最新文章
相关文章
更多