什么是 babel?

babel 是一个 JavaScript compiler。
babel 可以做以下几个事情:

  • 转换语法
  • polyfill 目标环境中缺少的功能(例如通过 core-js 实现的第三方 polyfill)
  • 源代码转换
  • 解析 jsx
  • 解析 flow 和 TypeScript

babel 有以下几个特点:

  • 可插件化
  • 可调试(支持 Source Map)
  • 规范性(遵循 ECMAScript)
  • 可压缩(使用尽可能少的代码而不依赖庞大的运行时环境)

    AST

    要想弄清楚 babel 的原理之前,首先要搞清楚 AST 的概念。
    我们需要了解 babel 是如何将 JavaScript 代码生成 AST的,babel 又是如何操作 AST 的。
    image.png

分析 AST 代码可以查看: https://astexplorer.net/

babel 的工作流程

image.png
大多数的编译器工作流程基本分为三个部分:

  1. Parse(解析):将源码转换成更加抽象的表示方法 (AST)
  2. Transform(转换):将 AST 进行特殊处理,让它复合编译器的期望
  3. Generator (代码生成):将第二步得到转换过的 AST 生成新的代码

如:ES6 -> ES5:

  1. Parse:得到 ES6 AST
  2. Transform:得到 ES5 AST
  3. Generator:生成 ES5 Code

1、Parse (解析)

一般来说,Parse 阶段可以细分为两个阶段:词法分析(Lexical Analysis, LA)和语法分析(Syntactic Analysis, SA)。

词法分析

词法分析阶段可以看成对代码进行分词,它接收一段源代码,然后执行一段 tokenize 函数,把代码分割成 Token。Token 是一段数组,可以通过:https://esprima.org/demo/parse.html# 生成。

  1. var answer = 6 * 7;

如上述代码,拆分成的 Token 数组为:

  1. [
  2. {
  3. "type": "Keyword", // 关键字
  4. "value": "var"
  5. },
  6. {
  7. "type": "Identifier", // 标识符
  8. "value": "answer"
  9. },
  10. {
  11. "type": "Punctuator", // 标点符号
  12. "value": "="
  13. },
  14. {
  15. "type": "Numeric", // 数值
  16. "value": "6"
  17. },
  18. {
  19. "type": "Punctuator",
  20. "value": "*"
  21. },
  22. {
  23. "type": "Numeric",
  24. "value": "7"
  25. },
  26. {
  27. "type": "Punctuator",
  28. "value": ";"
  29. }
  30. ]

具体 tokenize 函数可以参考这段代码:https://github.com/babel/babel/tree/master/packages/babel-parser/src/tokenizer

语法分析

词法分析后,代码就已经变成一个 Tokens 数组了,现在需要通过语法分析吧 Tokens 数组转化为 AST。
具体 parser 可以参考:https://github.com/babel/babel/tree/master/packages/babel-parser/src/parser

  1. {
  2. "type": "Program", // AST 的根节点一定是 Program
  3. "body": [
  4. {
  5. "type": "VariableDeclaration", // 变量声明
  6. "declarations": [ // 声明
  7. {
  8. "type": "VariableDeclarator",
  9. "id": {
  10. "type": "Identifier", // 标识符
  11. "name": "answer"
  12. },
  13. "init": {
  14. "type": "BinaryExpression", // 二项式
  15. "operator": "*",
  16. "left": {
  17. "type": "Literal", // 词法
  18. "value": 6,
  19. "raw": "6"
  20. },
  21. "right": {
  22. "type": "Literal",
  23. "value": 7,
  24. "raw": "7"
  25. }
  26. }
  27. }
  28. ],
  29. "kind": "var"
  30. }
  31. ],
  32. "sourceType": "script"
  33. }

2、Transform(转换)

具体转换的规范可以参考:https://github.com/estree/estree
Babel 对于 AST的遍历是深度优先遍历,对于 AST 上的每一个分支,Babel 都会先向下遍历走到尽头,然后再向上遍历退出刚才遍历过的节点,然后寻找下一个分支。

如上述代码,从 declarations 开始遍历:

  1. 声明了一个变量,并且我们已经知道了它的内部属性:(id、init)然后我们再依次访问它每一个属性及子节点
  2. id 的类型是 Identifier,表示这是一个标识符,它的名字是 answer
  3. init 也有几个属性:(type、operator、left、right)
    1. type 为 BinaryExpression 表示这是一个二项式
    2. operator 操作符为 *
    3. left 表示左值,值为 6
    4. right 表示右值,值为 7

