Skip to content

ReactElement、JSX 与 createElement 源码主线

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

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

这篇文章重点讲:

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

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. createElementjsx 是什么关系

这两个 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. keyref 被单独拿出来,不放进普通 props
  4. props 才是大部分输入数据所在的位置

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


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

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

更完整的理解应该是:

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

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

它负责的是:

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

它不负责的是:

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

可以直接这样区分:

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

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


4. 为什么 keyref 不和普通 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 keyrefprops 三者的边界到底是什么

这 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 是特殊对象,可能表示 memoforwardRefFragment

也就是说,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 构建的个人技术笔记。