什么是 JSX ?

我们使用 React 时最大的感受是它让可以用一种更直观、更声明式的方式来描述用户界面,即 JSX。JSX 的语法类似于 HTML,但它并不是 HTML,而是 JavaScript 的一种语法扩展。

JSX 的本质

既然 JSX 并不是标准的三大件(HTML、CSS、JavaScript),那么它必然需要被转换(Transform)为标准的 JavaScript 代码才能在浏览器中运行。最常用的转换包括:

  • Babel
// 使用 @babel/preset-react 预设 { "presets": ["@babel/preset-react"] }
  • TypeScript
// tsconfig.json { "compilerOptions": { "jsx": "react" } }
  • ESBuild
esbuild.build({ entryPoints: ["app.jsx"], bundle: true, outfile: "out.js", jsx: "transform", });
  • SWC (Speedy Web Compiler):用 Rust 编写的快速 JavaScript/TypeScript 编译器,支持 JSX 转换
// .swcrc { "jsc": { "parser": { "syntax": "ecmascript", "jsx": true }, "transform": { "react": { "pragma": "React.createElement", "pragmaFrag": "React.Fragment" } } } }

Babel Transform

本节以 Babel 为准,其他编译器处理方式类似。

INFO

@babel/preset-react 7.25.7 版本为准,主要包含以下两个插件:

转换行为在未来可能会改变

@babel/preset-react 包含两种 runtime:

  • automatic:自动选择 runtime
  • classic:经典 runtime

目前来说,automatic 是主流的 runtime,无需手动引入 React

以如下的代码为例:

function Child() { return <div>Child</div>; } function App() { const element = <h1 style={{ color: "red" }}>Hello, world!</h1>; console.log("🚀 ~ element:", element); return ( <div> {element} <Child /> </div> ); }

会被转换为:

// 自动引入 jsx 和 jsxs 函数 import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; function Child() { return /*#__PURE__*/ _jsx("div", { children: "Child", }); } function App() { const element = /*#__PURE__*/ _jsx("h1", { style: { color: "red", }, children: "Hello, world!", }); console.log("🚀 ~ element:", element); return /*#__PURE__*/ _jsxs("div", { children: [element, " ", /*#__PURE__*/ _jsx(Child, {})], }); }

而采用的 classic runtime,还必须手动引入 React,否则会报错。

import React from "react";

转换结果如下:

// 与 automatic 的区别,需要引入 React,否则 React.createElement 会报错 function Child() { return /*#__PURE__*/ React.createElement("div", null, "Child"); } function App() { const element = /*#__PURE__*/ React.createElement( "h1", { style: { color: "red", }, }, "Hello, world!" ); console.log("🚀 ~ element:", element); return /*#__PURE__*/ React.createElement( "div", null, element, " ", /*#__PURE__*/ React.createElement(Child, null) ); }

在开发模式下,指定 developmenttrue,可以提供开发所需要的调试信息。

{ "presets": [ [ "@babel/preset-react", { "development": true, "runtime": "automatic" } ] ] }

代码转换结果如下:

var _jsxFileName = "xxx.jsx"; import { jsxDEV as _jsxDEV } from "react/jsx-dev-runtime"; function Child() { return /*#__PURE__*/ _jsxDEV( "div", { children: "Child", }, void 0, false, { fileName: _jsxFileName, lineNumber: 2, columnNumber: 10, }, this ); } function App() { const element = /*#__PURE__*/ _jsxDEV( "h1", { style: { color: "red", }, children: "Hello, world!", }, void 0, false, { fileName: _jsxFileName, lineNumber: 5, columnNumber: 19, }, this ); console.log("🚀 ~ element:", element); return /*#__PURE__*/ _jsxDEV( "div", { children: [ element, " ", /*#__PURE__*/ _jsxDEV( Child, {}, void 0, false, { fileName: _jsxFileName, lineNumber: 9, columnNumber: 17, }, this ), ], }, void 0, true, { fileName: _jsxFileName, lineNumber: 8, columnNumber: 5, }, this ); }

babel playground

无论是 automatic 还是 classic。本质都是将 JSX 转换成为函数调用,而函数执行后最终会返回一个 ReactElement 的数据结构。

比如上面的 element 就是 ReactElement 的数据结构,大概长这样:

React 19 对 JSX 的升级

React 19(包括 18 ) 对 JSX 的升级提案主要体现在以下几个方面:

  • 废弃"模块模式"组件
  • 废弃函数组件上的 defaultProps
  • 废弃从对象中展开 key
  • 废弃字符串 refs(并移除生产模式的 _owner 字段)
  • ref 提取移至类组件 render 时机和 forwardRef render 时机
  • defaultProps 解析移至类组件 render 时机
  • 更改 JSX 转译器以使用新的元素创建方法
    • 始终将 children 作为 props 传递
    • key 与其他 props 分开传递
    • 在开发环境中
      • 传递一个标志来确定是否为静态
      • __source__self 与其他 props 分开传递

目标是最终移除对 forwardRef 的需求,使元素创建变得简单:

function jsx(type, props, key) { return { $$typeof: ReactElementSymbol, type, key, props, }; }

升级提案需要两方面的配合:

  1. Babel 对 JSX 处理的升级
  2. React 对 JSX 转换函数的逻辑升级

Babel 对 JSX 处理的升级

runtime 为automatic

在开发模式下,即:使用 @babel/plugin-transform-react-jsx-development。会将绝大部分 JSX 转换为 jsxDEV 函数调用。

function App() { return ( <h1 key="key" style={{ color: "red" }}> title </h1> ); }
// 自动引入 jsxDEV 函数 import { jsxDEV as _jsxDEV } from "react/jsx-dev-runtime"; const element = /*#__PURE__*/ _jsxDEV( "h1", { style: { color: "red", }, children: "title ", }, "key", false, { fileName: _jsxFileName, lineNumber: 14, columnNumber: 15, }, this );

jsxDEV 函数类型:

type jsxDEV = ( type, config, maybeKey, isStaticChildren, source, self ) => ReactElement;

接收 6 个参数:

  • type:元素类型
  • config:即 props 对象,包含 children 等属性
  • maybeKey:单独剥离的 key 属性
  • isStaticchildren 是否为静态
  • source:源码信息
  • self:当前组件实例,通常直接传入 this,在类组件才有意义

type 包含以下几种类型:

  • 字符串:表示原生 DOM 组件,如 div
  • 函数:表示类、函数组件
  • Symbol(或者特殊字符串 polyfill,主要针对React 内置组件):如 FragmentStrictMode

Babel 会根据 type 的类型,选择不同的转换逻辑。如果是原生标签(以小写开头的字符串),会转换为 string 类型,否则保持原样。

function App() { const element1 = <div />; const element2 = <span />; const element3 = <Test />; const element4 = <component.Test />; const element5 = <React.StrictMode></React.StrictMode>; const element6 = <></>; }

会被转换为:

import { jsxDEV as _jsxDEV, Fragment as _Fragment, } from "react/jsx-dev-runtime"; function App() { const element1 = /*#__PURE__*/ _jsxDEV( "div" /** ... */ ); const element2 = /*#__PURE__*/ _jsxDEV( "span" /** ... */ ); const element3 = /*#__PURE__*/ _jsxDEV( Test /** ... */ ); const element4 = /*#__PURE__*/ _jsxDEV( component.Test /** ... */ ); const element5 = /*#__PURE__*/ _jsxDEV( React.StrictMode /** ... */ ); const element6 = /*#__PURE__*/ _jsxDEV(_Fragment /** ... */); }
TIP

这也是为什么 React 希望组件名以大写字母开头,因为这样可以避免与原生标签混淆而导致 Babel 转换错误。

config 对象,即:对应 props 属性,会将除了 key 之外的所有属性收敛到其中,包含 children 属性(children 在以前的版本中会解析为 jsxDev 单独的参数 )。

const element = ( <h1 key="key" ref={ref} other={other}> title </h1> );

会被转换为:

