什么是 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 v7.26.3 版本为准,主要包含以下两个插件:

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

@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;

    // 下面注释提到了对 <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;
    }

    let props;
    if (!("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
        // 不会对 ref 做特殊处理,所以 ref 不会再作为保留属性,而是会保留在 props 中
        if (propName !== "key") {
          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];
          }
        }
      }
    }

    // 对从 props 中获取的 key 做特殊处理,添加开发环境警告
    if (key) {
      const displayName =
        typeof type === "function"
          ? type.displayName || type.name || "Unknown"
          : type;
      defineKeyPropWarningGetter(props, displayName);
    }

    // 最后调用 ReactElement 函数创建 React 元素
    return ReactElement(
      type,
      key,
      self,
      source,
      getOwner(),
      props,
      debugStack,
      debugTask
    );
  }
}
react/packages/react/src/jsx/ReactJSXElement.js
function ReactElement(
  type,
  key,
  self,
  source,
  owner,
  props,
  debugStack,
  debugTask
) {
  // 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.
  const ref = refProp !== undefined ? refProp : null;

  // 分不同的情况构建 element,主要区别在于 ref 的处理
  let element;
  if (__DEV__) {
    // 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
    };
    // 对 ref 做特殊处理,添加开发环境警告
    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 {
    // 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
    };
  }

  /**
   * 🚧 省略掉对 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;

  // config 中包含除 children 的所有 props,包含 key,ref
  if (config != null) {
    if (__DEV__) {
      // 对旧的 JSX 转换做警告
      if (
        !didWarnAboutOldJSXRuntime &&
        "__self" in config &&
        // Do not assume this is the result of an oudated JSX transform if key
        // is present, because the modern JSX transform sometimes outputs
        // createElement to preserve precedence between a static key and a
        // spread key. To avoid false positive warnings, we never warn if
        // there's a key.
        !("key" in config)
      ) {
        didWarnAboutOldJSXRuntime = true;
        console.warn(
          "Your app (or one of its dependencies) is using an outdated JSX " +
            "transform. Update to the modern JSX transform for " +
            "faster performance: https://react.dev/link/new-jsx-transform"
        );
      }
    }

    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" &&
        // 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"
      ) {
        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];
      }
    }
  }
  if (__DEV__) {
    if (key) {
      const displayName =
        typeof type === "function"
          ? type.displayName || type.name || "Unknown"
          : type;
      defineKeyPropWarningGetter(props, displayName);
    }
  }

  return ReactElement(
    type,
    key,
    undefined,
    undefined,
    getOwner(),
    props,
    __DEV__ && enableOwnerStacks ? Error("react-stack-top-frame") : undefined,
    __DEV__ && enableOwnerStacks ? createTask(getTaskName(type)) : undefined
  );
}
TIP

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

参考