前端构建基石之-AST&BABEL

125 阅读3分钟

1.AST

1.1 AST是什么?

AST,抽象语法树。

1.2 最常用的抽象语法树是什么?

babel eslint

1.3 为什么要用AST?

抽象语法树的核心目的是翻译。

1.4 举个例子🌰

中文: 我是小杜
英文: i am xiaodu

这其实就是一个翻译的过程。细分有以下步骤:
1. 句子 -> 单词 (这其实就是一个词法分析 lexical analysis)
2. 单词 -> 逐词翻译 (语法分析 syntacyic analysis)
3. 润色 (简单来说就是数据结构翻译成另一个数据结构 ast -> newAst)transform
4. 合并生成产物

根据以上,再结合前端知识不难想到:

    <div>
        <p>
            <span123></span>
        </p>
    </div>
    
====解析之后:
    
{
    tag:div
    children: {
        tag: p,
        children: {
            tag: span,
            children: 123
        }
    }
}


=====以上其实就是一个解析的过程
1. input -> tokenizer -> token   词法分析
2. token -> parser -> ast        语法分析
3. ast -> transform -> newAst    代码转换
4. newAst -> generate -> output  生成代码过程

2. 实现一个简单的编译器Compiler

image.png

定义以下的输入输出:
const input = '(add 2 (subtract 4 5))'
const output = '(add 2 (subtract 4 5))'

2.1 词法分析

就是一个将输入的字符串拆成toke的过程。

2.1.1 分析过程

function tokenizer(input){
    const tokens = [];
    let current = 0;

    while(current < input.length){
        let char = input[current];

        if(char === '(' || char === ')'){
            tokens.push({
                type: 'paren',
                value: char
            })
            current++;
            continue;
        }

        // 空格跳过
        let WHITESPACE = /\s/;
        if(WHITESPACE.test(char)){ 
            current++;
            continue;
        }

        // 处理数字
        let NUMBERS = /[0-9]/;
        if(NUMBERS.test(char)){
            let value = '';
            while(NUMBERS.test(char)){
                value += char;
                char = input[++current];
            }
            tokens.push({
                type: 'number',
                value
            })
            continue;
        }

        // 处理字符串
        let LETTERS = /[a-z]/i;
        if(LETTERS.test(char)){
            let value = '';
            while(LETTERS.test(char)){
                value += char;
                char = input[++current];
            }

            tokens.push({
                type: 'name',
                value
            })
            continue;
        }

        throw new TypeError('Unknown character: ' + char);
    }

    return tokens;
}

==== 调用测试:
const input = '(add 2 (subtract 4 5))'
const tokens = tokenizer(input);
console.log(tokens)

2.1.2 分析结果

[
  { 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: '5' },
  { type: 'paren', value: ')' },
  { type: 'paren', value: ')' }
]

2.2 语法分析

通过递归的方式把语法分析之后生成的tokens,转化为一个有层级结构的数据。

2.2.1 分析过程

function parser(tokens){
    let current = 0;
    function walk(){
        let token = tokens[current];
        if(token.type === 'number'){
            current++;
            return {
                type: 'NumberLiteral',
                value: token.value
            }
        }

        if(token.type === 'name'){
            current++;
            return {
                type: 'Identifier',
                name: token.value
            }
        }

        if(token.type === 'paren' && token.value === '('){
            token = tokens[++current];
            let node = {
                type: 'CallExpression',
                name: token.value,
                params: []
            }

            token = tokens[++current];
            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: []
    }

    while(current < tokens.length){
        ast.body.push(walk());
    }
    return ast;
}

====调用测试:
需要结合第一步的tokenizer
const input = '(add 2 (subtract 4 5))'
const tokens = tokenizer(input);
const ast = parser(tokens);
console.log(tokens)
console.log(JSON.stringify(ast))

2.2.2 分析结果

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

2.3 代码转换

其实类似于构建函数的钩子

2.3.1 转换过程

// webpack plugin 执行钩子 visitor 观察者模式
function traverser(ast, visitor){
    // parent 类似context上下文
    function traverserArray(array, parent){
        array.forEach(child => {
            traverserNode(child, parent);
        })
    }


    function traverserNode(node, parent){
        let methods = visitor[node.type];

        // 执行钩子函数
        if(methods && methods.enter){
            methods.enter(node, parent);
        }

        switch(node.type){
            case 'Program':
                traverserArray(node.body, node); 
                break;
            
            case 'CallExpression':
                traverserArray(node.params, node);
                break;
            
            case 'NumberLiteral':
                break;
            default:
                throw new TypeError(node.type);
        }
     
        if(methods && methods.exit){
            methods.exit(node, parent);
        }
    }

    traverserNode(ast, null);
}


function transformer(ast){
    let newAst = {
        type: 'Program',
        body: []
    }
    ast._context = newAst.body;

    traverser(ast, {
        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);
            }
        }
    })

    return newAst;
}

====调用测试:
const input = '(add 2 (subtract 4 5))'
const tokens = tokenizer(input);
const ast = parser(tokens);
const newAst = transformer(ast);
console.log(tokens)
console.log(JSON.stringify(ast))
console.log(JSON.stringify(newAst))

2.3.2 转换结果

{
  "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": "5"
              }
            ]
          }
        ]
      }
    }
  ]
}

2.4 代码生成

将新的ast生成新的代码结构。

2.4.1 生成过程

function codeGenerator(node){
    switch(node.type){
        case 'Program':
            return node.body.map(codeGenerator).join('\n');
        case 'ExpressionStatement':
            return codeGenerator(node.expression) + ';';
        case 'CallExpression':
            return (
                codeGenerator(node.callee) + '(' + node.arguments.map(codeGenerator).join(', ') + ')'
            )
        case 'Identifier':
            return node.name;
        case 'NumberLiteral':
            return node.value;
    }
}

====调用测试:
const input = '(add 2 (subtract 4 5))'
const tokens = tokenizer(input);
const ast = parser(tokens);
const newAst = transformer(ast);
// console.log(tokens)
// console.log(JSON.stringify(ast))
// console.log(JSON.stringify(newAst))
console.log(codeGenerator(newAst))

2.4.2 生成结果

add(2, subtract(4, 5));

2.5 编译整体过程

image.png