Babel浅谈[下]

三. 原理

1、简述

Babel 的运行原理可以通过以下这张图来概括。整体来看,可以分为三个过程,分别是:1. 解析,2. 转换,3. 生成。而解析过程又可分为词法解析和语法解析两个过程。


我先以 箭头函数 转换成 函数声明 为例,大致描述下每一个过程,然后再通过具体的代码来演示每个过程做了什么事。

  • 解析:将箭头函数源代码字符串解析成箭头函数对应的抽象语法树,如图中红色框部分。
/*
 const fn = () => { console.log('xuemingli'); }; 经过词法分析得到如下Tokens
*/
[
  { type: 'Keyword', value: 'const' },
  { type: 'Identifier', value: 'fn' },
  { type: 'Punctuator', value: '=' },
  { type: 'Punctuator', value: '(' },
  { type: 'Punctuator', value: ')' },
  { type: 'Punctuator', value: '=>' },
  { type: 'Punctuator', value: '{' },
  { type: 'Identifier', value: 'console' },
  { type: 'Punctuator', value: '.' },
  { type: 'Identifier', value: 'log' },
  { type: 'Punctuator', value: '(' },
  { type: 'String', value: "'xuemingli'" },
  { type: 'Punctuator', value: ')' },
  { type: 'Punctuator', value: ';' },
  { type: 'Punctuator', value: '}' },
  { type: 'Punctuator', value: ';' }
]

  • 语法分析:将词法分析得到的 Tokens 解析成箭头函数对应的 AST(抽象语法树)。从 Tokens 提供的信息来分析出代码之间的逻辑关系,这种逻辑关系抽象成树状结构,就叫做 AST,对应到数据类型来说,它就是一个 JSON 对象。大家可以在AST 解析器[3]这个工具中的 JSON Tab 来查看,由于太大,我就不贴在这里了,下图展示了 AST 树状结构的样子。

  • 词法分析: 将箭头函数源代码字符串分割成 Tokens。 Tokens 是由 JS 中的标识符、运算符、括号、数字以及字符串等等独立且有意义的最小单元组成的集合,对应到数据类型来说,它就是一个数组,数组项是包含 type 和 value 这两个属性的对象,如下所示。


  • 转换:通过 @babel/transform-arrow-functions 插件操作(包括增、删、改)箭头函数对应的抽象语法树上的节点得到函数声明对应的抽象语法树,如图中蓝色框部分。在这里就能回答 「插件做了什么事?」这个问题了,所有的 babel 插件都是在 转换 这个过程中起作用的,都是操作源代码对应的抽象语法树上的节点来得到目标代码对应的抽象语法树。我们可以简单的把插件视为一个 visitor,可以 visit 以及 operate 抽象语法树上的任意一个节点。

  • 生成:将函数声明对应的抽象语法树按照 JS 语法规则,拼接成函数声明代码字符串,如图中黄色框部分。

2、代码实现

接下来我们通过实现一个最简易的编译器来将 (add 2 (subtract 4 2)) 这种语法转换成我们熟悉的 add(2, subtract(4, 2))这种语法。最终我们提供如下方法:

// 词法分析器
function tokenizer(sourceCode) {
    const tokens = [];
    ...
    return tokens;
}
// 语法解析器
function parser(tokens) {
    const ast = {};
    ...
    return ast;
}
// 遍历器
function traverse(ast, visitor) {
    /*
    将源代码对应的ast通过visitor的遍历和操作
    得到目标代码对应的newAst
    */
}
// 转换器
function transformer(ast) {
    const newAst = {};
    traverse(ast, visitor);
    return newAst;
}
// 生成器
function codeGenerator(newAst) {
    let targetCode = '';
    ...
    return targetCode;
}
// 编译器
function compiler(sourceCode) {
  const tokens = tokenizer(sourceCode);
  const ast    = parser(tokens);
  const newAst = transformer(ast);
  const targetCode = codeGenerator(newAst);
  return targetCode;
}
module.exports = {
  tokenizer,
  parser,
  traverse,
  transformer,
  codeGenerator,
  compiler,
};

事实上,我们实现的这个简易编译器的功能和组织结构与 Babel 生态系统中的功能和组织结构是一一对应的,如下图。


2.1 解析
2.1.1 词法分析

