React 代码的转换主要是通过 Babel 来完成的。React 使用了 JSX 语法,而 JSX 是一种 JavaScript 的语法扩展,它看起来像 HTML,但不能直接在浏览器中运行。Babel 作为一个 JavaScript 编译器,可以将 JSX 转换为标准的 JavaScript 代码(ES5/ES6),从而使得 React 代码可以在浏览器中执行。
Webpack 集成在大多数现代 React 项目中,Babel 通常与 Webpack 集成使用。下面是一个基础的Webpack 配置示例:
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react'],
},
},
},
],
},
resolve: {
extensions: ['.js', '.jsx'],
},
};
在这个配置中,Webpack 通过模块解析规则找到每个导入的模块,并确定这些模块的位置。Webpack 支持多种模块类型,包括 ES6 模块、CommonJS 模块等。
对于 React 代码,通常会包含诸如 import React from 'react';
或 import App from './App';
的语句。Webpack 会解析这些导入,查找相应的模块文件。
- 解析路径:Webpack 会根据配置的
resolve
选项(如extensions
,alias
等)来解析模块的路径。 - 文件扩展名:Webpack 会根据
resolve.extensions
自动尝试各种扩展名(如.js
,.jsx
,.ts
,.tsx
)来找到对应的模块。
然后Webpack 依赖加载器(Loaders)来处理非 JavaScript 文件和特殊的 JavaScript 语法。在 React 项目中,最常用的加载器是 babel-loader
,它用于转换 JSX 和 ES6+ 代码。
babel-loader
使用 Babel 将 JSX 和现代 JavaScript 语法转换为兼容的 ES5 代码。@babel/preset-react
处理 JSX,@babel/preset-env
处理 ES6+ 语法。- 其他加载器:项目中可能还有其他加载器,例如处理 CSS、图像、字体等资源的加载器。
这个过程顺序为:@babel/preset-react
先处理 JSX ,得到 ES6+语法,然后@babel/preset-env
再处理成 ES5 的语法。
那我我们就来研究一下这个
@babel/preset-react
干了什么?
我们编写如下一段 JSX 语法:
const App = () => {
return <h1>Hello, World!</h1>;
};
export default App;
在转换过程中,上述代码最终会被转换为:
"use strict";
const App = () => {
return /*#__PURE__*/React.createElement("h1", null, "Hello, World!");
};
export default App;
代码解释转换后的代码中,<h1>Hello, World!</h1>
被替换成了 React.createElement
的调用。这是因为 JSX 语法只是 React 语法糖,本质上它会被转换成对 React.createElement
的函数调用。这个函数返回一个描述 UI 的 JavaScript 对象(也就是虚拟 DOM),React 根据这个对象来渲染实际的 DOM。转换后的代码可以在任何支持 ES5 的环境中运行。即使浏览器不支持 JSX 语法或者不支持最新的 JavaScript 特性,Babel 也能将这些代码转换成兼容性良好的 JavaScript。
上面转化为React.createElement
就是由@babel/preset-react
这个预设的 babel 来做的。
我们来看看它干了什么事?
在 源码中我们看到,它又使用到了另外的插件,@babel/plugin-transform-react-jsx
和@babel/plugin-transform-react-jsx-development
,这两个里面才是真正的实现了转换React.createElement
的功能。
它这是用了两个插件是为了区分生产环境,我们就看看@babel/plugin-transform-react-jsx
做的工作。@babel/plugin-transform-react-jsx
也是 Babel 的一个插件,专门用于将 JSX 语法转换为标准的 JavaScript 代码。这个插件的核心任务是将 JSX 结构转换成 React.createElement
函数调用,从而使 JSX 可以在 JavaScript 环境中运行。
下面是 @babel/plugin-transform-react-jsx
的核心工作流程:
1. 解析 JSX 元素
Babel 在解析代码时,会将 JSX 元素转换为 AST 中的特定节点类型。例如:
const element = <h1>Hello, World!</h1>;
这个代码片段会被解析成一个包含 JSXElement
节点的 AST,其中 h1
是 JSXIdentifier
节点,Hello, World!
是 JSXText
节点。
**2. 转换 JSX 到 ****React.createElement**
@babel/plugin-transform-react-jsx
插件会遍历 AST,找到所有的 JSXElement
节点,然后将它们转换为对应的 React.createElement
函数调用。
伪代码示例:
function JSXElementToCreateElement(path) {
const node = path.node;
const openingElement = node.openingElement;
const tagName = openingElement.name.name; // 例如 'h1'
const attributes = openingElement.attributes; // 例如 []
const children = node.children; // 例如 ['Hello, World!']
// 转换成 React.createElement(tagName, props, ...children)
path.replaceWith(
t.callExpression(
t.memberExpression(t.identifier("React"), t.identifier("createElement")),
[
t.stringLiteral(tagName), // 第一个参数是标签名
t.nullLiteral(), // 第二个参数是属性,这里为空
...children.map(child => t.stringLiteral(child.value)) // 子元素
]
)
);
}
3. 处理属性
如果 JSX 元素有属性,这些属性会被转换为一个对象,并作为 React.createElement
的第二个参数。例如:
const element = <h1 className="header">Hello, World!</h1>;
转换为:
const element = React.createElement("h1", { className: "header" }, "Hello, World!");
4. 处理子元素
JSX 元素中的子元素(例如文本节点或嵌套的 JSX 元素)会作为 React.createElement
的第三个及后续参数传递。插件会递归地处理这些子元素,并将它们转换为相应的 JavaScript 表达式。
5. 插件的整体结构@babel/plugin-transform-react-jsx
的结构大致如下:
module.exports = function(babel) {
const { types: t } = babel;
return {
visitor: {
JSXElement(path) {
// 将 JSX 元素转换为 React.createElement
JSXElementToCreateElement(path);
}
}
};
};
visitor
对象用于定义插件要如何处理 AST 中的不同节点类型。对于@babel/plugin-transform-react-jsx
来说,最重要的是JSXElement
节点。JSXElementToCreateElement
是一个自定义函数,用于将JSXElement
节点转换为React.createElement
函数调用。
总结
@babel/plugin-transform-react-jsx
的核心工作是将 JSX 语法转换为 JavaScript 的函数调用(通常是 React.createElement
)。通过遍历 AST 中的 JSX 元素节点,并将这些节点转换为等效的 JavaScript 表达式,这个插件实现了从 JSX 到普通 JavaScript 代码的转换。这使得开发者可以编写直观的 JSX 代码,而不必手动构造复杂的 JavaScript 表达式。
本文只讲到了冰山一角,还有很多工作没有涉及到,让大家直接初步了解 react 最终转化成ES代码以后是怎么工作的。