JSX如何编译成js

470 阅读4分钟

用过旧版本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): 根据变换后的抽象语法树再生成代码字符串。

image.png

解析(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代码。