Skip to content

2.1 JSX 与 React.createElement 的幕后

我们先问一句看起来很“幼稚”的问题:JSX 到底是什么?它不是语法糖的总结,而是一个编译期协议。React 19 把这个协议的运行时落在 jsx/jsxs/jsxDEV 三个入口上;同时保留了历史上的 createElement 作为“纯运行时工厂”。这两套路径的差异,正好能解释 React 为什么在 19 里重构了元素创建的细节。

JSX 运行时:jsx / jsxs / jsxDEV

  • 入口位置:react/packages/react/src/jsx/ReactJSX.js:17-26jsxjsxsjsxDEV 暴露给编译后的代码,Fragment 则直接映射到符号 REACT_FRAGMENT_TYPE
  • 真实工厂在 ReactJSXElement.js
    • 生产环境的 jsxProd 负责“干净地”生成元素对象,保留 key 与非保留属性,避免多余变形:react/packages/react/src/jsx/ReactJSXElement.js:290-339
    • 开发环境的 jsxDEV 额外做三件事:校验 children 的 key、为 spread 进来的 key 打警告、注入更友好的调试栈:react/packages/react/src/jsx/ReactJSXElement.js:432-587
    • 为什么区分 jsxjsxsjsxs 代表“静态子节点数组”的场景,能在 DEV 下更早冻结 children,减少无意的变更:react/packages/react/src/jsx/ReactJSX.js:20-26react/packages/react/src/jsx/ReactJSXElement.js:389-422

元素对象长什么样:ReactElement

  • 元素的规范结构在 ReactElement 中被一次性创建:react/packages/react/src/jsx/ReactJSXElement.js:169-238
    • $$typeof: REACT_ELEMENT_TYPE 标记这是一个 React 元素:react/packages/react/src/jsx/ReactJSXElement.js:188-193,228-236
    • typekeyprops 是三大核心字段。
    • DEV 下还挂了 _owner_debugStack_debugTask,用于更精确的报错与归因:react/packages/react/src/jsx/ReactJSXElement.js:197-205,257-275
  • React 19 关于 ref 的改变:
    • ref 不再是“特殊的外部参数”,而是常规 props 的一部分;访问 element.ref 在 DEV 会给出弃用提醒:react/packages/react/src/jsx/ReactJSXElement.js:142-159
    • 这背后的动机是把 ref 从“魔法”拉回“普通属性”,减少不同入口的行为差异。

key 的语义与约束

  • key 永远不是一个“真正的 props 值”,它只参与元素身份区分,不应通过 props.key 访问:
    • DEV 下访问会注入 getter 并提示你改为显式传递:react/packages/react/src/jsx/ReactJSXElement.js:120-139
  • key 的来源优先级与规范化:
    • 来自第三参数 maybeKey(编译器生成)或来自 config.key,都被强制字符串化并做字符串强制校验:react/packages/react/src/jsx/ReactJSXElement.js:299-311,538-549
  • 为什么反对“把带 key 的对象 spread 进来”?这会让 key 的意图隐藏在对象结构里,降低可读性与工具可分析性:react/packages/react/src/jsx/ReactJSXElement.js:503-528

JSX 与 createElement 的根本区别

  • JSX 是“编译器协议”,React 可以假设编译器总是传入一个新的 config 对象;因此在没有 key 的情况下可以直接复用 config,避免对象重建:react/packages/react/src/jsx/ReactJSXElement.js:313-336
  • createElement 是“手写运行时 API”,React 不能假设调用方总是传新对象,所以会显式拷贝并过滤保留字段(key__self__source):react/packages/react/src/jsx/ReactJSXElement.js:611-662
  • children 处理也略有差异:
    • JSX 由编译器保证 children 形态;
    • createElement 允许多参数 children,并在 DEV 冻结数组避免意外变更:react/packages/react/src/jsx/ReactJSXElement.js:664-680
  • 另外,createElement 在 DEV 下会提示你正在使用过时的 JSX transform(通过 __self 检测),引导迁移到现代编译器:react/packages/react/src/jsx/ReactJSXElement.js:618-636

为什么要这么设计?

  • 把“编译期语义”和“运行时语义”分开,是为了让 React 能在不破坏生态的前提下优化常见路径:
    • 编译器能生成更可分析的调用(jsx/jsxs),React 在运行时就少做无意义工作(比如不必总是拷贝 props)。
    • 运行时保留 createElement 兼容性,但在 DEV 下给出更明确的迁移提示和异常归因栈(owner stackdebug task)。
  • ref 作为常规 props 统一到同一个入口,有助于减少“额外通道”的复杂度,也让未来的静态分析更简单:react/packages/react/src/jsx/ReactJSXElement.js:142-159,169-179
  • key 的强约束(禁止通过 spread 隐藏、强制字符串化与校验)是为了服务协调器的 Diff:隐式 key 难以推断,也更容易制造“看不见的 bug”。

运行时校验与调试栈

  • Child keys 的校验在 DEV 下进行,并把“静态位置的元素”标记为已验证,避免每帧重复告警:react/packages/react/src/jsx/ReactJSXElement.js:840-858
  • 是否是有效元素通过 $$typeof 检查,不依赖构造器或原型链:react/packages/react/src/jsx/ReactJSXElement.js:867-874
  • 为了更好地指向“谁创建了元素”,React 在 DEV 里构造了一个受限的堆栈与任务对象:react/packages/react/src/jsx/ReactJSXElement.js:66-77,83-94,360-386,410-421

小结:把“语义”放在对的位置

  • JSX 是协议,createElement 是兼容层;二者最终都落到同一个元素工厂 ReactElement,只是在 DEV 下走不同的校验与调试分支。
  • 这些看似“吹毛求疵”的约束(keyref、children 冻结),最终都是为了让协调器的工作更可预测,错误更可定位,性能更稳定。

Last updated: