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 最终生成的元素结构基本相同。

参考资料

相关推荐
做前端的娜娜子1 小时前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
小帅不太帅1 小时前
架构没变、规模没变,DeepSeek V4 Flash 正式版凭什么暴涨 47 分?
前端·aigc·deepseek
jarvisuni2 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
卷福同学3 小时前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端
先吃饱再说3 小时前
编写一个颜色选择器之后,我理解了 React 工程化的三层架构
react.js·前端框架·前端工程化
先吃饱再说3 小时前
别说你懂 useEffect:从底层机制到生命周期管理,这篇全讲透了
react.js·前端框架
赵庆明老师3 小时前
Vben精讲:21-详解web-antd:tsconfig.json
前端·json·vim
先吃饱再说3 小时前
从“传事件”到“只传值”:React + TypeScript 组件 Props 设计的两次进化
react.js·前端框架·前端工程化
wc883 小时前
微软EDGE浏览器功能学习
前端·学习·edge