AST原理,让你蜕变为高级前端工程师的原理

9,696 阅读7分钟

一.抽象语法树

抽象语法树(Abstract Syntax Tree)

webpack和Link等很多工具和库的核心都是通过Abstract Syntax Tree抽象语法树这个概念来实现对代码的检查、分析等操作的。

通过了解抽象语法树这个概念,你也可以随意编写类似的工具。

二.抽象语法树用途

  • 代码语法的检查、代码风格的检查、代码格式化、代码高亮、代码错误提示、代码自动补全等
    • 如JSLint、JSHint对代码错误或风格的检查,发现一些潜在错误
    • IDE的错误提示、格式化、高亮、自动补全等
  • 代码混淆压缩
    • UglifyJS2等
  • 优化变更代码,改变代码结构使其达到想要的结构
    • 代码打包工具webpack、rollup等
    • CommonJS、AMD、CMD、UMD等代码规范之间的转化
    • CoffeeScript、TypeScript、JSX等转化为原生Javascript

三.抽象语法树定义

这些工具的原理都是通过Javascript Parser把代码转化为一颗抽象语法树(AST),这颗树定义了代码的结构,通过操纵这棵树,我们可以精准定位到声明语句、赋值语句、运算语句等等,实现对代码的分析、优化、变更等操作

