想想react会怎么做(前传)之 jsx编译JSX 的全称是 Javasrcipt and XML,是一个 JavaSc
这一节其实和react核心库的代码并没有关系,算是前传。
JSX是什么
const element = <h1>Hello, world!</h1>
这个有趣的标签语法既不是字符串也不是 HTML,它是JSX
JSX 的全称是 Javasrcipt and XML,是一个 JavaScript 的语法扩展,JSX 可以很好地描述 UI 应该呈现出它应有交互的本质形式。
但是react其实不强制要求使用jsx
例如,使用jsx的代码如下:
class Hello extends React.Component {
render() {
return <div>Hello {this.props.toWhat}</div>;
}
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Hello toWhat="World" />);
不使用jsx的代码如下:
class Hello extends React.Component {
render() {
return React.createElement('div', null, `Hello ${this.props.toWhat}`);
}
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(React.createElement(Hello, {toWhat: 'World'}, null));
看起来确实jsx要更加生动形象一些,所以我们react一般还是会使用jsx的
JSX编译是什么
所以其实JSX是一种语法糖,它最后都会被编译成方法的调用(React.createElement,jsx,jsxs方法),而这些函数就是来自react,这样就让jsx和react关联了起来。
我们可以使用babel playground来看看jsx编译之后的效果,打开babel plaground,使用react的presets
React runtime:classisc
React runtime使用classic的效果:
可以看到左边的就是jsx,右边的就是jsx编译的结果,左边的jsx编译成了React.createElement的调用以及嵌套调用
React runtime:Automatic
然后我们再使用React runtime为Automatic:
可以看到左边的jsx编译成了jsxs和jsx的调用和嵌套调用,还在上方从react/jsx-runtime中引入了jsx和jsxs方法
可以认为在react17之前jsx都是编译成React.createElement,之后就是编译成jsx,jsxs
两个runtime的区别
老版本(classisc)
会将jsx编译成React.createElement的调用,举个例子
源代码:
import React from 'react';
function App() {
return <div name="abc">
<div>123</div>
<div>456</div>
</div>;
}
编译后
import React from 'react';
function App() {
return /*#__PURE__*/React.createElement("div", {
name: "abc"
}, /*#__PURE__*/React.createElement("div", null, "123"),
/*#__PURE__*/React.createElement("div", null, "456"));
}
React.createElement的第一个参数为元素名,第二个参数为元素的参数,如果没有则为null,第三个参数及其之后为元素的子元素(有几个子元素就有几个参数)
这个编译结果还是有一些缺点(来自React官方文档)
- 由于jsx被编译成React.createElement,所有导致当你使用jsx的时候,你必须手动引入React
- 有一些性能优化和代码精简是React.createElement不允许的(我理解就是不方便通过修改React.createElement来做优化和精简)
新版本(automatic)
为了解决这些问题,react17之后,就会使用新版本的runtime对jsx进行编译
源代码
function App() {
return <div name="abc" key="key1">
<div>123</div>
<div>456</div>
</div>;
}
编译后
// 这行import是编译器自动把你引入的,不需要你手动引入
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
function App() {
return /*#__PURE__*/_jsxs("div", {
name: "abc",
children: [/*#__PURE__*/_jsx("div", {
children: "123"
}), /*#__PURE__*/_jsx("div", {
children: "456"
})]
}, "key1");
}
jsx模版编译后变成了jsx方法调用,可以看到新版本源代码不需要再import React来使用jsx了。
如果子元素为多个时使用jsxs方法,单个时使用jsx方法
jsx/jsxs方法的第一个参数还是元素名
第二个参数是一个对象,包含所有的属性和子元素,子元素使用children数组表示
第三个参数是元素的key值,单独拎出来了
编译具体过程
这个编译过程就是指上述jsx->函数调用的过程
编译过程使用插件:@babel/plugin-transform-react-jsx。
git源码:github.com/babel/babel…
整体过程如图:
createPlugin
文件目录:packages/babel-plugin-transform-react-jsx
可以看到src/index.ts导出了createPlugin方法,参数为name(插件名),development(运行环境)
所以编译的关键逻辑就在create-plugin.ts文件的createPlugin方法之中
declare
babel使用declare方法定义一个bable插件,declare方法的执行结果就是createPlugin的返回值
import { declare } from "@babel/helper-plugin-utils";
export default function createPlugin({
name,
development,
}: {
name: string;
development: boolean;
}) {
return declare((_, options: Options) => {
//...
}
}
初始化
对插件的配置进行初始化,包括我们上述的runtime配置
declare((_, options: Options) => {
// 配置初始化
const {
// defaults to true, Enables @babel/plugin-transform-react-pure-annotations. It will mark top-level React method calls as pure for tree shaking.
pure: PURE_ANNOTATION,
// defaults to true, 是否在使用XML名称空间标记名时抛出错误,虽然JSX规范允许这样做,但React的JSX目前不支持,所有要抛出错误
throwIfNamespace = true,
// 文档上没看到这个options,源码中说新版本runtime已经支持这个option,所以不管它了
filter,
// runtime配置:automatic | classic,在babel8之后默认值从classic改成了automatic
runtime: RUNTIME_DEFAULT = process.env.BABEL_8_BREAKING
? "automatic"
: development
? "automatic"
: "classic",
// automatic模式下配置,导入函数时替换的导入源,默认"react"
importSource: IMPORT_SOURCE_DEFAULT = DEFAULT.importSource,
// classic模式下配置,编译jsx时替换的函数,默认"React.createElement"
pragma: PRAGMA_DEFAULT = DEFAULT.pragma,
// classic模式下配置,当编译JSX fragments替换的组件,默认"React.Fragment"
pragmaFrag: PRAGMA_FRAG_DEFAULT = DEFAULT.pragmaFrag,
} = options;
// babel版本判断处理
if (process.env.BABEL_8_BREAKING) {
// useSpread,useBuiltIns,filter参数不让使用
// ...
}else{
// useSpread,useBuiltIns不是boolean报错,
// useSpread,useBuiltIns不能同时存在
// ...
}
}
解析与处理
declare方法返回的对象中我们可以看到解析与转化的具体逻辑了:
// 也是Babel的一个插件,主要作用是允许Babel解析JSX语法
import jsx from "@babel/plugin-syntax-jsx";
return declare((_, options: Options) => {
// ...
return {
// 插件名
name,
// 继承@babel/plugin-syntax-jsx插件的功能
inherits: jsx
// 当前插件的遍历逻辑(通过visitor处理ast节点)
visitor:{
}
}
}
为什么需要inherits来继承@babel/plugin-syntax-jsx库的能力?
- 语法解析: @babel/plugin-syntax-jsx 负责让 Babel 能够解析 JSX 语法。从而,任何对 JSX 进行转换的插件在能够转换之前都需要能解析 JSX。通过继承这个插件,babel-plugin-transform-react-jsx 获得了解析 JSX 的能力,不需要重新实现这个解析逻辑。
- 依赖性管理: inherits 排除了在 babel-plugin-transform-react-jsx 中直接包含和管理 JSX 语法解析逻辑的需要。对于 Babel 插件开发者来说,这意味着他们可以专注于转换逻辑本身,而不是如何解析 JSX。、
visitor作用?
我理解这里的visitor就是通过一个个回调和参数来获取和处理ast节点,其实和通过@babel/traverse工具来遍历某个代码片段的ast节点是一模一样的
处理ast节点
这里的要讲的就是visitor的具体逻辑了,这里列出来所有使用到的回调和参数
return declare((_, options: Options) => {
// ...
return {
// ...
// 当前插件的遍历逻辑(通过visitor处理ast节点)
visitor:{
JSXNamespacedName(path){
//...
},
JSXSpreadChild(path){
// ...
},
Program: {
enter(path, state) {
// ...
}
},
JSXFragment: {
exit(path, file) {
// ...
},
},
JSXElement: {
exit(path, file) {
// ...
},
},
JSXAttribute(path) {
// ...
},
}
}
}
JSXNamespacedName
JSXNamespacedName是 ast 节点的类型之一,指的是那些使用了命名空间命名的jsx节点
命名空间名称由冒号分隔,例如<my:component />,这里my是命名空间,component是实际的组件名称。
但是react中不支持命名空间,所以在这里默认是直接抛出错误 React's JSX doesn't support namespace tags
JSXNamespacedName(path) {
if (throwIfNamespace) {
throw path.buildCodeFrameError(
`Namespace tags are not supported by default. React's JSX doesn't support namespace tags. \
You can set `throwIfNamespace: false` to bypass this warning.`,
);
}
}
JSXSpreadChild
JSXSpreadChild 是 Babel AST 中用于描述 JSX 元素里的扩展子节点的类型,这个节点类型表示了在 JSX 元素的子节点中使用了扩展操作符(即 {...})来扩展表达式,例如:
// JSX 代码
<div>{...myVar}</div>
// 对应的 Babel AST
{
type: "JSXSpreadChild",
expression: {
type: "Identifier",
name: "myVar"
}
}
而react中是不允许使用扩展子节点的,所以这个地方babel直接就抛出错误:"Spread children are not supported in React "
JSXSpreadChild(path) {
throw path.buildCodeFrameError(
"Spread children are not supported in React.",
);
},
实际项目中使用报错如下:
这里应该直接使用 {listItems}
但是不知道为什么,直播工程中使用SpreadChild就没有报错,可能进行了兼容处理?这个后续我再看看
Program
-
Program 对象代表了整个文件的 AST(抽象语法树)节点,通常在这里处理文件级别的转换逻辑
-
enter是插件进入 Program 节点时执行的回调
-
path:是一个包含了当前AST节点信息的对象,提供了操作当前节点和其子节点的方法
-
state: 是一个跨插件传递信息的对象,允许插件访问和共享整个转换过程中的状态。
- 可以使用state.set(key, value)将值存在state中
- 可以使用state.get(key)获取通过state.set(key,value)存储的值
import { types as t } from "@babel/core";
import { addNamed, addNamespace, isModule } from "@babel/helper-module-imports";
// 匹配的正则
const JSX_SOURCE_ANNOTATION_REGEX =
/^\s**?\s*@jsxImportSource\s+([^\s]+)\s*$/m;
const JSX_RUNTIME_ANNOTATION_REGEX = /^\s**?\s*@jsxRuntime\s+([^\s]+)\s*$/m;
const JSX_ANNOTATION_REGEX = /^\s**?\s*@jsx\s+([^\s]+)\s*$/m;
const JSX_FRAG_ANNOTATION_REGEX = /^\s**?\s*@jsxFrag\s+([^\s]+)\s*$/m;
const get = (pass: PluginPass, name: string) =>
pass.get(`@babel/plugin-react-jsx/${name}`);
const set = (pass: PluginPass, name: string, v: any) =>
pass.set(`@babel/plugin-react-jsx/${name}`, v);
// ...
Program:{
enter(path, state) {
const { file } = state;
// 一些状态初始化
let runtime: string = RUNTIME_DEFAULT;
let source: string = IMPORT_SOURCE_DEFAULT;
let pragma: string = PRAGMA_DEFAULT;
let pragmaFrag: string = PRAGMA_FRAG_DEFAULT;
let sourceSet = !!options.importSource;
let pragmaSet = !!options.pragma;
let pragmaFragSet = !!options.pragmaFrag;
// file.ast.comments是当前文件中的所有注释节点
if (file.ast.comments) {
// 遍历注释节点数组
for (const comment of file.ast.comments) {
// 通过正则匹配 @jsxImportSource 注释
const sourceMatches = JSX_SOURCE_ANNOTATION_REGEX.exec(
comment.value,
);
if (sourceMatches) {
source = sourceMatches[1];
sourceSet = true;
}
// 通过正则匹配 @jsxRuntime 注释
const runtimeMatches = JSX_RUNTIME_ANNOTATION_REGEX.exec(
comment.value,
);
if (runtimeMatches) {
runtime = runtimeMatches[1];
}
// 通过正则匹配 @jsx 注释
const jsxMatches = JSX_ANNOTATION_REGEX.exec(comment.value);
if (jsxMatches) {
pragma = jsxMatches[1];
pragmaSet = true;
}
// 通过正则匹配 @jsxFrag 注释
const jsxFragMatches = JSX_FRAG_ANNOTATION_REGEX.exec(
comment.value,
);
if (jsxFragMatches) {
pragmaFrag = jsxFragMatches[1];
pragmaFragSet = true;
}
}
}
// 将当前的runtime值存到state之中,babel8之后默认automatic
set(state, "runtime", runtime);
// 如果运行时是classic
if (runtime === "classic") {
// 如果importSource被设置,抛出错误
if (sourceSet) {
throw path.buildCodeFrameError(
// 运行时是classic时不能设置importSourve
`importSource cannot be set when runtime is classic.`,
);
}
// 这里pragma默认React.createElement, pragmaFrag默认React.Fragment
// 将React.createElement转换成MemberExpression节点
const createElement = toMemberExpression(pragma);
// 将React.Fragment转换成MemberExpression节点
const fragment = toMemberExpression(pragmaFrag);
// 将节点信息存进state中
set(state, "id/createElement",
() => t.cloneNode(createElement));
set(state, "id/fragment", () => t.cloneNode(fragment));
// 保存defaultPure配置
set(state, "defaultPure", pragma === DEFAULT.pragma);
// 运行时为automatic时的逻辑
} else if (runtime === "automatic") {
// 运行时为automatic时不能设置pragma和pragmaSet
if (pragmaSet || pragmaFragSet) {
throw path.buildCodeFrameError(
`pragma and pragmaFrag cannot be set when runtime is automatic.`,
);
}
// 将创建好的导入语句的MemberExpression节点保存到state
const define = (name: string, id: string) =>
set(state, name, createImportLazily(state, path, id, source));
define("id/jsx", development ? "jsxDEV" : "jsx");
define("id/jsxs", development ? "jsxDEV" : "jsxs");
define("id/createElement", "createElement");
define("id/fragment", "Fragment");
set(state, "defaultPure", source === DEFAULT.importSource);
} else {
throw path.buildCodeFrameError(
`Runtime must be either "classic" or "automatic".`,
);
}
},
}
// 将字符串形式的标识符转换为 ast 中的 MemberExpression 节点
/**
* 具体步骤:
* 1.分割传入的 id 字符串,使用点 . 作为分隔符,得到标识符数组。
* 2.使用 map 函数遍历这个数组,并为每个标识符创建一个 Identifier 节点。
* 3.使用 reduce 函数组合这些 Identifier 节点,构造一个嵌套的
* MemberExpression。
* 示例:
* 输入:React.creatElement
* 输出:
* {
* "type": "MemberExpression",
* "object": {
* "type": "Identifier",
* "name": "React"
* },
* "property": {
* "type": "Identifier",
* "name": "createElement"
* }
* }
*/
function toMemberExpression(id: string):
Identifier | MemberExpression {
return (
id.split(".")
.map(name => t.identifier(name))
.reduce((object, property) =>
t.memberExpression(object, property))
);
}
function createImportLazily(
/** babel state */
pass: PluginPass,
/** babel path */
path: NodePath<Program>,
/** 需要导入的方法名,比如:jsx, jsxs, createElement, Fragment */
importName: string,
/** 导入源,默认react */
source: string,
): () => Identifier | MemberExpression {
return () => {
// 获取实际导入源,默认是react/jsx-runtime
const actualSource = getSource(source, importName);
// 如果是Esmodule
if (isModule(path)) {
let reference = get(pass, `imports/${importName}`);
// 如果已经有reference state,直接return出去
if (reference) return t.cloneNode(reference);
// 没有reference就计算,然后存入state
/**
* 这个reference代表对importName的引用
* addNamed举个例子:
* 当需要引用 useState 时,创建一个延迟执行的函数
* const importUseState = createImportLazily(
* pass, path, "useState", "react"
* );
* 在插件的某个部分需要使用 useState 时,调用该函数
* const useStateReference = importUseState();
*/
reference = addNamed(path, importName, actualSource, {
importedInterop: "uncompiled",
importPosition: "after",
});
set(pass, `imports/${importName}`, reference);
return reference;
} else {
// 如果不是Esmodule的导入逻辑
let reference = get(pass, `requires/${actualSource}`);
if (reference) {
reference = t.cloneNode(reference);
} else {
reference = addNamespace(path, actualSource, {
importedInterop: "uncompiled",
});
set(pass, `requires/${actualSource}`, reference);
}
return t.memberExpression(reference, t.identifier(importName));
}
};
}
// 通过拼接得到实际导入源,默认是react/jsx-runtime
function getSource(source: string, importName: string) {
switch (importName) {
case "Fragment":
return `${source}/${development ? "jsx-dev-runtime" : "jsx-runtime"}`;
case "jsxDEV":
return `${source}/jsx-dev-runtime`;
case "jsx":
case "jsxs":
return `${source}/jsx-runtime`;
// 如果是createElement直接return react
case "createElement":
return source;
}
}
所以在这里Program中的逻辑就是做一些数据准备,为后面的实例转换逻辑提供计算好的节点数据
JSXFragment
JSXFragment: {
// 插件即将离开 JSXFragment 节点时执行的逻辑
exit(path, file) {
let callExpr;
// 生成编译Fragment元素的回调方法
if (get(file, "runtime") === "classic") {
callExpr = buildCreateElementFragmentCall(path, file);
} else {
callExpr = buildJSXFragmentCall(path, file);
}
// 执行该回调来对<></>进行替换
path.replaceWith(t.inherits(callExpr, path.node));
},
},
/** 生成将 <></> 编译成
* React.createElement(React.Fragment, null, ...children)的回调
*/
function buildCreateElementFragmentCall(
path: NodePath<JSXFragment>,
file: PluginPass,
) {
return call(
file,
// 获取state的key
"createElement",
// 回调的参数
[
get(file, "id/fragment")(),
t.nullLiteral(),
...t.react.buildChildren(path.node),
]
);
}
/** 生成将 <></> 编译成
* React.jsx(type, arguments)的回调
*/
function buildJSXFragmentCall(
path: NodePath<JSXFragment>,
file: PluginPass,
) {
const args = [get(file, "id/fragment")()];
const children = t.react.buildChildren(path.node);
args.push(
t.objectExpression(
children.length > 0
? [
buildChildrenProperty(
children,
),
]
: [],
),
);
// 如何子元素>1就编译成jsxs方法,反之编译成jsx
return call(file, children.length > 1 ? "jsxs" : "jsx", args);
}
// 根据name得到对应的函数调用表达式
function call(
pass: PluginPass,
name: string,
args: CallExpression["arguments"],
) {
// get(pass, `id/${name}`)()可以获取之前存储的函数引用
const node = t.callExpression(get(pass, `id/${name}`)(), args);
return node;
}
JSXElement
JSXElement: {
exit(path, file) {
let callExpr;
// 得到编译jsx元素的回调
if (
get(file, "runtime") === "classic" ||
shouldUseCreateElement(path)
) {
callExpr = buildCreateElementCall(path, file);
} else {
callExpr = buildJSXElementCall(path, file);
}
// 执行回调进行替换
path.replaceWith(t.inherits(callExpr, path.node));
},
},
/**
* 生成将jsx元素编译成React.createElement(type,args,children)的回调
*/
function buildCreateElementCall(
path: NodePath<JSXElement>,
file: PluginPass,
) {
// openingElement 属性代表了一个 JSX 元素的开始标签(例如 <div>)
const openingPath = path.get("openingElement");
return call(file, "createElement", [
// 根据元素开始标签获取tag名,比如说<div> -> "div"
getTag(openingPath),
// 获取元素props对象,比如<div a="1" b="2"> -> {a:"1",b:"2"}
buildCreateElementOpeningElementAttributes(
file,
path,
openingPath.get("attributes"),
),
// 元素的children数组
...t.react.buildChildren(path.node),
]);
}
/**
* 生成将jsx元素编译成jsx(type,args,key)的回调
*/
function buildJSXElementCall(
path: NodePath<JSXElement>,
file: PluginPass)
{
// 获取元素开始标签
const openingPath = path.get("openingElement");
// 参数列表,第一项为元素tag名
const args: t.Expression[] = [getTag(openingPath)];
// 属性列表
const attribsArray = [];
// 提取参数
const extracted = Object.create(null);
for (const attr of openingPath.get("attributes")) {
if (attr.isJSXAttribute() && t.isJSXIdentifier(attr.node.name)) {
const { name } = attr.node.name;
switch (name) {
case "__source":
case "__self":
if (extracted[name]) throw sourceSelfError(path, name);
// 如果是key,放入extracted参数中
case "key": {
const keyValue =
convertAttributeValue(attr.node.value);
extracted[name] = keyValue;
break;
}
// 默认把元素属性放入属性列表
default:
attribsArray.push(attr);
}
} else {
attribsArray.push(attr);
}
}
// 把子元素也放入和属性列表合并,放到一起,例如:
//{a:"1",b:"2",children:[]}
const children = t.react.buildChildren(path.node);
let attribs: t.ObjectExpression;
if (attribsArray.length || children.length) {
attribs = buildJSXOpeningElementAttributes(
attribsArray,
children,
);
} else {
// attributes should never be null
attribs = t.objectExpression([]);
}
args.push(attribs);
// 如何子元素>1就编译成jsxs方法,反之编译成jsx
return call(file, children.length > 1 ? "jsxs" : "jsx", args);
}
JSXAttribute
本质上就是对于属性为jsx元素的时候做兼容处理,加上花括号
JSXAttribute(path) {
// 如果属性是一个jsxELement
if (t.isJSXElement(path.node.value)) {
// 将属性用 {} 包裹一下
// 比如:<div title=<div a="1"/> > --> <div title={<div a="1"/>} >
path.node.value = t.jsxExpressionContainer(path.node.value);
}
},
写在最后
这样我们就完整的过完了babel编译jsx的整体,希望这个解读能让你对jsx编译时有更多的理解,也能学习到其中的一些解决问题的思路
参考
Introducing the New JSX Transform:legacy.reactjs.org/blog/2020/0…
@babel/plugin-transform-react-jsx:babeljs.io/docs/babel-…
转载自:https://juejin.cn/post/7410347763898875916