第 2 章:JSX 与 React 元素
你每天都在写 JSX,但 React 运行时可从来没见过 JSX。它收到的,是 JSX 编译后产生的一串 jsx() 调用。先看一个例子:
// 你写的:
const app = <div className="app">你好</div>;
Babel 或 SWC 会把它编译成:
// 编译产物:
const app = jsx('div', {className: 'app', children: '你好'});
把 app 打印到控制台,你会看到:
{
$$typeof: Symbol(react.transitional.element),
type: 'div',
key: null,
props: {className: 'app', children: '你好'}
}
这个对象就是 React 元素。
先记住这一句:JSX 是写给编译器看的装修图纸写法,编译成
jsx()调用后,返回的 React 元素才是一张 React 自己看得懂的图纸。整本书里,元素处处出现,Fiber 处处出现,两者的关系是:元素是图纸,Fiber 是按图纸施工出来的结构。
一图流:JSX 到 React 元素
jsx() 做了三件事
jsx() 的生产实现叫 jsxProd,在 ReactJSXElement.js:282。它拿着 type、config(就是编译出来的 props 对象)和 maybeKey,干三件事。
第一件,把 key 摘出来。key 是给 diff 用的标记,不该混进普通 props。jsxProd 先检查 maybeKey(编译器把静态 key 作为第三个参数传进来),再检查 config.key(spread 进来的 key):
if (maybeKey !== undefined) {
key = '' + maybeKey;
}
if (hasValidKey(config)) {
key = '' + config.key;
}
第二件,整理 props。多数情况下 config 是编译器新建的对象,直接复用当 props 就行:
if (!('key' in config)) {
props = config; // 直接复用,省一次拷贝
} else {
props = {};
for (const propName in config) {
if (propName !== 'key') {
props[propName] = config[propName]; // 把 key 从 props 里剔掉
}
}
}
第三件,调 ReactElement,把这三个东西组装成元素对象。工厂函数在 ReactJSXElement.js:161。
React 元素长什么样
生产环境下的元素结构,就四个字段加一个标记:
{
$$typeof: REACT_ELEMENT_TYPE, // Symbol 标记
type, // 是什么:'div'、函数组件、类组件……
key, // diff 用(第 7 章)
ref, // 兼容字段,见下
props, // 全部属性,含 children
}
$$typeof 是防伪标记。它是一个全局 Symbol,在 ReactSymbols.js:18 里定义。React 19 把它从 Symbol.for('react.element') 改名成了 Symbol.for('react.transitional.element')。为什么需要它?第 8 章会看到,渲染器拿到对象后会先验 $$typeof 再当元素处理。这有个实际意义:防 XSS。早年有人用服务端返回的 JSON 伪造 React 元素,JSON.parse 出来的对象里不会有 Symbol,验 $$typeof 直接拦下。动手实验 3 你会亲手试到。
ref 在 19 里搬家了。老版本的 ref 是元素上的特殊字段。19 起它变成普通 prop,源码里注释写得很明白(ReactJSXElement.js:162):
// 把 props.ref 当作唯一真相源
const refProp = props.ref;
const ref = refProp !== undefined ? refProp : null;
元素顶层的 ref 字段保留着,但 dev 下是不可枚举的 getter,一访问就报废弃警告,纯粹为兼容旧代码。这个改动就是 forwardRef 能退休的原因,第 3 章讲 Fiber 的 ref 字段时会再碰见它。
key 为什么住在元素上,不住 props 里
你在 <li key={item.id}> 里写的 key,jsx() 会把它从 props 里剔出来,单独挂在元素的 key 字段上。你在组件里 props.key 永远拿不到它。
设计原因在第 7 章(多节点 diff)会展开:React 靠 key 判断"这个节点在上一次渲染里是不是同一个",它是给协调算法读的,不是给组件读的。把它和普通 props 分开,组件就不用担心误读,diff 也能直接取。
jsx() 和 createElement() 有什么不一样
createElement 是老接口,React.createElement 就是它,实现也在同一个文件里(ReactJSXElement.js:560)。三个差别值得知道:
- children 的传法:
jsx里 children 是 props 的一个键;createElement里 children 是第三个参数之后的所有参数,编译时代码会帮你拼进 props。手写createElement时多传的参数会按顺序成为 children 数组。 - 过滤字段:
createElement还会过滤__self、__source(旧 JSX 转换留下的调试字段);jsx不需要,因为新转换根本不产生它们。 - props 对象:
jsx直接复用 config,createElement总是新建 props 对象。
现代编译器和 React 19 都默认走 jsx(react/jsx-runtime),createElement 是历史遗留的兼容入口。
动手实验
- 扒开一个元素:在你的 React 项目里写
const el = <div id="x">hi</div>;然后console.dir(el)。对照上面的字段:type是'div'、key是null、$$typeof是 Symbol。再写const fn = () => null; const el2 = <fn />;,看看type变成什么。 - 验证 ref 搬家:
const el = <div ref={myRef} />;,console.dir(el.props),你会发现ref躺在 props 里。再访问一次el.ref,dev 模式下应该弹废弃警告。 - 亲手试防伪:在控制台执行
再对照 isValidElement:const fake = JSON.parse('{"type":"div","props":{}}');console.log(fake.$$typeof); // undefined
isValidElement(fake)返回false,因为$$typeof对不上。
小结
- JSX 是语法糖,编译成
jsx()调用;React 运行时只认元素对象。 - 元素是普通对象:
$$typeof/type/key/props,_owner仅 dev 有。 - 19 的改动:
ref并入 props,$$typeof符号改名,forwardRef退休。 - key 单独挂元素上,是给 diff 用的,不污染 props。
- 元素是"图纸",下一步要施工。第 3 章看 Fiber:图纸是怎么变成一棵能跑起来的树的。