React 开发者必知:JSX 转换机制与内部实现

在 React 项目中,我们会使用 JSX 语法。简单来说,JSX 可以让你在 JavaScript 代码中编写类似 HTML(实际上是 XML)结构的代码,其本质是简化组件和元素结构的声明方式。

JSX 转译原理

JavaScript 本身不支持 JSX 语法,从我们编写的 React 代码到最终的运行产物,需要经历转译。以 Babel 为例,转译主要经历三个阶段:解析(parse)遍历(traverse)和生成(generate) 。针对 JSX 语法,Babel 提供专门的插件 @babel/plugin-transform-react-jsx 来处理转换(被包含在预设 @babel/preset-react 中)。

jsx 复制代码
// 转换前
function App() {
  return <div>Hello World</div>;
}

// React 17 之前(经典转换)
// 注意: 需要手动导入 React
// import React from 'react';
// 转换后
function App() {
  return React.createElement("div", null, "Hello World");
}
​
// React 17 及之后(新的 JSX 转换)
// 注意:不需要显式导入 React,由转译器(不是编译器?)自动导入所需的函数
// 转换后
import { jsx as _jsx } from "react/jsx-runtime";
​
function App() {
  return _jsx("div", { children: "Hello World" });
}

Babel 在处理 JSX 时,会先将其解析成抽象语法树(AST),然后遍历时通过访问者模式修改 AST 节点,最终生成 JavaScript 代码。下面是 @babel/plugin-transform-react-jsx 插件的核心实现逻辑:

js 复制代码
module.exports = function (babel) {
  const { types: t } = babel;
​
  return {
    visitor: {
      JSXElement(path) {
        // 获取元素类型
        const openingElement = path.node.openingElement;
        const tagName = openingElement.name.name;
​
        // 处理属性
        const attributes = openingElement.attributes.map((attr) => {
          // 转换 JSX 属性为对象属性
          // ...
        });
​
        // 处理子元素
        const children = path.node.children.map((child) => {
          // 转换子元素
          // ...
        });
​
        // 创建 React.createElement 调用
        const callExpression = t.callExpression(
          t.memberExpression(
            t.identifier("React"),
            t.identifier("createElement"),
          ),
          [
            t.stringLiteral(tagName),
            t.objectExpression(attributes),
            ...children,
          ],
        );
​
        // 替换原 JSX 节点
        path.replaceWith(callExpression);
      },
    },
  };
};

两种 createElement 的内部实现

React.createElement

当 JSX 被转换成 React.createElement() 调用后,该函数会创建一个 React 元素(React Element):

js 复制代码
function createElement(type, config, ...children) {
  // 提取特殊的 props: key, ref
  let key = null;
  let ref = null;
​
  if (config != null) {
    if (config.key !== undefined) key = "" + config.key;
    if (config.ref !== undefined) ref = config.ref;
  }
​
  // 处理 props
  const props = {};
  // 复制 config 中的属性到 props
  for (let propName in config) {
    if (propName !== "key" && propName !== "ref") {
      props[propName] = config[propName];
    }
  }
​
  // 处理 children
  if (children.length === 1) {
    props.children = children[0];
  } else if (children.length > 1) {
    props.children = children;
  }
​
  // 创建 React 元素
  const element = {
    $$typeof: Symbol.for("react.element"),
    type,
    key,
    ref,
    props,
    _owner: null, // React 内部使用,用于跟踪创建该元素的组件,对调试和警告信息有帮助
  };
​
  return element;
}

:::color4 React 元素是描述 UI 的不可变对象,包含以下主要属性:

  • type: 元素的类型(字符串表示 DOM 元素,函数或类表示组件)
  • props: 传递给元素的属性
  • key: 用于在列表中标识元素,便于后续 Diff 算法识别
  • ref: 引用元素的 DOM 节点或组件实例
  • $$typeof: 用于标识这是一个 React 元素的符号,因为 Symbol 不能被 JSON 序列化,所以可以防止 XSS 攻击(服务器的 JSON 数据无法伪造有效的 React 元素)

:::

React 17 中的 JSX Runtime

React 17 引入新的 JSX 转换方式,使用 jsx-runtime 包中的函数。这个包提供两个主要函数:jsxjsxs,后者专门用于处理有多个子元素的情况:

js 复制代码
// react/jsx-runtime 简化实现
export function jsx(type, config, maybeKey) {
  let key = null;
  let ref = null;
​
  if (maybeKey !== undefined) {
    key = "" + maybeKey;
  } else if (config && config.key !== undefined) {
    key = "" + config.key;
  }
​
  if (config && config.ref !== undefined) {
    ref = config.ref;
  }
​
  // 创建 props 对象
  const props = {};
  for (let propName in config) {
    if (propName !== "key" && propName !== "ref") {
      props[propName] = config[propName];
    }
  }
​
  return {
    $$typeof: Symbol.for("react.element"),
    type,
    key,
    ref,
    props,
    _owner: null,
  };
}
​
// 处理有多个子元素的情况
export function jsxs(type, config, maybeKey) {
  // 实际实现与 jsx 基本相同,但针对多子元素场景有优化
  return jsx(type, config, maybeKey);
}

浅析

  • 为什么需要 JSX 语法 :JSX 提供一种声明式的语法,使组件的结构和行为更直观,同时保持 JavaScript 的全部能力。相比于手动调用 createElement 函数,JSX 使代码更易读、更接近最终渲染的 UI 结构。
  • 为什么 babel 转译不直接生成 React Element:保持 API 的稳定性和灵活性,同时允许内部实现变化。这样 React 团队可以在不破坏现有代码的情况下修改元素的创建过程、添加新特性或进行性能优化。
  • 为什么改变了导入方式:新的 JSX 转换消除了不必要的导入并优化函数调用,可以带来轻微的性能改进和更小的包大小(通过自动导入特定的函数而非整个 React 包,可以更好地支持打包工具的优化)。

小结

  1. JSX 本质上是一种语法糖,它允许在 JavaScript 中编写类似 HTML 的代码,简化 UI 结构(Schema)的声明(其他框架也有应用场景)。
  2. JSX 在编译时会被转换函数调用(React.createElement 或 jsx/jsxs),运行时生成 React Element。
  3. React 17 引入的新 JSX 转换简化了开发体验,不再需要手动导入 React,但是无论使用哪种转换方式,React 最终生成的元素结构基本相同。

参考资料

相关推荐
vipbic3 分钟前
中后台越做越乱后,我用插件化把它救回来了
前端·vue.js
Hyyy9 分钟前
Computer Use 适合做什么,不适合做什么——一次真实使用后的思考
前端
小和尚同志35 分钟前
前端 AI 单元测试思考与落地
前端·人工智能·aigc
invicinble2 小时前
c端系统,其实更像一个信息展示平台
前端
李姆斯3 小时前
管理是否可以被完全量化
前端·产品经理·团队管理
名字还没想好☜3 小时前
Next.js 中间件实战:鉴权、重定向与 A/B 分流
开发语言·前端·javascript·中间件·react·next.js
广州灵眸科技有限公司3 小时前
瑞芯微RV1126B开发板(EASY-EAI-PI2) INI文件操作
java·前端·javascript·网络·人工智能
江华森4 小时前
Web 开发基础:HTTP 与 REST
前端·网络协议·http
IT_陈寒5 小时前
Redis的KEYS命令把我搞崩溃了,改用SCAN才活过来
前端·人工智能·后端
Jackson__5 小时前
为什么 Agent 越聊越慢?聊聊 Context(上下文)管理
前端·agent·ai编程