在计算机科学中,抽象语法树(abstract syntax tree 或者缩写为AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。

Javascript的语法是为了给开发者更好的编程而设计的,但是不适合程序的理解。所以需要转化为AST来使之更适合程序分析,浏览器编译器一般会把源码转化为AST来进行进一步的分析等其他操作。

var AST = "is Tree";
{
    "type": "Program",
    "body": [{
        "type": "VariableDeclaration",
        "kind": "var",
        "declarations": [{
            "type": "VariableDeclarator",
            "id": {
                "type": "Identifier",
                "name": "AST"
            },
            "init": {
                "type": "Literal",  //文本格式
                "value": "is Tree",
                "raw": "\"is Tree\""
            }
        }]
    }]
}

https://astexplorer.net/

四.JavaScript Parser

  • JavaScript Parser,把js源码转化为抽象语法树的解析器
  • 浏览器会把js源码通过解析器转化为抽象语法树,再进一步转化为字节码或直接生成机器码
  • 一般来说每个js引擎都会有自己的抽象语法树格式,Chrome的v8引擎,FireFox的SpiderMonkey引擎等等,MDN提供了详细的SpiderMonkey AST Format的详细说明,算是业界标准。

4.1 常用JavaScript Parser解析工具有:

  • esprima
  • traceur
  • acorn
  • shift

4.2 esprima

  • 通过esprima把源码转化为AST
  • 通过estraverse遍历并更新AST
  • 通过escodegen将AST重新生成源码
  • astexplorer AST 可视化工具
npm install esprima estraverse escodegen
let esprima = require('esprima');           //源代码转成AST语法树
let estraverse = require('estraverse');     //遍历语法树
let escodegen = require('escodegen');       //把AST语法树重新生成代码的工具

let sourceCode = 'function ast(){}'
let ast = esprima.parse(sourceCode);

let indent = 0;
function pad(){
    return "  ".repeat(indent)
}
estraverse.traverse(ast,{
    enter(node){
        console.log(pad() + node.type);
        indent += 2;
    },
    leave(node){
        indent -= 2;
        console.log(pad() + node.type)
    }
})

五.转化箭头函数

  • 访问者模式Visitor对于某个对象或者一组对象,不同的访问者,产生的结果不同,执行操作也不同
  • @babel/core Babel的编译器,核心API都在这里面,比如常见的transform、parse
  • babylon Babel的解析器
  • babel-types 用于AST节点的Lodash式工具库,它包含了构造,验证以及变换AST节点的方法,对编写处理AST逻辑非常有用
  • babel-traverse用于对AST的遍历,维护了整棵树的状态,并且负责替换、移除和添加节点
  • babel-types-api
  • Babel插件手册
  • babeljs.io babel可视化编辑器
  • babel-plugin-transform-es2015-arrow-functions babel转化箭头函数
npm install @babel/core babel-types -D
let babel = require('@babel/core');     //用来生成语法树,并且遍历转化语法树
let types = require('babel-types');     //用来生成新节点,或者判断某个节点是否是某个类型
const { generate } = require('escodegen');

const sourceCode = `const sum = (a,b) => a+b`;

//插件的结构
let transformArrayFunction = {
    visitor: {  //访问者模式;可以访问源代码生成的语法树所有节点,捕获特定节点
        //捕获箭头函数表达式,转成普通函数
        ArrowFunctionExpression: (path,state) => {
            let id = path.parent.id;        //path.node代表当前节点,path.parent代表父节点
            let arrowNode = path.node;
            let params = arrowNode.params;
            let body = arrowNode.body;      //BinaryExpression
            let generator = arrowNode.generator;
            let async = arrowNode.async;
            //types.blockStatement 生成一个函数体
            let functionExpression = types.functionExpression(id,params,types.blockStatement([types.returnStatement(body)]),generator,async)
            path.replaceWith(functionExpression);       //将转化的新节点替换老节点
        }
    }
}
let result = babel.transform(sourceCode,{
    plugins: [transformArrayFunction],

});

console.log(result);
{
  metadata: {},
  options: {
    babelrc: false,
    configFile: false,
    passPerPreset: false,
    envName: 'development',
    cwd: 'd:\\111前端优选\\TODO\\ast',
    root: 'd:\\111前端优选\\TODO\\ast',
    plugins: [ [Plugin] ],
    presets: [],
    parserOpts: { sourceType: 'module', sourceFileName: undefined, plugins: [] },
    generatorOpts: {
      filename: undefined,
      auxiliaryCommentBefore: undefined,
      auxiliaryCommentAfter: undefined,
      retainLines: undefined,
      comments: true,
      shouldPrintComment: undefined,
      compact: 'auto',
      minified: undefined,
      sourceMaps: false,
      sourceRoot: undefined,
      sourceFileName: 'unknown'
    }
  },
  ast: null,
  code: 'const sum = function sum(a, b) {\n  return a + b;\n};',  //结果
  map: null,
  sourceType: 'module'
}

语法树操作三步:

  • 根据源代码生成语法树
  • 转化语法树
  • 根据语法树生成转化后的代码

六.AST

1.解析过程

AST整个解析过程分为两个步骤:

  • 分词,将整个代码字符串分割成语法单元数组
  • 语法分析,建立分析语法单元之间的关系

2.语法单元

Javascript代码中语法单元主要包括以下几种:

  • 关键字: const、let、var等
  • 标识符:可能是一个变量,也可能是if/else关键字,或者true/false常量
  • 运算符
  • 数字
  • 空格
  • 注释

3.词法分析

let sourceCode = `let   element   = <h1>hello</h1>`;

/**
 * 1.分词,把token拆开 词法分析,就是把代码转成一个token数组
 */

function lexical(code){
    const tokens = [];
    for(let i=0;i<code.length;i++){
        let ch = code.charAt(i);    //l     i=3 ch=空格
        if(/[a-zA-Z_]/.test(ch)){   //判断是否为合理变量名、标识符
            const token = {type: 'Indentifier',value: ch};
            tokens.push(token);
            for(i++;i<code.length;i++){     //再向后移,判断是不是英文字母
                ch = code.charAt(i);    //i=1 ch=e
                if(/[a-zA-Z_]/.test(ch)){ 
                    token.value += ch;      //value=l value=le value=let
                }else{      //i=3 ch=空格
                    if(token.value == 'let'){
                        token.type = 'KeyWord';
                    }
                    i--;        //将空格回减
                    break;
                }
            }
            continue;
        }else if(/\s/.test(ch)){    //如果ch是空格的话
            const token = {
                type: "WhiteSpace",
                value: " "
            }
            tokens.push(token);
            for(i++;i<code.length;i++){
                ch = code.charAt(i);
                if(/\s/.test(ch)){
                    token.value += ch;
                }else{  //关键字和变量名之间的空格(多个)结束
                    i--;
                    break;
                }
            }
            continue;
        }else if(ch == '='){
            const token = {
                type: "Equal",
                value: "="
            };
            tokens.push(token);
        }else if(ch == '<'){
            const token = {
                type: 'JSXElement',     //遇到小于号,则为JSX元素
                value: ch
            }
            tokens.push(token);         //<h1>hello</h1>
            let isClose = true;         //判断是否遇到闭合标签  <hr/> <h1></h1>
            for(i++;i<code.length;i++){
                ch = code.charAt(i);    //ch = h
                token.value += ch;
                if(ch == "/"){
                    isClose = true;     //遇到斜杠时则下一个大于号则为闭合标签
                }
                if(ch == ">"){          //说明标签结束
                    if(isClose){
                        break;
                    }
                }
            }
            continue;
        }
    }
    return tokens;
}
let tokens = lexical(sourceCode);
console.log(tokens);
/**
[
  { type: 'KeyWord', value: 'let' },
  { type: 'WhiteSpace', value: '   ' },
  { type: 'Indentifier', value: 'element' },
  { type: 'WhiteSpace', value: '   ' },
  { type: 'Equal', value: '=' },
  { type: 'WhiteSpace', value: ' ' },
  { type: 'JSXElement', value: '<h1>' },
  { type: 'Indentifier', value: 'hello' },
  { type: 'JSXElement', value: '</h1>' }
]
 */

4.语法分析

  • 语义分析则是将得到的词汇进行一个立体的组合,确定词语之间的关系
  • 简单来说语法分析是对语句和表达式的识别,是一个递归的过程
function parse(tokens){
    let ast = {
        type: 'Program',
        body: [],
        sourceType: 'module'
    };
    let i = 0;      //当前的索引
    let currentToken;   //当前的token
    while(currentToken = tokens[i]){
        //第一次的时候 currentToken = { type: 'KeyWord', value: 'let' }
        if(currentToken.type == 'KeyWord' && currentToken.value == 'let'){  //或者var/const
            let VariableDeclaration = {
                type: 'VariableDeclaration',declarations: []
            };
            ast.body.push(VariableDeclaration);
            i+=2;   //{ type: 'Indentifier', value: 'element' },
            currentToken = tokens[i];
            let variableDeclarator = {
                type: 'VariableDeclarator',
                id: {
                    type: 'Indentifier',name: currentToken.value
                },
            }
            VariableDeclaration.declarations.push(variableDeclarator);
            i+=2;  //i=4 //
            currentToken = tokens[i];   //{ type: 'JSXElement', value: '<h1>hello</h1>' },
            if(currentToken.type == "String"){
                variableDeclarator.init = {type: 'StringLiteral',value: currentToken.value}
            }else if(currentToken.type == "JSXElement"){
                let value = currentToken.value;
                let [,type,children] = value.match(/<([^>]+?)>([^>]+)<\/\1>/);   //<h1></h1>  type=h1 children=hello
                variableDeclarator.init = {
                    type: 'JSXElement', //JSX元素
                    openingElement: {
                        type: 'openingElement',
                        name: {
                            type: 'JSXIndetifier',
                            name: type
                        }
                    },
                    closingElement: {
                        type: 'closingElement',
                        name: {
                            type: 'JSXIndentifier',
                            name: type
                        }
                    },
                    children: [
                        {type: 'JSXElement',value: children}
                    ]
                }
            }
        }
        i++;
    }
    return ast;
}

let tokens = [
    { type: 'KeyWord', value: 'let' },
    { type: 'WhiteSpace', value: ' ' },
    { type: 'Indentifier', value: 'element' },
    { type: 'WhiteSpace', value: ' ' },
    { type: 'Equal', value: '=' },
    { type: 'WhiteSpace', value: ' ' },
    { type: 'JSXElement', value: '<h1>hello</h1>' }
]

let ast = parse(tokens);
ast.body[0].declarations[0].init = {
    "type": "ExpressionStatement",
    "expression": {
        "type": "CallExpression",
        "callee": {
            "type": "MemberExpression",
            "computed": false,
            "object": {
                "type": "Indentifier",
                "name": "React"
            },
            "property": {
                "type": "Indentifier",
                "name": "createElement"
            }
        },
        "arguments": [
            {
                "type": "Literal",
                "value": "h1",
                "raw": "\"h1\""
            },
            {
                "type": "Literal",
                "value": null,
                "raw": "null"
            },
            {
                "type": "Literal",
                "value": "hello",
                "raw": "\"hello\""
            }
        ]
    }
}
console.log(JSON.stringify(ast));

/**
 * {"type":"Program","body":[{"type":"VariableDeclaration","declarations":[{"type":"VariableDeclarator","id":{"type":"Indentifier","name":"element"},"init":{"type":"ExpressionStatement","expression":{"type":"CallExpression","callee":{"type":"MemberExpression","computed":false,"object":{"type":"Indentifier","name":"React"},"property":{"type":"Indentifier","name":"createElement"}},"arguments":[{"type":"Literal","value":"h1","raw":"\"h1\""},{"type":"Literal","value":null,"raw":"null"},{"type":"Literal","value":"hello","raw":"\"hello\""}]}}}]}],"sourceType":"module"}
 */

至此就简单实现了语法树的转化,主要是在于思路上对源码到语法树解析的三个步骤。通过语法树原理的分析,有助于我们对Babel,Webpack等编译插件的原理分析,并应用于日常开发中。


获取更多前端资讯,欢迎搜索并关注公众号【前端优选】

前端优选

本文使用 mdnice 排版