Appearance
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-26将jsx、jsxs、jsxDEV暴露给编译后的代码,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 - 为什么区分
jsx和jsxs?jsxs代表“静态子节点数组”的场景,能在 DEV 下更早冻结 children,减少无意的变更:react/packages/react/src/jsx/ReactJSX.js:20-26,react/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-236type、key、props是三大核心字段。- 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
- DEV 下访问会注入 getter 并提示你改为显式传递:
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 stack与debug 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 下走不同的校验与调试分支。 - 这些看似“吹毛求疵”的约束(
key、ref、children 冻结),最终都是为了让协调器的工作更可预测,错误更可定位,性能更稳定。