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

参考资料

相关推荐
a别念m22 分钟前
webpack基础与进阶
前端·webpack·node.js
芭拉拉小魔仙36 分钟前
【Vue3/Typescript】从零开始搭建H5移动端项目
前端·vue.js·typescript·vant
axinawang37 分钟前
通过RedisCacheManager自定义缓存序列化(适用通过注解缓存数据)
前端·spring·bootstrap
前端南玖1 小时前
Vue3响应式核心:ref vs reactive深度对比
前端·javascript·vue.js
哔哩哔哩技术1 小时前
B站在KMP跨平台的业务实践之路
前端
微笑边缘的金元宝1 小时前
svg实现3环进度图,可动态调节进度数值,(vue)
前端·javascript·vue.js·svg
程序猿小D1 小时前
第28节 Node.js 文件系统
服务器·前端·javascript·vscode·node.js·编辑器·vim
Trae首席推荐官1 小时前
字节跳动技术副总裁洪定坤:TRAE 想做 AI Development
前端·人工智能·trae
小妖6661 小时前
uni-app bitmap.load() 返回 code=-100
前端·javascript·uni-app
前端与小赵1 小时前
uni-app隐藏返回按钮
前端·uni-app