Babel 会维护一个称作 Visitor 的对象,这个对象定义了用于 AST 中获取具体节点的方法。

Visitor

一个 Visitor 一般来说是这样的:

  1. var visitor = {
  2. ArrowFunction() {
  3. console.log('我是箭头函数');
  4. },
  5. IfStatement() {
  6. console.log('我是一个if语句');
  7. },
  8. CallExpression() {}
  9. };

当我们遍历 AST 的时候,如果匹配上一个 type,就会调用 visitor 里的方法。
这只是一个简单的 Visitor。
上面说过,Babel 遍历 AST 其实会经过两次节点:遍历的时候和退出的时候,所以实际上 Babel 中的 Visitor 应该是这样的:

  1. var visitor = {
  2. Identifier: {
  3. enter() {
  4. console.log('Identifier enter');
  5. },
  6. exit() {
  7. console.log('Identifier exit');
  8. }
  9. }
  10. };

该细节可以参考:https://github.com/babel/babel/tree/master/packages/babel-traverse

3、generate (生成)

经过 transfor 阶段,需要转译的代码已经经过转换生成新的 AST 了,最后一个阶段就是根据这个新生成的 AST 生成新的代码。

Babel 是通过 generator 来生成新代码的:https://github.com/babel/babel/tree/master/packages/babel-generator,当然这个步骤也是深度优先遍历。

以上的三个阶段对应了三个 babel 的核心 package:

  • babel-parser
  • babel-traverse
  • babel-generator

此外 Babel 还有几个较为核心的 package:

  • babel-cli
  • babel-polyfill
  • babel-preset-env:预设,除此之外还有 flow,react,TypeScript
  • babel-register
  • babel-standalone:为浏览器和其他非node .js环境提供了一个独立的Babel构建。

工具包:

  • babel-core
  • babel-code-frame
  • babel-runtime
  • babel-template
  • babel-types

如何实现一个 tiny-compiler?