const element = /*#__PURE__*/ _jsxDEV( "h1", { ref: ref, other: other, children: "title", }, "key", // key 会被单独提出来,便于提高后续 React 解析性能 false, { fileName: _jsxFileName, lineNumber: 5, columnNumber: 15, }, this );

maybeKey 用于单独提取 key 属性,为什么会叫 maybeKey 呢?

比如以下场景:

const element = ( <h1 key="key" {...props}> title </h1> );

props 里面说不定本身就包含 key 属性,而单纯从静态分析,Babel 无法得知这一点。

虽然如此,上面的代码依旧会被解析成:

const element = /*#__PURE__*/ _jsxDEV( "h1", { ...props, children: "title", }, "key", false, { fileName: _jsxFileName, lineNumber: 5, columnNumber: 15, }, this );
INFO

现阶段,React 会在运行时依旧从 config 中解析 key 属性,但是会抛出 warning 信息,提示 key 应该被单独指定出来。下一步(不能确定什么时候)会停止从 config 中解析 key

现阶段还有一个问题:

An unresolved issue is how we distinguish <div key="Hi" {...props} /> from <div {...props} key="Hi" /> which currently have different semantics depending on if props has a key.

如果是:

const element = ( <h1 {...props} key="key"> title </h1> );

会被转换为:

import { createElement as _createElement } from "react"; const element = /*#__PURE__*/ _createElement( "h1", { ...props, key: "key", __self: this, __source: { fileName: _jsxFileName, lineNumber: 2, columnNumber: 15, }, }, "title" );

Babel 会使用 React.createElement 来代替 jsxDEV 函数。

NOTE

对这样的处理,笔者也有点疑惑。按理说,相比较于<div key="Hi" {...props} />, <div {...props} key="Hi" /> 更应该解析为jsxDEV("div",config,"key")

Babel 的对此的编译行为未来可能会有所改变,在此之前,将 key 放在前面,避免解析为 React.createElement

,能获得更好性能。

isStatic 用于标记 children 是否为静态。静态意味着 children 存在,且数量是固定的。比如:

const noChildren = <div />; const oneChild = <div>Hi</div>; const multipleChildren = ( <div> <span>Hi</span> <span>Hi</span> </div> ); const multipleChildren1 = ( <div> {getChild()} {someCondition ? getChild1() : getChild2()} </div> ); const dynamicChildren = ( <div> {items.map((item) => ( <span key={item.id}>{item.name}</span> ))} </div> );

会被转换为:

const noChildren = /*#__PURE__*/ _jsxDEV( "div", {}, void 0, false /** ... */ ); const oneChild = /*#__PURE__*/ _jsxDEV( "div", { children: "Hi", }, void 0, false /** ... */ ); const multipleChildren = /*#__PURE__*/ _jsxDEV( "div", { children: [ /*#__PURE__*/ _jsxDEV( "span", { children: "Hi", }, void 0, false /** ... */ ), /*#__PURE__*/ _jsxDEV( "span", { children: "Hi", }, void 0, false /** ... */ ), ], }, void 0, true /** ... */ ); const multipleChildren1 = /*#__PURE__*/ _jsxDEV( "div", { children: [getChild(), someCondition ? getChild1() : getChild2()], }, void 0, true /** ... */ ); const dynamicChildren = /*#__PURE__*/ _jsxDEV( "div", { children: items.map((item) => /*#__PURE__*/ _jsxDEV( "span", { children: item.name, }, item.id, false /** ... */ ) ), }, void 0, false /** ... */ );

isStatic 和 Babel 对config.children 的解析相关联:

  • 不存在 children 时,config中也不存在 children 属性,isStaticfalse
  • 只存在一个 children 时,config中虽然存在 children 属性,但不是数组类型,isStaticfalse
  • 存在多个 children 时,config中存在 children 属性,且是数组类型
    • 如果 children 中不是以map 函数形式存在,数量可数,isStatictrue, 否则 isStaticfalse

简单来说,当config.children被解析为数组类型时,且数量是静态,可数的。isStatictrue

NOTE

isStatic 属性名会让人误解为:children 是完全不可变的。比如 div>Hi</div> 或者 <div><span color="red"/></div>这样,而不是包含动态属性,比如 <div>{getChild()}</div> 或者 <div><span color={dynamic}/></div>

现阶段,Babel 主要是从 children

的个数来判断是否为静态。

在生产模式下,即使用 @babel/plugin-transform-react-jsx。会将绝大部分 JSX 转换为 jsx 或者 jsxs 函数调用。

类型如下:

type jsx = (type, config, maybeKey) => ReactElement; type jsxs = jsx;

相比于开发环境会少isStatic, 和source, self 等用于调试信息的参数。

type, config,maybeKey转换方式和开发模式类似,jsxjsxs的区别在于:

  • 如果在开发模式下,isStatic 被解析为 true 的情况,生产模式会转换为 jsxs 函数调用
  • 如果在开发模式下,isStatic 被解析为 false 的情况,生产模式会转换为 jsx 函数调用

还是以这段代码举例:

const noChildren = <div />; const oneChild = <div>Hi</div>; const multipleChildren = ( <div> <span>Hi</span> <span>Hi</span> </div> ); const multipleChildren1 = ( <div> {getChild()} {someCondition ? getChild1() : getChild2()} </div> ); const dynamicChildren = ( <div> {items.map((item) => ( <span key={item.id}>{item.name}</span> ))} </div> );

会被转换为:

import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; const noChildren = /*#__PURE__*/ _jsx("div", {}); const oneChild = /*#__PURE__*/ _jsx("div", { children: "Hi", }); const multipleChildren = /*#__PURE__*/ _jsxs("div", { children: [ /*#__PURE__*/ _jsx("span", { children: "Hi", }), /*#__PURE__*/ _jsx("span", { children: "Hi", }), ], }); const multipleChildren1 = /*#__PURE__*/ _jsxs("div", { children: [getChild(), someCondition ? getChild1() : getChild2()], }); const dynamicChildren = /*#__PURE__*/ _jsx("div", { children: items.map((item) => /*#__PURE__*/ _jsx( "span", { children: item.name, }, item.id ) ), });
INFO

上文提到的开发模式中,会将<div {...props} key="Hi" /> 转换为 React.createElement的行为。在生产模式依旧存在。

runtime 为classic

当 runtime 为 classic 时,无论是开发模式,还是生产模式,所有的 JSX 都会转换 React.createElement 函数

React.createElement 类型:

type createElement = (type, config, children) => ReactElement;
  • type 转换方式和 runtimeautomatic 时一致
  • config 中包含除 children 的所有 props,包含 keyref
    • children 会被单独提出来,从第三个参数开始,平铺传入,后续的入参都会被视为是 children 的一项

比如:

import React from "react"; function Child() { return <div>Child</div>; } function App() { const element = ( <h1 key={"key"} style={{ color: "red" }} {...props}> Hello, world! </h1> ); return ( <div {...props} key={"key"}> {element} <Child /> </div> ); }

开发模式下,会被转换为:

function _extends() { return ( (_extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }), _extends.apply(null, arguments) ); } import React from "react"; function Child() { return /*#__PURE__*/ React.createElement( "div", { __self: this, __source: { fileName: _jsxFileName, lineNumber: 13, columnNumber: 10, }, }, "Child" ); } function App() { const element = /*#__PURE__*/ React.createElement( "h1", _extends( { key: "key", style: { color: "red", }, }, props, { __self: this, __source: { fileName: _jsxFileName, lineNumber: 17, columnNumber: 5, }, } ), "Hello, world!" ); return /*#__PURE__*/ React.createElement( "div", _extends({}, props, { key: "key", __self: this, __source: { fileName: _jsxFileName, lineNumber: 23, columnNumber: 5, }, }), // 从第三个参数开始,后续的入参都会被视为是 children 的一项 element, " ", /*#__PURE__*/ React.createElement(Child, { __self: this, __source: { fileName: _jsxFileName, lineNumber: 24, columnNumber: 17, }, }) ); }

生产模式下,会被转换为:

function _extends() { return ( (_extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }), _extends.apply(null, arguments) ); } import React from "react"; function Child() { return /*#__PURE__*/ React.createElement("div", null, "Child"); } function App() { const element = /*#__PURE__*/ React.createElement( "h1", _extends( { key: "key", style: { color: "red", }, }, props ), "Hello, world!" ); return /*#__PURE__*/ React.createElement( "div", _extends({}, props, { key: "key", }), // 从第三个参数开始,后续的入参都会被视为是 children 的一项 element, " ", /*#__PURE__*/ React.createElement(Child, null) ); }

开发模式相比于生产模式,也仅仅多了一些调试信息,比如 config.__self, config.__source

React 对 JSX 转换函数的逻辑升级

jsxDEV 函数

先从最复杂开发模式下的 runtime 为 automatic 的情况开始。从上可知,JSX 会被转换成jsxDEV 函数调用。

jsxDEV 核心功能包括:

  1. 类型检查:
    • 验证 JSX 元素的类型是否有效
    • 如果类型无效,会给出详细的错误信息
  2. 子元素处理:
    • 验证子元素的 key 属性
    • 处理静态子元素和动态子元素
  3. 属性处理:
    • 处理 key 和 ref 属性
    • 警告不正确的 key 使用方式
    • 创建 props 对象,移除保留属性(如 key 和 ref)
  4. 默认属性处理:
    • 如果组件有 defaultProps,不会再将其合并到 props 中
    • 类组件会在 render 阶段才真正的处理 defaultProps
  5. 创建 React 元素:
    • 最后调用 ReactElement 函数创建 React 元素
INFO

jsxDEV 函数代码中包含许多仅在开发环境(DEV)下运行的检查和警告。这些检查用于帮助开发者发现潜在的问题,如类型错误、key 使用不当等。

下方给出的源码中只会保留相对重要的主链路逻辑,省略了部分校验的逻辑
react/packages/react/src/jsx/ReactJSXElement.js
/** * JSX 转换的入口函数,它调用 jsxDEVImpl 来实际处理 JSX 转换 * * https://github.com/reactjs/rfcs/pull/107 */ export function jsxDEV(type, config, maybeKey, isStaticChildren, source, self) { return jsxDEVImpl( type, config, maybeKey, isStaticChildren, source, self, // 开发环境下的特性开关,主要用于获取错误堆栈和优化异步调用栈的 debug __DEV__ && enableOwnerStacks ? Error("react-stack-top-frame") : undefined, __DEV__ && enableOwnerStacks ? createTask(getTaskName(type)) : undefined ); } function jsxDEVImpl( type, config, maybeKey, isStaticChildren, source, self, debugStack, debugTask ) { if (__DEV__) { /** * 🚧 省略掉对 type,children key 的检查和警告 🚧 */ // 现阶段,依旧会从 config 中获取 key 值,但是为下一阶段做准备,会警告 key 使用不当 // Warn about key spread regardless of whether the type is valid. if (hasOwnProperty.call(config, "key")) { const componentName = getComponentNameFromType(type); const keys = Object.keys(config).filter((k) => k !== "key"); const beforeExample = keys.length > 0 ? "{key: someKey, " + keys.join(": ..., ") + ": ...}" : "{key: someKey}"; if (!didWarnAboutKeySpread[componentName + beforeExample]) { const afterExample = keys.length > 0 ? "{" + keys.join(": ..., ") + ": ...}" : "{}"; console.error( 'A props object containing a "key" prop is being spread into JSX:\n' + " let props = %s;\n" + " <%s {...props} />\n" + "React keys must be passed directly to JSX without using spread:\n" + " let props = %s;\n" + " <%s key={someKey} {...props} />", beforeExample, componentName, afterExample, componentName ); didWarnAboutKeySpread[componentName + beforeExample] = true; } } let key = null; let ref = null; // 下面注释提到了对 <div {...props} key="Hi" or <div key="Hi" {...props} /> 两种情况的处理还有点问题 // Currently, key can be spread in as a prop. This causes a potential // issue if key is also explicitly declared (ie. <div {...props} key="Hi" /> // or <div key="Hi" {...props} /> ). We want to deprecate key spread, // but as an intermediary step, we will use jsxDEV for everything except // <div {...props} key="Hi" />, because we aren't currently able to tell if // key is explicitly declared to be undefined or not. if (maybeKey !== undefined) { if (__DEV__) { checkKeyStringCoercion(maybeKey); } key = "" + maybeKey; } // 依旧会从 config 中获取 key 值 if (hasValidKey(config)) { if (__DEV__) { checkKeyStringCoercion(config.key); } key = "" + config.key; } // 从 config 中获取 ref 值, 在 19 中,enableRefAsProp 和 disableStringRefs 都是 true // 下面的逻辑不会进入,不用关心 if (hasValidRef(config)) { if (!enableRefAsProp) { ref = config.ref; if (!disableStringRefs) { ref = coerceStringRef(ref, getOwner(), type); } } if (!disableStringRefs) { warnIfStringRefCannotBeAutoConverted(config, self); } } let props; // const enableFastJSXWithoutStringRefs = enableRefAsProp && disableStringRefs; // 所以第一个条件肯定是满足的,只需要关心 `!("key" in config)` 的情况 if ( (enableFastJSXWithoutStringRefs || (enableRefAsProp && !("ref" in config))) && !("key" in config) ) { // 性能优化措施,尽可能重用原始的 props 对象 // If key was not spread in, we can reuse the original props object. This // only works for `jsx`, not `createElement`, because `jsx` is a compiler // target and the compiler always passes a new object. For `createElement`, // we can't assume a new object is passed every time because it can be // called manually. // // Spreading key is a warning in dev. In a future release, we will not // remove a spread key from the props object. (But we'll still warn.) We'll // always pass the object straight through. props = config; } else { // We need to remove reserved props (key, prop, ref). Create a fresh props // object and copy over all the non-reserved props. We don't use `delete` // because in V8 it will deopt the object to dictionary mode. props = {}; for (const propName in config) { // Skip over reserved prop names if (propName !== "key" && (enableRefAsProp || propName !== "ref")) { // ref 不会再作为保留属性,而是会保留在 props 中 if (enableRefAsProp && !disableStringRefs && propName === "ref") { props.ref = coerceStringRef(config[propName], getOwner(), type); } else { props[propName] = config[propName]; } } } } // 在 19 中,disableDefaultPropsExceptForClasses 为 true // 所以不会再将 defaultProps 合并到 props 中 if (!disableDefaultPropsExceptForClasses) { // Resolve default props if (type && type.defaultProps) { const defaultProps = type.defaultProps; for (const propName in defaultProps) { if (props[propName] === undefined) { props[propName] = defaultProps[propName]; } } } } // 最后调用 ReactElement 函数创建 React 元素 return ReactElement( type, key, ref, self, source, getOwner(), props, debugStack, debugTask ); } }
react/packages/shared/ReactFeatureFlags.js
// 19 中上面涉及到的几个特性开关都是 true // Passes `ref` as a normal prop instead of stripping it from the props object // during element creation. export const enableRefAsProp = true; export const disableStringRefs = true; // TODO: Land at Meta before removing. export const disableDefaultPropsExceptForClasses = true;
react/packages/react/src/jsx/ReactJSXElement.js
function ReactElement( type, key, _ref, self, source, owner, props, debugStack, debugTask ) { let ref; if (enableRefAsProp) { // When enableRefAsProp is on, ignore whatever was passed as the ref // argument and treat `props.ref` as the source of truth. The only thing we // use this for is `element.ref`, which will log a deprecation warning on // access. In the next release, we can remove `element.ref` as well as the // `ref` argument. const refProp = props.ref; // An undefined `element.ref` is coerced to `null` for // backwards compatibility. ref = refProp !== undefined ? refProp : null; } else { ref = _ref; } // 分不同的情况构建 element,主要区别在于 ref 的处理 let element; if (__DEV__ && enableRefAsProp) { // In dev, make `ref` a non-enumerable property with a warning. It's non- // enumerable so that test matchers and serializers don't access it and // trigger the warning. // // `ref` will be removed from the element completely in a future release. element = { // This tag allows us to uniquely identify this as a React Element $$typeof: REACT_ELEMENT_TYPE, // Built-in properties that belong on the element type, key, props, // Record the component responsible for creating this element. _owner: owner, }; if (ref !== null) { Object.defineProperty(element, "ref", { enumerable: false, get: elementRefGetterWithDeprecationWarning, }); } else { // Don't warn on access if a ref is not given. This reduces false // positives in cases where a test serializer uses // getOwnPropertyDescriptors to compare objects, like Jest does, which is // a problem because it bypasses non-enumerability. // // So unfortunately this will trigger a false positive warning in Jest // when the diff is printed: // // expect(<div ref={ref} />).toEqual(<span ref={ref} />); // // A bit sketchy, but this is what we've done for the `props.key` and // `props.ref` accessors for years, which implies it will be good enough // for `element.ref`, too. Let's see if anyone complains. Object.defineProperty(element, "ref", { enumerable: false, value: null, }); } } else if (!__DEV__ && disableStringRefs) { // In prod, `ref` is a regular property and _owner doesn't exist. element = { // This tag allows us to uniquely identify this as a React Element $$typeof: REACT_ELEMENT_TYPE, // Built-in properties that belong on the element type, key, ref, props, }; } else { // In prod, `ref` is a regular property. It will be removed in a // future release. element = { // This tag allows us to uniquely identify this as a React Element $$typeof: REACT_ELEMENT_TYPE, // Built-in properties that belong on the element type, key, ref, props, // Record the component responsible for creating this element. _owner: owner, }; } /** * 🚧 省略掉对 element 的开发时 debug 属性的处理 🚧 */ return element; }

经过上面的流程,函数jsxDEV 将编译后 JSX 对象转换为 ReactElement 元素,这个元素是 React 内部用于描述组件树的节点,作为 React 渲染和更新机制的输入。

性能提升

Babel 会将 JSX 的 props 使用一个新的对象来承载,在以前的版本中,jsxDEV 总是会创建一个新的对象,用于承载剔除掉 refkey等保留属性而现在,会尽可能重用这个对象,以减少内存分配和垃圾回收的负担

性能问题

生产模式下的 jsx & jsxs 函数本质就是jsxProd 函数,即 jsxDEV 函数去除了校验逻辑的版本,不再赘述。

TIP

ReactElement 元素 和 Fiber 节点是不同的,后文会详解介绍 Fiber 节点,注意不要混淆。

createElement 函数

上文提到 automatic 模式下有些特殊情况,或者是 classic 模式下,JSX 会被转换成 createElement 函数调用。

下面来看下 createElement 函数的实现。

react/packages/react/src/jsx/ReactJSXElement.js
export function createElement(type, config, children) { /** * 🚧 省略掉对 type,children key 的检查和警告 🚧 */ let propName; // Reserved names are extracted const props = {}; let key = null; let ref = null; // config 中包含除 children 的所有 props,包含 key,ref if (config != null) { if (hasValidRef(config)) { if (!enableRefAsProp) { ref = config.ref; if (!disableStringRefs) { ref = coerceStringRef(ref, getOwner(), type); } } if (__DEV__ && !disableStringRefs) { warnIfStringRefCannotBeAutoConverted(config, config.__self); } } if (hasValidKey(config)) { if (__DEV__) { checkKeyStringCoercion(config.key); } key = "" + config.key; } // 将 config 中除 key 之外的属性添加到 props 对象中,由于每次都会构建新的 props 对象,性能会差点 // Remaining properties are added to a new props object for (propName in config) { if ( hasOwnProperty.call(config, propName) && // Skip over reserved prop names propName !== "key" && (enableRefAsProp || propName !== "ref") && // Even though we don't use these anymore in the runtime, we don't want // them to appear as props, so in createElement we filter them out. // We don't have to do this in the jsx() runtime because the jsx() // transform never passed these as props; it used separate arguments. propName !== "__self" && propName !== "__source" ) { if (enableRefAsProp && !disableStringRefs && propName === "ref") { props.ref = coerceStringRef(config[propName], getOwner(), type); } else { props[propName] = config[propName]; } } } } // 从第三个参数开始,后续的入参都会被视为是 children 的一项,将其收敛到 props 属性中 // Children can be more than one argument, and those are transferred onto // the newly allocated props object. const childrenLength = arguments.length - 2; if (childrenLength === 1) { props.children = children; } else if (childrenLength > 1) { const childArray = Array(childrenLength); for (let i = 0; i < childrenLength; i++) { childArray[i] = arguments[i + 2]; } if (__DEV__) { if (Object.freeze) { Object.freeze(childArray); } } props.children = childArray; } // 与 jsxDEV 不同的是,少了特性开关,所以组件的 defaultProps,总是会被合并到 props 中 // Resolve default props if (type && type.defaultProps) { const defaultProps = type.defaultProps; for (propName in defaultProps) { if (props[propName] === undefined) { props[propName] = defaultProps[propName]; } } } return ReactElement( type, key, ref, undefined, undefined, getOwner(), props, __DEV__ && enableOwnerStacks ? Error("react-stack-top-frame") : undefined, __DEV__ && enableOwnerStacks ? createTask(getTaskName(type)) : undefined ); }
TIP

我们偶尔可能也会用到 React.cloneElement 函数,它主要用于克隆 ReactElement 元素,并进行一些扩展。核心代码流程和 React.createElement函数大致一致,不再赘述。

参考