Skip to main content

第 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。它拿着 typeconfig(就是编译出来的 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 都默认走 jsxreact/jsx-runtime),createElement 是历史遗留的兼容入口。

动手实验

  1. 扒开一个元素:在你的 React 项目里写 const el = <div id="x">hi</div>; 然后 console.dir(el)。对照上面的字段:type'div'keynull$$typeof 是 Symbol。再写 const fn = () => null; const el2 = <fn />;,看看 type 变成什么。
  2. 验证 ref 搬家const el = <div ref={myRef} />;console.dir(el.props),你会发现 ref 躺在 props 里。再访问一次 el.ref,dev 模式下应该弹废弃警告。
  3. 亲手试防伪:在控制台执行
    const fake = JSON.parse('{"type":"div","props":{}}');
    console.log(fake.$$typeof); // undefined
    再对照 isValidElementisValidElement(fake) 返回 false,因为 $$typeof 对不上。

小结

  • JSX 是语法糖,编译成 jsx() 调用;React 运行时只认元素对象。
  • 元素是普通对象:$$typeof / type / key / props_owner 仅 dev 有。
  • 19 的改动:ref 并入 props,$$typeof 符号改名,forwardRef 退休。
  • key 单独挂元素上,是给 diff 用的,不污染 props。
  • 元素是"图纸",下一步要施工。第 3 章看 Fiber:图纸是怎么变成一棵能跑起来的树的。