Skip to content

ReactElement、JSX 与 createElement 源码主线 ​

想读 React 源码,最适合先进入的入口通常不是 Fiber,而是 JSX。

因为 React 整个更新链路,最前面都要先回答一个问题:组件返回的 JSX,最后到底变成了什么。

这篇文章重点讲:

  1. JSX 在 React 源码视角里是什么
  2. React.createElement 和新版 jsx 运行时是什么关系
  3. React Element 的核心结构长什么样
  4. key、ref、props 在源码层是怎么被拆开的

1. JSX 最后并不是 DOM ​

这是 React 源码最适合先建立的第一条认识。

例如:

jsx
function WelcomeCard({ name }) {
  return <h1 className="title">Hello, {name}</h1>;
}

在源码视角里,它最终会变成一个“描述 UI 的对象”,而不是浏览器 DOM。

早期更接近:

js
React.createElement("h1", { className: "title" }, "Hello, ", name);

现代 JSX transform 更常见的是编译成:

js
jsx("h1", {
  className: "title",
  children: ["Hello, ", name]
});

也就是说,JSX -> React Element 才是前面真正的入口主线。


2. createElement 和 jsx 是什么关系 ​

这两个 API 在学习资料里都常见。

把这两个入口放回各自位置看:

  1. React.createElement 是更早期大家熟悉的入口
  2. 新版 JSX transform 更常直接编译到 jsx / jsxs
  3. 它们最终目标都一样,生成 React Element

所以源码上真正值得看的,不是只纠结调用名,而是看“最后 Element 对象是怎么组织出来的”。


3. React Element 长什么样 ​

可以用一份精简后的源码主线理解它。

下面不是 React 仓库里逐字符原样代码,而是保留核心字段后的示意:

js
function ReactElement(type, key, ref, props) {
  return {
    $$typeof: REACT_ELEMENT_TYPE,
    type,
    key,
    ref,
    props,
    _owner: null
  };
}

这里最值得注意的是:

  1. $$typeof 用来标识这是不是 React Element
  2. type 表示节点类型,可能是字符串标签,也可能是函数组件
  3. key 和 ref 被单独拿出来,不放进普通 props
  4. props 才是大部分输入数据所在的位置

🌟 把这句话记住:React Element 不是 DOM 节点,而是一份用来描述“要渲染什么”的标准对象。


3.1 React Element 的完整释义和能力边界 ​

如果只说“React Element 是对象”,这还不够落地。

更完整的理解应该是:

React Element 是 React 在运行时里统一使用的一种 UI 描述数据结构。它描述的是“当前节点应该按什么类型、带什么输入、处在什么层级关系里继续往下渲染”,而不是“这个节点已经存在于页面上”。`

这句话里最关键的是“描述”两个字。

它负责的是:

  1. 描述当前节点类型
  2. 携带 props
  3. 携带 key、ref 这类特殊字段
  4. 为后续 Fiber 创建提供统一输入

它不负责的是:

  1. 不直接表示真实 DOM 实例
  2. 不保存更新队列和副作用状态
  3. 不承担调度信息
  4. 不等于组件实例

可以直接这样区分:

概念更像什么主要职责
React Element一份声明式输入对象描述“要渲染什么”
Fiber一份可执行工作节点组织“怎么更新它”
DOM 节点浏览器真实节点表示“页面上已经存在什么”

所以 React Element 的边界非常清楚:它是渲染输入,不是运行中工作节点,更不是最终宿主实例。


4. 为什么 key 和 ref 不和普通 props 混在一起 ​

这是源码层很值得注意的设计。

在业务代码里你经常这样写:

jsx
<UserRow key={user.id} ref={rowRef} name={user.name} />

但到了 React Element 里,React 不会把它完全原封不动塞进一个大对象。

React 会把它拆成几层:

  1. key
  2. ref
  3. 其他普通 props

拆开存放。

原因是:

  1. key 主要服务于同层节点身份识别
  2. ref 主要服务于命令式引用
  3. 普通 props 才是组件输入数据

所以它们虽然都写在 JSX 标签上,但语义并不一样。


4.1 key、ref、props 三者的边界到底是什么 ​

这 3 个字段在 JSX 里长得很像,但源码层必须分清。

4.1.1 props ​

props 更偏组件输入数据。

它负责的是:

  1. 给组件传数据
  2. 给宿主节点传属性
  3. 传递 children

4.1.2 key ​

key 更偏节点身份标识。

它负责的是:

  1. 帮助同层节点在 diff 中识别身份
  2. 帮助状态保留和复用

它不负责:

  1. 不会作为普通 props 传给组件
  2. 不表达业务数据本身

4.1.3 ref ​

ref 更偏命令式引用入口。

它负责的是:

  1. 让外部拿到宿主节点或组件暴露的引用
  2. 让命令式操作有受控入口

它不负责:

  1. 不参与 diff 身份比较
  2. 不作为普通业务输入参与渲染逻辑

所以把职责放回原位看:

  1. props 解决“输入是什么”
  2. key 解决“它是谁”
  3. ref 解决“我之后怎么拿到它”

这也是 React 源码必须把它们拆开的根本原因。


5. 一个更接近源码主线的 createElement 示意 ​

下面这段代码同样是精简后的源码思路示意:

js
function createElement(type, config, children) {
  let key = null;
  let ref = null;
  const props = {};

  if (config != null) {
    if (config.key !== undefined) {
      key = '' + config.key;
    }

    if (config.ref !== undefined) {
      ref = config.ref;
    }

    for (const propName in config) {
      if (propName !== "key" && propName !== "ref") {
        props[propName] = config[propName];
      }
    }
  }

  if (arguments.length > 3) {
    props.children = Array.prototype.slice.call(arguments, 2);
  } else if (children !== undefined) {
    props.children = children;
  }

  return ReactElement(type, key, ref, props);
}

这段代码最重要的价值,不是逐行背,而是帮助你看到 4 件事:

  1. config 会被拆成特殊字段和普通 props
  2. children 也只是 props 的一部分
  3. 最后目标是组出一个标准 Element 对象
  4. React 后面处理的很多工作,都是从这份 Element 开始的

6. type 字段为什么这么关键 ​

Element 里的 type 决定了后面这棵节点怎么走。

例如:

  1. type === "div",说明这是宿主节点
  2. type 是函数,说明这是函数组件
  3. type 是特殊对象,可能表示 memo、forwardRef、Fragment 等

也就是说,React 后面进入 Fiber 阶段时,第一件重要的事之一就是看当前 Element 的 type,从而决定应该按哪种分支继续处理。


6.1 type 的能力边界 ​

type 很关键,但它也有明确边界。

它负责的是:

  1. 告诉 React 当前节点属于哪一类
  2. 决定后续该走宿主节点分支、函数组件分支还是特殊组件分支

它不负责的是:

  1. 不保存节点状态
  2. 不直接表示最终 DOM
  3. 不决定节点的更新优先级

所以 type 更像“分类入口”,而不是“完整节点信息”。

如果把 Element 看成一张表单:

  1. type 像“当前类型”
  2. props 像“输入参数”
  3. key 像“身份标签”
  4. ref 像“命令式访问入口”

只有这样拆开看,后面 Fiber 才不会显得像凭空冒出来。


7. JSX 到 Element,再到 Fiber 是怎么接起来的 ​

这条线很值得先串起来:

mermaid
flowchart TD
    A[JSX] --> B[jsx / createElement]
    B --> C[React Element]
    C --> D[根据 type 创建或复用 Fiber]

如果只看 JSX,不看 Element,会不知道 React 后面处理的输入长什么样。
如果只看 Fiber,不看 Element,也会不知道 Fiber 的来源是什么。

所以源码阅读里,这一层非常适合作为入口。


8. 源码里常见的相关文件 ​

如果你自己去仓库里读,通常会优先碰到这些位置:

文件作用
packages/react/src/jsx/ReactJSXElement.jsElement 创建主线
packages/react/src/ReactCreateElement.jscreateElement 相关入口
packages/shared/ReactSymbols.js各类 React 类型标记

阅读时建议先重点看:

  1. Element 对象有哪些字段
  2. key / ref / props 是怎么拆开的
  3. $$typeof 为什么存在

9. 这一层最常见的误区 ​

9.1 以为 JSX 最终就是 DOM ​

不是。

它先变成 React Element。

9.2 以为 children 是什么特别神秘的东西 ​

从源码视角看,它本质上也是 props 的一部分。

9.3 以为 key 是普通 props ​

不是。

它是 React 内部特别关心的身份标识字段。


10. 一句话总结 ​

React 在 JSX 这一层的源码主线,本质上是在做 把 JSX 编译结果整理成一份标准 React Element 对象,并把 key、ref、props、type 这些信息拆开,为后面的 Fiber 和调和阶段提供统一输入。

基于 VitePress 构建的个人技术笔记。