将 (add 2 (subtract 4 2)) 代码字符串分割成如下 Tokens:

 [
  { type: 'paren',  value: '('        },
  { type: 'name',   value: 'add'      },
  { type: 'number', value: '2'        },
  { type: 'paren',  value: '('        },
  { type: 'name',   value: 'subtract' },
  { type: 'number', value: '4'        },
  { type: 'number', value: '2'        },
  { type: 'paren',  value: ')'        },
  { type: 'paren',  value: ')'        },
 ]

具体代码实现如下,并添加了较为详细的注释:

// tokenizer.js
function tokenizer(sourceCode) {
  // 声明一个游标用来记录遍历的位置
  let current = 0;
  let tokens = [];
  // 通过一个循环,不断的遍历源码字符串
  while (current < sourceCode.length) {
    let char = sourceCode[current];
    if (char === '(') {
      tokens.push({
        type: 'paren',
        value: '(',
      });
      current++;
      continue;
    }
    if (char === ')') {
      tokens.push({
        type: 'paren',
        value: ')',
      });
      current++;
      continue;
    }
    // 由于空白字符对我们来说意义不大,故不放到Tokens中,直接跳过
    let WHITESPACE = /\s/;
    if (WHITESPACE.test(char)) {
      current++;
      continue;
    }
    // 因为数字可以是任意数量的字符,我们想要将整个数字序列捕获为一个标记。
    //(如 123 456)
    let NUMBERS = /[0-9]/;
    if (NUMBERS.test(char)) {
      let value = '';
      while (NUMBERS.test(char)) {
        value += char;
        char = sourceCode[++current];
      }
      tokens.push({ type: 'number', value });
      continue;
    }
    // 这里是想要获取运算函数名称,如 add 和 subtract
    let LETTERS = /[a-z]/i;
    if (LETTERS.test(char)) {
      let value = '';
      while (LETTERS.test(char)) {
        value += char;
        char = sourceCode[++current];
      }
      tokens.push({ type: 'name', value });
      continue;
    }
    throw new TypeError('I dont know what this character is: ' + char);
  }
  return tokens;
}
2.1.2 语法分析

将 Tokens 解析成 (add 2 (subtract 4 2)) 对应的 AST 的对象形式,如下所示:

{
  type: 'Program',
  body: [{
    type: 'CallExpression',
    name: 'add',
    params: [{
      type: 'NumberLiteral',
      value: '2'
    }, {
      type: 'CallExpression',
      name: 'subtract',
      params: [{
        type: 'NumberLiteral',
        value: '4'
      }, {
        type: 'NumberLiteral',
        value: '2'
      }]
    }]
  }]
}

具体代码实现如下,并添加了较为详细的注释:

// parse.js
function parser(tokens) {
  let current = 0;
  // 这里定义了一个被递归的函数walk,每次遍历Tokens时都会调用
  function walk() {
    let token = tokens[current];
    // 如果遍历到 number 就返回如下对象
    if (token.type === 'number') {
      current++;
      return {
        type: 'NumberLiteral',
        value: token.value,
      };
    }
    // 如果遍历到'(',跳过并取下一个Token,并返回如下对象,因为'('之后肯定是运算函数名称
    // 如 add 和 subtract
    if (
      token.type === 'paren' &&
      token.value === '('
    ) {
      token = tokens[++current];
      let node = {
        type: 'CallExpression',
        name: token.value,
        params: [],
      };
      token = tokens[++current];
      // 如果没有遍历到')',则递归walk,知道遍历到')'返回一个
      // {type: 'CallExpression', name: token.value, params: [xxx]}
      while (
        (token.type !== 'paren') ||
        (token.type === 'paren' && token.value !== ')')
      ) {
        node.params.push(walk());
        token = tokens[current];
      }
      current++;
      return node;
    }
    throw new TypeError(token.type);
  }
  let ast = {
    type: 'Program',
    body: [],
  };
  // 遍历Tokens并调用walk
  while (current < tokens.length) {
    ast.body.push(walk());
  }
  return ast;
}
2.2 转换
2.2.1 遍历 AST 节点

现在我们已经通过 parse 方法得到了 (add 2 (subtract 4 2)) 对应的 AST 了,这就轮到插件起作用了,插件可以遍历并操作 AST 上的任意节点,将节点修改成我们想要的样子。最终我们想得到 add(2, subtract(4, 2)) 对应的 AST 对象,如下所示:

{
  type: 'Program',
  body: [{
    type: 'ExpressionStatement',
    expression: {
      type: 'CallExpression',
      callee: {
        type: 'Identifier',
        name: 'add'
      },
      arguments: [{
        type: 'NumberLiteral',
        value: '2'
      }, {
        type: 'CallExpression',
        callee: {
          type: 'Identifier',
          name: 'subtract'
        },
        arguments: [{
          type: 'NumberLiteral',
          value: '4'
        }, {
          type: 'NumberLiteral',
          value: '2'
        }]
      }
    }
  }]
}

我这里用对象声明模拟了一个插件,可以视为 Babel 系统概念中的 visitor ,可以看到,插件最终对外暴露的就是一个包含处理各种节点逻辑的对象,只要匹配上节点,就走处理节点的逻辑,将其处理成我们希望的样子。如下所示:

{
    NumberLiteral: {
      enter(node, parent) {
        parent._context.push({
          type: 'NumberLiteral',
          value: node.value,
        });
      }
    },
    // 将CallExpression节点处理成我们希望得到的节点样子
    CallExpression: {
      enter(node, parent) {
        let expression = {
          type: 'CallExpression',
          callee: {
            type: 'Identifier',
            name: node.name,
          },
          arguments: [],
        };
        node._context = expression.arguments;
        if (parent.type !== 'CallExpression') {
          expression = {
            type: 'ExpressionStatement',
            expression: expression,
          };
        }
        parent._context.push(expression);
      }
    }
}

具体代码实现如下,并添加了较为详细的注释,traverse 方法需要结合 transformer 方法一起看,会更加清晰:

// transformer.js
function traverse(ast, visitor) {
  // 用来处理节点集合,即数组
  function traverseArray(array, parent) {
    array.forEach(child => {
      traverseNode(child, parent);
    });
  }
  // 用来处理单个节点,即对象
  function traverseNode(node, parent) {
    let methods = visitor[node.type];
    if (methods && methods.enter) {
      methods.enter(node, parent);
    }
    switch (node.type) {
      case 'Program':
        traverseArray(node.body, node);
        break;
      case 'CallExpression':
        traverseArray(node.params, node);
        break;
      case 'NumberLiteral':
        break;
      default:
        throw new TypeError(node.type);
    }
    if (methods && methods.exit) {
      methods.exit(node, parent);
    }
  }
  traverseNode(ast, null);
}

function transformer(ast) {
  let newAst = {
    type: 'Program',
    body: [],
  };
  const visitor = {
    NumberLiteral: {
      enter(node, parent) {
        parent._context.push({
          type: 'NumberLiteral',
          value: node.value,
        });
      },
    },
    CallExpression: {
      enter(node, parent) {
        let expression = {
          type: 'CallExpression',
          callee: {
            type: 'Identifier',
            name: node.name,
          },
          arguments: [],
        };
        node._context = expression.arguments;
        if (parent.type !== 'CallExpression') {
          expression = {
            type: 'ExpressionStatement',
            expression: expression,
          };
        }
        parent._context.push(expression);
      },
    }
  };
  ast._context = newAst.body;
  traverser(ast, visitor);
  return newAst;
}
2.3 生成

生成的过程比较简单,就是将 add(2, subtract(4, 2)) 对应的 AST 对象按照拼接成 add(2, subtract(4, 2)) 代码字符串便可,具体代码如下:

// generator.js
function codeGenerator(node) {
  switch (node.type) {
  // 遍历到'Program'节点,拼接个换行符即可
    case 'Program':
      return node.body.map(codeGenerator)
        .join('\n');
    // 遍历到'ExpressionStatement'节点,又将expression节点作为参数递归codeGenerator
    // 并拼接个 ';'
    case 'ExpressionStatement':
      return (
        codeGenerator(node.expression) +
        ';'
      );
    // 遍历到'CallExpression'节点,拼接运算表达式,
    // 如 (4, 2)
    case 'CallExpression':
      return (
        codeGenerator(node.callee) +
        '(' +
        node.arguments.map(codeGenerator)
          .join(', ') +
        ')'
      );
    // 遍历到'Identifier'节点,拼接运算表达式,
    // 如 subtract add
    case 'Identifier':
      return node.name;
    case 'NumberLiteral':
      return node.value;
    default:
      throw new TypeError(node.type);
  }
}

转载 Babel浅谈

你可能感兴趣的:(Babel浅谈[下])