用过旧版本React(<=17)的小伙伴,一定遇到过:如果没有在每个组件的头部引入import React from 'react',就会报错。但在开发者看来,导入了React,但是代码中并没有用上React啊!这是怎么回事呢? 先让我们来看一下下面的代码
代码1:
function App() {
return <h1>Hello World</h1>;
}
代码2:
import React from 'react';
function App() {
return React.createElement('h1', null, 'Hello world');
}
代码1和代码2是等价的,代码1是要被babel编译成代码2的。React项目中集成了babel-loader,而babel又预设了高阶js、jsx、flow、typescript的处理插件,将各种语法处理成浏览器能够理解的向后兼容的JavaScript语法。其中@babel/preset-react负责将jsx处理成js,也就是React.createElement形式,再结合React库,就可以转换为实际的DOM操作了。 回归正题,现在应该能理解,为什么不引入React会报错了,因为编译后的文件中调用了React.createElement()。
在React17发布后,不引入react也不会报错,这又是为什么呢? 因为新版本React采用了新的JSX转换器,这还得上溯到babel, babel的v7.9.0加入了React自动运行时功能,也就是在编译时自动引入jsx,这里的的jsx函数就可以理解为React.createElement。
// Inserted by a compiler (don't import it yourself!)
import {jsx as _jsx} from 'react/jsx-runtime';
function App() {
return _jsx('h1', { children: 'Hello world' });
}
说明白以上这些后,我们再来更深入一点,了解下Babel是如何把这些JSX转成JS的,这里仅以旧版React.createElement为例。 首先Babel是一个JavaScript编译器。通常说到的编译器,都是获取源代码,产生一个二进制文件,但是JavaScript编译器是将ECMAScript 2015+代码转换为向后兼容版本的JavaScript代码,babel还预设了JSX、Flow、Typescript的语法处理。因此一定程度上可以认为babel自带了编译JSX语法的能力,如果是其他的语法,则需要编写相应的babel插件。换句话说,你甚至可以定义自己的语法规则,只要你编写好相应的babel插件。
虽然Babel是针对浏览器的特殊编译器,但是它工作的流程和其他编译器相似,都是分为三个阶段:
- 解析(Parsing):将代码字符串解析成抽象语法树。
- 转换(Transformation):对抽象语法树进行转换操作。
- 生成(Code Generation): 根据变换后的抽象语法树再生成代码字符串。
解析(Parsing)
Babel会将源码解析抽象为抽象语法树(Abstract Tree, AST)。
比如React.createElement('h1', null, 'Hello world');经过词法分析、语法分析,会被解析为
{
"type": "ExpressionStatement",
"start": 0,
"end": 47,
"loc": {
"start": {...},
"end": {...}
},
"expression": {
"type": "CallExpression",
"start": 0,
"end": 46,
"loc": {
"start": {...},
"end": {...}
},
"callee": {
"type": "MemberExpression",
"start": 0,
"end": 19,
"loc": {
"start": {...},
"end": {...}
},
"object": {
"type": "Identifier",
"start": 0,
"end": 5,
"loc": {
"start": {...},
"end": {...},
"identifierName": "React"
},
"name": "React"
},
"computed": false,
"property": {
"type": "Identifier",
"start": 6,
"end": 19,
"loc": {
"start": {...},
"end": {...},
"identifierName": "createElement"
},
"name": "createElement"
}
},
"arguments": [
{
"type": "StringLiteral",
"start": 20,
"end": 24,
"loc": {
"start": {...},
"end": {...}
},
"extra": {
"rawValue": "h1",
"raw": "'h1'"
},
"value": "h1"
},
{
"type": "NullLiteral",
"start": 26,
"end": 30,
"loc": {
"start": {...},
"end": {...}
}
},
{
"type": "StringLiteral",
"start": 32,
"end": 45,
"loc": {
"start": {...},
"end": {...}
},
"extra": {
"rawValue": "Hello world",
"raw": "'Hello world'"
},
"value": "Hello world"
}
]
}
}
简化一点为
{
type": "ExpressionStatement",
expression: {
"type": "CallExpression",
"callee": {
"type": "MemberExpression",
"object": {
"type": "Identifier",
"name": "React"
},
"property": {
"type": "Identifier",
"name": "createElement",
}
},
"arguments": [
{
"type": "StringLiteral",
"value": "h1"
},
{
"type": "NullLiteral",
},
{
"type": "StringLiteral",
"value": "Hello world"
}
]
}
}
可以看出AST是一个树状结构,其中type表示节点类型,预设值比较多,比如:
- ExpressionStatement表示“表达式语句”
- StringLiteral表示“字符串字面值”,类似的有123类型是NumberLiteral,true类型是BooleanLiteral,等等。
- Identifier表示“标识符”。变量名、属性名、参数名等各种声明和引用的名字,都是Identifer。
到这里,完成了解析过程。
转换(Transformation)
该阶段使用babel预设的插件@babel/traverse和@babel/preset-react,对解析得到的AST进行遍历处理。当然并不是每一个节点都需要处理,babel的插件机制让我们可以针对特定类型的节点进行增删改查。这里主要将jsx的语法结构转换成能被浏览器广泛理解的语法结构,比如,JSX元素转换为React.createElement()调用,如此得到新的AST树。
生成(Code Generation)
该阶段,Babel使用一个生成器(Code Generator)对经过转换的AST进行遍历,将每个节点重新还原为符合目标语法的JavaScript代码字符串。
经过以上三个阶段,就生成了能被浏览器理解的运行的目标js代码。