https://github.com/jamiebuilds/the-super-tiny-compiler/blob/master/the-super-tiny-compiler.js
上述代码极具参考价值。
具体流程如下:

  1. tokenizer:将代码生成 token
  2. parser: 将 token 解析成 AST
  3. transformer:将 AST traverser 成 newAST
  4. generator:将 newAST 生成新代码
  1. 'use strict';
  2. function tokenizer(input) {
  3. let current = 0;
  4. let tokens = [];
  5. while (current < input.length) {
  6. let char = input[current];
  7. if (char === '(') {
  8. tokens.push({
  9. type: 'paren',
  10. value: '(',
  11. });
  12. current++;
  13. continue;
  14. }
  15. if (char === ')') {
  16. tokens.push({
  17. type: 'paren',
  18. value: ')',
  19. });
  20. current++;
  21. continue;
  22. }
  23. let WHITESPACE = /\s/;
  24. if (WHITESPACE.test(char)) {
  25. current++;
  26. continue;
  27. }
  28. let NUMBERS = /[0-9]/;
  29. if (NUMBERS.test(char)) {
  30. let value = '';
  31. while (NUMBERS.test(char)) {
  32. value += char;
  33. char = input[++current];
  34. }
  35. tokens.push({ type: 'number', value });
  36. continue;
  37. }
  38. if (char === '"') {
  39. let value = '';
  40. char = input[++current];
  41. while (char !== '"') {
  42. value += char;
  43. char = input[++current];
  44. }
  45. char = input[++current];
  46. tokens.push({ type: 'string', value });
  47. continue;
  48. }
  49. let LETTERS = /[a-z]/i;
  50. if (LETTERS.test(char)) {
  51. let value = '';
  52. while (LETTERS.test(char)) {
  53. value += char;
  54. char = input[++current];
  55. }
  56. tokens.push({ type: 'name', value });
  57. continue;
  58. }
  59. throw new TypeError('I dont know what this character is: ' + char);
  60. }
  61. return tokens;
  62. }
  63. function parser(tokens) {
  64. let current = 0;
  65. function walk() {
  66. let token = tokens[current];
  67. if (token.type === 'number') {
  68. current++;
  69. return {
  70. type: 'NumberLiteral',
  71. value: token.value,
  72. };
  73. }
  74. if (token.type === 'string') {
  75. current++;
  76. return {
  77. type: 'StringLiteral',
  78. value: token.value,
  79. };
  80. }
  81. if (
  82. token.type === 'paren' &&
  83. token.value === '('
  84. ) {
  85. token = tokens[++current];
  86. let node = {
  87. type: 'CallExpression',
  88. name: token.value,
  89. params: [],
  90. };
  91. token = tokens[++current];
  92. while (
  93. (token.type !== 'paren') ||
  94. (token.type === 'paren' && token.value !== ')')
  95. ) {
  96. node.params.push(walk());
  97. token = tokens[current];
  98. }
  99. current++;
  100. return node;
  101. }
  102. throw new TypeError(token.type);
  103. }
  104. let ast = {
  105. type: 'Program',
  106. body: [],
  107. };
  108. while (current < tokens.length) {
  109. ast.body.push(walk());
  110. }
  111. return ast;
  112. }
  113. function traverser(ast, visitor) {
  114. function traverseArray(array, parent) {
  115. array.forEach(child => {
  116. traverseNode(child, parent);
  117. });
  118. }
  119. function traverseNode(node, parent) {
  120. let methods = visitor[node.type];
  121. if (methods && methods.enter) {
  122. methods.enter(node, parent);
  123. }
  124. switch (node.type) {
  125. case 'Program':
  126. traverseArray(node.body, node);
  127. break;
  128. case 'CallExpression':
  129. traverseArray(node.params, node);
  130. break;
  131. case 'NumberLiteral':
  132. case 'StringLiteral':
  133. break;
  134. default:
  135. throw new TypeError(node.type);
  136. }
  137. if (methods && methods.exit) {
  138. methods.exit(node, parent);
  139. }
  140. }
  141. traverseNode(ast, null);
  142. }
  143. function transformer(ast) {
  144. let newAst = {
  145. type: 'Program',
  146. body: [],
  147. };
  148. ast._context = newAst.body;
  149. traverser(ast, {
  150. NumberLiteral: {
  151. enter(node, parent) {
  152. parent._context.push({
  153. type: 'NumberLiteral',
  154. value: node.value,
  155. });
  156. },
  157. },
  158. StringLiteral: {
  159. enter(node, parent) {
  160. parent._context.push({
  161. type: 'StringLiteral',
  162. value: node.value,
  163. });
  164. },
  165. },
  166. CallExpression: {
  167. enter(node, parent) {
  168. let expression = {
  169. type: 'CallExpression',
  170. callee: {
  171. type: 'Identifier',
  172. name: node.name,
  173. },
  174. arguments: [],
  175. };
  176. node._context = expression.arguments;
  177. if (parent.type !== 'CallExpression') {
  178. expression = {
  179. type: 'ExpressionStatement',
  180. expression: expression,
  181. };
  182. }
  183. parent._context.push(expression);
  184. },
  185. }
  186. });
  187. return newAst;
  188. }
  189. function codeGenerator(node) {
  190. switch (node.type) {
  191. case 'Program':
  192. return node.body.map(codeGenerator)
  193. .join('\n');
  194. case 'ExpressionStatement':
  195. return (
  196. codeGenerator(node.expression) +
  197. ';'
  198. );
  199. case 'CallExpression':
  200. return (
  201. codeGenerator(node.callee) +
  202. '(' +
  203. node.arguments.map(codeGenerator)
  204. .join(', ') +
  205. ')'
  206. );
  207. case 'Identifier':
  208. return node.name;
  209. case 'NumberLiteral':
  210. return node.value;
  211. case 'StringLiteral':
  212. return '"' + node.value + '"';
  213. default:
  214. throw new TypeError(node.type);
  215. }
  216. }
  217. function compiler(input) {
  218. let tokens = tokenizer(input);
  219. let ast = parser(tokens);
  220. let newAst = transformer(ast);
  221. let output = codeGenerator(newAst);
  222. return output;
  223. }
  224. module.exports = {
  225. tokenizer,
  226. parser,
  227. traverser,
  228. transformer,
  229. codeGenerator,
  230. compiler,
  231. };