精华内容
下载资源
问答
  • 抽象语法树
    2021-04-17 11:07:36

    1460000018997898

    AST 抽象语法树简介

    AST(Abstract Syntax Tree)是源代码的抽象语法结构树状表现形式,Webpack、ESLint、JSX、TypeScript 的编译和模块化规则之间的转化都是通过 AST 来实现对代码的检查、分析以及编译等操作。

    JavaScript 语法的 AST 语法树

    JavaScript 中想要使用 AST 进行开发,要知道抽象成语法树之后的结构是什么,里面的字段名称都代表什么含义以及遍历的规则,可以通过 http://esprima.org/demo/parse... 来实现 JavaScript 语法的在线转换。

    通过在线编译工具,可以将 function fn(a, b) {} 编译为下面的结构。

    {

    "type": "Program",

    "body": [

    {

    "type": "FunctionDeclaration",

    "id": {

    "type": "Identifier",

    "name": "fn"

    },

    "params": [

    {

    "type": "Identifier",

    "name": "a"

    },

    {

    "type": "Identifier",

    "name": "b"

    }

    ],

    "body": {

    "type": "BlockStatement",

    "body": []

    },

    "generator": false,

    "expression": false,

    "async": false

    }

    ],

    "sourceType": "script"

    }

    将 JavaScript 语法编译成抽象语法树后,需要对它进行遍历、修该并重新编译,遍历树结构的过程为 “先序深度优先”。

    esprima、estraverse 和 escodegen

    esprima、estraverse 和 escodegen 模块是操作 AST 的三个重要模块,也是实现 babel 的核心依赖,下面是分别介绍三个模块的作用。

    1、esprima 将 JS 转换成 AST

    esprima 模块的用法如下:

    // 文件:esprima-test.js

    const esprima = require("esprima");

    let code = "function fn() {}";

    // 生成语法树

    let tree = esprima.parseScript(code);

    console.log(tree);

    // Script {

    // type: 'Program',

    // body:

    // [ FunctionDeclaration {

    // type: 'FunctionDeclaration',

    // id: [Identifier],

    // params: [],

    // body: [BlockStatement],

    // generator: false,

    // expression: false,

    // async: false } ],

    // sourceType: 'script' }

    通过上面的案例可以看出,通过 esprima 模块的 parseScript 方法将 JS 代码块转换成语法树,代码块需要转换成字符串,也可以通过 parseModule 方法转换一个模块。

    2、estraverse 遍历和修改 AST

    查看遍历过程:

    // 文件:estraverse-test.js

    const esprima = require("esprima");

    const estraverse = require("estraverse");

    let code = "function fn() {}";

    // 遍历语法树

    estraverse.traverse(esprima.parseScript(code), {

    enter(node) {

    console.log("enter", node.type);

    },

    leave() {

    console.log("leave", node.type);

    }

    });

    // enter Program

    // enter FunctionDeclaration

    // enter Identifier

    // leave Identifier

    // enter BlockStatement

    // leave BlockStatement

    // leave FunctionDeclaration

    // leave Program

    上面代码通过 estraverse 模块的 traverse 方法将 esprima 模块转换的 AST 进行了遍历,并打印了所有的 type 属性并打印,每含有一个 type 属性的对象被叫做一个节点,修改是获取对应的类型并修改该节点中的属性即可。

    其实深度遍历 AST 就是在遍历每一层的 type 属性,所以遍历会分为两个阶段,进入阶段和离开阶段,在 estraverse 的 traverse 方法中分别用参数指定的 entry 和 leave 两个函数监听,但是我们一般只使用 entry。

    3、escodegen 将 AST 转换成 JS

    下面的案例是一个段 JS 代码块被转换成 AST,并将遍历、修改后的 AST 重新转换成 JS 的全过程。

    // 文件:escodegen-test.js

    const esprima = require("esprima");

    const estraverse = require("estraverse");

    const escodegen = require("escodegen");

    let code = "function fn() {}";

    // 生成语法树

    let tree = esprima.parseScript(code);

    // 遍历语法树

    estraverse.traverse(tree, {

    enter(node) {

    // 修改函数名

    if (node.type === "FunctionDeclaration") {

    node.id.name = "ast";

    }

    }

    });

    // 编译语法树

    let result = escodegen.generate(tree);

    console.log(result);

    // function ast() {

    // }

    在遍历 AST 的过程中 params 值为数组,没有 type 属性。

    实现 Babel 语法转换插件

    实现语法转换插件需要借助 babel-core 和 babel-types 两个模块,其实这两个模块就是依赖 esprima、estraverse 和 escodegen 的。

    使用这两个模块需要安装,命令如下:

    npm install babel-core babel-types

    1、plugin-transform-arrow-functions

    plugin-transform-arrow-functions 是 Babel 家族成员之一,用于将箭头函数转换 ES5 语法的函数表达式。

    // 文件:plugin-transform-arrow-functions.js

    const babel = require("babel-core");

    const types = require("babel-types");

    // 箭头函数代码块

    let sumCode = `

    const sum = (a, b) => {

    return a + b;

    }`;

    let minusCode = `const minus = (a, b) => a - b;`;

    // 转化 ES5 插件

    let ArrowPlugin = {

    // 访问者(访问者模式)

    visitor: {

    // path 是树的路径

    ArrowFunctionExpression(path) {

    // 获取树节点

    let node = path.node;

    // 获取参数和函数体

    let params = node.params;

    let body = node.body;

    // 判断函数体是否是代码块,不是代码块则添加 return 和 {}

    if (!types.isBlockStatement(body)) {

    let returnStatement = types.returnStatement(body);

    body = types.blockStatement([returnStatement]);

    }

    // 生成一个函数表达式树结构

    let func = types.functionExpression(null, params, body, false, false);

    // 用新的树结构替换掉旧的树结构

    types.replaceWith(func);

    }

    }

    };

    // 生成转换后的代码块

    let sumResult = babel.transform(sumCode, {

    plugins: [ArrowPlugin]

    });

    let minusResult = babel.transform(minusCode, {

    plugins: [ArrowPlugin]

    });

    console.log(sumResult.code);

    console.log(minusResult.code);

    // let sum = function (a, b) {

    // return a + b;

    // };

    // let minus = function (a, b) {

    // return a - b;

    // };

    我们主要使用 babel-core 的 transform 方法将 AST 转化成代码块,第一个参数为转换前的代码块(字符串),第二个参数为配置项,其中 plugins 值为数组,存储修改 babal-core 转换的 AST 的插件(对象),使用 transform 方法将旧的 AST 处理成新的代码块后,返回值为一个对象,对象的 code 属性为转换后的代码块(字符串)。

    内部修改通过 babel-types 模块提供的方法实现,API 可以到 https://github.com/babel/babe... 中查看。

    ArrowPlugin 就是传入 transform 方法的插件,必须含有 visitor 属性(固定),值同为对象,用于存储修改语法树的方法,方法名要严格按照 API,对应的方法会修改 AST 对应的节点。

    在 types.functionExpression 方法中参数分别代表,函数名(匿名函数为 null)、函数参数(必填)、函数体(必填)、是否为 generator 函数(默认 false)、是否为 async 函数(默认 false),返回值为修改后的 AST,types.replaceWith 方法用于替换 AST,参数为新的 AST。

    2、plugin-transform-classes

    plugin-transform-classes 也是 Babel 家族中的成员之一,用于将 ES6 的 class 类转换成 ES5 的构造函数。

    // 文件:plugin-transform-classes.js

    const babel = require("babel-core");

    const types = require("babel-types");

    // 类

    let code = `

    class Person {

    constructor(name) {

    this.name = name;

    }

    getName () {

    return this.name;

    }

    }`;

    // 将类转化 ES5 构造函数插件

    let ClassPlugin = {

    visitor: {

    ClassDeclaration(path) {

    let node = path.node;

    let classList = node.body.body;

    // 将取到的类名转换成标识符 { type: 'Identifier', name: 'Person' }

    let className = types.identifier(node.id.name);

    let body = types.blockStatement([]);

    let func = types.functionDeclaration(className, [], body, false, false);

    path.replaceWith(func);

    // 用于存储多个原型方法

    let es5Func = [];

    // 获取 class 中的代码体

    classList.forEach((item, index) => {

    // 函数的代码体

    let body = classList[index].body;

    // 获取参数

    let params = item.params.length ? item.params.map(val => val.name) : [];

    // 转化参数为标识符

    params = types.identifier(params);

    // 判断是否是 constructor,如果构造函数那就生成新的函数替换

    if (item.kind === "constructor") {

    // 生成一个构造函数树结构

    func = types.functionDeclaration(className, [params], body, false, false);

    } else {

    // 其他情况是原型方法

    let proto = types.memberExpression(className, types.identifier("prototype"));

    // 左侧层层定义标识符 Person.prototype.getName

    let left = types.memberExpression(proto, types.identifier(item.key.name));

    // 右侧定义匿名函数

    let right = types.functionExpression(null, [params], body, false, false);

    // 将左侧和右侧进行合并并存入数组

    es5Func.push(types.assignmentExpression("=", left, right));

    }

    });

    // 如果没有原型方法,直接替换

    if (es5Func.length === 0) {

    path.replaceWith(func);

    } else {

    es5Func.push(func);

    // 替换 n 个节点

    path.replaceWithMultiple(es5Func);

    }

    }

    }

    };

    // 生成转换后的代码块

    result = babel.transform(code, {

    plugins: [ClassPlugin]

    });

    console.log(result.code);

    // Person.prototype.getName = function () {

    // return this.name;

    // }

    // function Person(name) {

    // this.name = name;

    // }

    上面这个插件的实现要比 plugin-transform-arrow-functions 复杂一些,归根结底还是将要互相转换的 ES6 和 ES5 语法树做对比,找到他们的不同,并使用 babel-types 提供的 API 对语法树对应的节点属性进行修改并替换语法树,值得注意的是 path.replaceWithMultiple 与 path.replaceWith 不同,参数为一个数组,数组支持多个语法树结构,可根据具体修改语法树的场景选择使用,也可根据不同情况使用不同的替换方法。

    总结

    通过本节我们了解了什么是 AST 抽象语法树、抽象语法树在 JavaScript 中的体现以及在 NodeJS 中用于生成、遍历和修改 AST 抽象语法树的核心依赖,并通过使用 babel-core 和 babel-types 两个模块简易模拟了 ES6 新特性转换为 ES5 语法的过程,希望可以为后面自己实现一些编译插件提供了思路。

    更多相关内容
  • react-ast使用react渲染抽象语法树。如果您发现它有用,请★此仓库★★★本质上很难使用抽象语法树。 这是一个React渲染器,它可以使用react与Abstrac react-ast交互来渲染抽象语法树。如果您发现它有用,请★此仓库...
  • 在计算机科学中,抽象语法树(abstract syntax tree或者缩写为AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。树上的每个节点都表示源代码中的一种结构。之...
  • ASTExtractor:Java语法的抽象语法树提取器 ASTExtractor是基于Eclipse编译器的Java源代码的抽象语法树(AST)提取器。 该工具充当Eclipse编译器的包装器,并允许以XML和JSON格式导出源代码文件或项目的AST。 该工具...
  • 自定义抽象语法树JSON模板
  • compiler:抽象语法树

    2021-05-30 19:41:59
    一个表达式解析器、词法分析器,最终是一个解释器,它实现了一个抽象语法树,用于以整数文字或变量标识符作为其操作数的二进制表达式的链式变量赋值。 当前,当遍历 AST 时,所有变量标识符都将返回 0。 表达式从...
  • 本文首先讨论了传统软件测试方法的缺点和局限性,给出了软件的故障模型,进而提出了基于抽象语法树的静态分析技术,并给出了故障自动检测算法。依据该算法开发了自动化测试工具,给出了实验结果和对比分析,并指出了...
  • 抽象语法树

    千次阅读 2021-04-24 21:08:54
    一、什么是AST抽象语法树 在传统的编译语言的流程中,程序的一段源代码在执行之前会经历三个步骤,统称为"编译": 分词/词法分析 这个过程会将由字符组成的字符串分解成有意义的代码块,这些代码块统称为词法单元...

    AST

    一、什么是AST抽象语法树

    在传统的编译语言的流程中,程序的一段源代码在执行之前会经历三个步骤,统称为"编译":

    • 分词/词法分析

    这个过程会将由字符组成的字符串分解成有意义的代码块,这些代码块统称为词法单元(token).

    举个例子: let a = 1, 这段程序通常会被分解成为下面这些词法单元: let 、a、=、1 ,空格是否被当成词法单元,取决于空格在这门语言中的意义。

    • 解析/语法分析

    这个过程是将词法单元流转换成一个由元素嵌套所组成的代表了程序语法结构的树,这个树被称为"抽象语法树"(abstract syntax code,AST)

    • 代码生成

    将AST转换成可执行代码的过程被称为代码生成.

    抽象语法树(abstract syntax code,AST)是源代码的抽象语法结构的树状表示,树上的每个节点都表示源代码中的一种结构,之所以说是抽象的,抽象表示把js代码进行了结构化的转化,转化为一种数据结构。这种数据结构其实就是一个大的json对象,json我们都熟悉,他就像一颗枝繁叶茂的树。有树根,有树干,有树枝,有树叶,无论多小多大,都是一棵完整的树。

    简单理解,就是把我们写的代码按照一定的规则转换成一种树形结构。

    二、AST的用途

    AST的作用不仅仅是用来在JavaScript引擎的编译上,我们在实际的开发过程中也是经常使用的,比如我们常用的babel插件将 ES6转化成ES5、使用 UglifyJS来压缩代码 、css预处理器、开发WebPack插件、Vue、React的编译等等,这些底层原理都是基于AST来实现的,AST能力十分强大, 能够帮助开发者理解JavaScript这门语言的精髓。

    三、AST的结构

    我们先来看一组简单的AST树状结构:

     

    const name = 'gzb'

    经过转化,输出如下AST树状结构:

     

    {

    "type": "Program",

    "body": [

    {

    "type": "VariableDeclaration",

    "declarations": [

    {

    "type": "VariableDeclarator",

    "id": {

    "type": "Identifier",

    "name": "name"

    },

    "init": {

    "type": "Literal",

    "value": "gzb",

    "raw": "'gzb'"

    }

    }

    ],

    "kind": "const"

    }

    ],

    "sourceType": "script"

    }

    我们可以看到,一个标准的AST结构可以理解为一个json对象,那我们就可以通过一些方法去解析和操作它,大家可以去在线转换工具体验:https://astexplorer.net/ 或者 https://esprima.org/demo/parse.html#

    四、AST编译过程

    常见的AST parser

    • 早期有uglifyjs和esprima
    • Espree,基于esprima,用于eslint
    • Acorn,号称是相对于esprima性能更优,体积更小
    • Babylon,出自acorn,用于babel
    • Babel-eslint,babel团队维护,用于配合使用ESLint

    AST编译流程图:

     

    我们可以看到,AST工具会源代码经过四个阶段的转换:

    1.词法分析scanner

     

    const name = 'gzb'

    假如有以上代码,在词法分析阶段,会先对整个代码进行扫描,生成tokens流,扫描过程如下:

    • 会通过条件判断语句判断这个字符是 字母, "/" , "数字" , 空格 , "(" , ")" , ";" 等等。
    • 如果是字母会继续往下看如果还是字母或者数字,会继续这一过程直到不是为止,这个时候发现找到的这个字符串是一个 "const", 是一个Keyword,并且下一个字符是一个 "空格", 就会生成{ "type" : "Keyword" , "value" : "const" }放入数组中。
    • 它继续向下找发现了一个字母 'name'(因为找到的上一个值是 "var" 这个时候如果它发现下一个字符不是字母可能直接就会报错返回)并且后面是空格,生成{ "type" : "Identifier" , "value" : "name" }放到数组中。
    • 发现了一个 "=", 生成了{ "type" : "Punctuator" , "value" : "=" }放到了数组中。
    • 发现了'gzb',生成了{ "type" : "String" , "value" : "gzb" }放到了数组中。

    解析如下:

     

    2.parser生成AST树

    这里我们使用esprima去生成, 安装相关依赖 npm i esprima --save

    以如下代码为例:

     

    const name = 'gzb'

    要得到其对应的AST,我们对其进行如下操作:

     

    const esprima = require('esprima');

    let code = "const name = 'gzb'";

    const ast = esprima.parseScript(code);

    console.log(ast);

    运行结果如下:

     

    $ node test.js

    Script {

    type: 'Program',

    body: [

    VariableDeclaration {

    type: 'VariableDeclaration',

    declarations: [Array],

    kind: 'const'

    }

    ],

    sourceType: 'script'

    }

    这样我们就得到了一棵AST树

    3.traverse对AST树遍历,进行增删改查

    这里我们使用estraverse去完成, 安装相关依赖 npm i estraverse --save

    还是上面的代码, 我们更改为 const name = 'guozebin'

     

    const esprima = require('esprima');

    const estraverse = require('estraverse');

    let code = "const name = 'gzb'";

    const ast = esprima.parseScript(code);

    estraverse.traverse(ast, {

    enter: function (node) {

    node.value = "guozebin";

    }

    });

    console.log(ast);

    运行结果如下:

     

    $ node test.js

    Script {

    type: 'Program',

    body: [

    VariableDeclaration {

    type: 'VariableDeclaration',

    declarations: [Array],

    kind: 'const',

    name: 'name',

    value: 'guozebin'

    }

    ],

    sourceType: 'script',

    name: 'name',

    value: 'guozebin'

    }

    这样一来,我们就完成了对AST的遍历更新。

    4.generator将更新后的AST转化成代码

    这里我们使用escodegen去生成, 安装相关依赖 npm i escodegen --save

    整体代码结构如下:

     

    const esprima = require('esprima');

    const estraverse = require('estraverse');

    const escodegen = require('escodegen');

    let code = "const name = 'gzb'";

    const ast = esprima.parseScript(code);

    estraverse.traverse(ast, {

    enter: function (node) {

    node.value = "guozebin";

    }

    });

    const transformCode = escodegen.generate(ast);

    console.log(transformCode);

    会得到如下结果:

     

    $ node test.js

    const name = 'guozebin';

    五、babel原理浅析

    Babel插件就是作用于抽象语法树。

    Babel 的三个主要处理步骤分别是: 解析(parse),转换(transform),生成(generate)。

    • 解析

    将代码解析成抽象语法树(AST),每个js引擎(比如Chrome浏览器中的V8引擎)都有自己的AST解析器,而Babel是通过Babylon(https://github.com/babel/babylon)实现的。解析过程有两个阶段:词法分析语法分析,词法分析阶段把字符串形式的代码转换为令牌(tokens)流,令牌类似于AST中节点;而语法分析阶段则会把一个令牌流转换成 AST的形式,同时这个阶段会把令牌中的信息转换成AST的表述结构。

    • 转换

    转换步骤接收 AST 并对其进行遍历,在此过程中对节点进行添加、更新及移除等操作。 Babel通过babel-traverse对其进行深度优先遍历,维护AST树的整体状态,并且可完成对其的替换,删除或者增加节点,这个方法的参数为原始AST和自定义的转换规则,返回结果为转换后的AST。

    • 生成

    代码生成步骤把最终(经过一系列转换之后)的 AST 转换成字符串形式的代码,同时还会创建源码映射(source maps)(https://www.html5rocks.com/en/tutorials/developertools/sourcemaps/)。.

    代码生成其实很简单:深度优先遍历整个 AST,然后构建可以表示转换后代码的字符串。

    Babel通过babel-generator再转换成js代码,过程就是深度优先遍历整个AST,然后构建可以表示转换后代码的字符串。

    babel插件开发

    如何编写一个babel插件可以查看https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/plugin-handbook.md

     

    const {

    parse

    } = require("@babel/parser");

    const traverse = require("@babel/traverse").default;

    const t = require("@babel/types");

    const generator = require("@babel/generator").default;

    const jscode = `const s = 1

    console.log(s)`

    let ast = parse(jscode);

    const visitor =

    {

    'CallExpression' (path) {

    const callee = path.get('callee');

    if (

    callee.node.type === 'MemberExpression' &&

    (callee.node.object).name === 'console'

    ) {

    path.remove()

    }

    }

    }

    traverse(ast, visitor);

    let {

    code

    } = generator(ast);

    console.log(code)

    六、vue中AST抽象语法树的运用;

    vue中AST主要运用在模板编译过程.

    我们先来看看vue模板编译的整体流程图:

     

    vue中的模板编译主要分为三个步骤:

    1. 解析器阶段: 将 template 里面的代码解析成AST抽象语法树;
    2. 优化器阶段: 将AST抽象语法树静态标签打上tag,防止重复渲染(优化了diff算法);
    3. 代码生成器阶段: 优化后的AST抽象语法树通过generate函数生成render函数字符串;

    我们来看看vue源码的整体实现过程:

     

    export const createCompiler = createCompilerCreator(function baseCompile (

    template: string,

    options: CompilerOptions

    ): CompiledResult {

    //生成ast的过程

    const ast = parse(template.trim(), options)

    //优化ast的过程,给ast抽象语法树静态标签打上tag,防止重复渲染

    if (options.optimize !== false) {

    optimize(ast, options)

    }

    //通过generate函数生成render函数字符串

    const code = generate(ast, options)

    return {

    ast,

    render: code.render,

    staticRenderFns: code.staticRenderFns

    }

    })

    解析器要实现的功能就是将模板解析成AST,我们这里主要来分析一下代码解析阶段,这里主要运用的是parse()这个函数,解析器内部也分为好几个解析器,比如HTML解析器文本解析器以及过滤解析器,其中最主要的就是HTML解析器。HTML解析器的作用就是解析HTML,它在解析HTML的过程中会不断触发各种钩子函数,我们来看看代码实现:

     

    parseHTML(template, {

    //解析开始标签

    start (tag, attrs, unary, start, end) {

    },

    //解析结束标签

    end (tag, start, end) {

    },

    //解析文本

    chars (text: string, start: number, end: number) {

    },

    //解析注释

    comment (text: string, start, end){

    }

    })

    举个例子:

     

    <div>111111</div>

    当上面这个模板被HTML解析器解析时,所触发的钩子函数依次是: startcharsend

    所以HTML解析器在实现上是一个函数,它有两个参数----模板和选项,我们的模板是一小段一小段去截取与解析的,所以需要不断循环截取,我们来看看vue内部实现原理:

     

    function parseHTML (html, options) {

    while (html) {

    //判断父元素为正常标签的元素的逻辑

    if (!lastTag || !isPlainTextElement(lastTag)) {

    //vue中要判断是 文本、注释、条件注释、DOCTYPE、结束、开始标签

    //除了文本标签, 其他的都是以 < 开头, 所以区分处理

    var textEnd = html.indexOf('<');

    if (textEnd === 0) {

    //注释的处理逻辑

    if (comment.test(html)) {}

    //条件注释的处理逻辑

    if (conditionalComment.test(html)) {}

    //doctype的处理逻辑

    var doctypeMatch = html.match(doctype);

    if (doctypeMatch) {}

    //结束标签的处理逻辑

    var endTagMatch = html.match(endTag);

    if (endTagMatch) {}

    //开始标签的处理逻辑

    var startTagMatch = parseStartTag();

    if (startTagMatch) {}

    }

    var text = (void 0), rest = (void 0), next = (void 0);

    //解析文本

    if (textEnd >= 0) {}

    // "<" 字符在当前 html 字符串中不存在

    if (textEnd < 0) {

    text = html

    html = ''

    }

    // 如果存在 text 文本

    // 调用 options.chars 回调,传入 text 文本

    if (options.chars && text) {

    // 字符相关回调

    options.chars(text)

    }

    }else{

    // 父元素为script、style、textarea的处理逻辑

    }

    }

    }

    以上就是vue解析器生成AST语法树的主流程了,代码细节的地方还需要自己去解读源码,源码位置:\vue\packages\vue-template-compiler\build.js

    七、react源码解析-jsx转换为ast树及渲染挂载

    jsx转换ast树

    通过@babel/plugin-transform-react-jsx将jsx语法的代码转换为React.createElement()的函数调用

    https://www.babeljs.cn/repl 可以测试

     

    const a=<h1 style={{"color":"red"}}>123<div>test</div></h1>

    经过编译之后

     

    "use strict";

    var a = React.createElement("h1", {

    style: {

    "color": "red"

    }

    }, "123", React.createElement("div", null, "test"));

    React.createElement()是如何转换为ast树的?

     

    ReactDOM.render(

    React.createElement(

    'div',

    {className:'red'},

    'Click Me'

    ),

    document.getElementById("root")

    )

    React.createElement将上面代码转化成ast树:

     

    {

    $$typeof: Symbol(react.element)

    key: null

    props: {className: "red", children: "Click Me"}

    ref: null

    type: "div"

    _owner: null

    _store: {validated: false}

    _self: null

    _source: null

    }

    createElement()函数接收参数,并将参数读取转换为ast树的一些所需参数字段,目前我们的数据是比较简洁的,关注点在于props和type就好了

     

    function createElement(type, config, children) {

    //根据上面的示例代码,type=div,config= {className:'red'},children='Click Me'

    var propName; // Reserved names are extracted

    debugger;

    var props = {};// 我们常用的props 目前组件

    var key = null;//该组件的唯一key

    var ref = null;// 我们的ref

    var self = null;

    var source = null;

    if (config != null) {

    if (hasValidRef(config)) {

    ref = config.ref;

    }

    if (hasValidKey(config)) {

    key = '' + config.key;

    }

    self = config.__self === undefined ? null : config.__self;

    source = config.__source === undefined ? null : config.__source; // Remaining properties are added to a new props object

    for (propName in config) {

    if (hasOwnProperty$1.call(config, propName) &amp;&amp; !RESERVED_PROPS.hasOwnProperty(propName)) {

    props[propName] = config[propName];

    }

    }

    } // Children can be more than one argument, and those are transferred onto

    // the newly allocated props object.

    // 当发现arguments的参数大于1的时候。说明是有多个兄弟子元素的,如果等于1的话说明只有一个元素

    var childrenLength = arguments.length - 2;

    if (childrenLength === 1) {

    // 直接将props的children设为当前children

    props.children = children;

    } else if (childrenLength > 1) {

    var childArray = Array(childrenLength);

    for (var i = 0; i &lt; childrenLength; i++) {

    childArray[i] = arguments[i + 2];

    }

    {

    if (Object.freeze) {

    Object.freeze(childArray);

    }

    }

    // 有多个兄弟元素的话,将兄弟节点放置在一个数组里面,赋值给props的children

    props.children = childArray;

    } // Resolve default props

    if (type && type.defaultProps) {

    var defaultProps = type.defaultProps;

    for (propName in defaultProps) {

    if (props[propName] === undefined) {

    props[propName] = defaultProps[propName];

    }

    }

    }

    {

    if (key || ref) {

    var displayName = typeof type === 'function' ? type.displayName || type.name || 'Unknown' : type;

    if (key) {

    defineKeyPropWarningGetter(props, displayName);

    }

    if (ref) {

    defineRefPropWarningGetter(props, displayName);

    }

    }

    }

    // ReactElement 返回回来的是我们最终的ast树的结构

    return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props);

    }

    渲染挂载

    ReactDOM.render方法主要具备两个功能:

    1. 将ast树转换为fiber 结构。填入许多调度、更新、diff相关数据,并转换ast树为dom树
    2. ast树转换为dom树,先是为我们准备好fiber版本的节点关系结构,然后从下往上的渲染一个一个的元素,最后将所有的节点添加到最外面的那一层dom上面,并赋值给最外层的fiber.stateNode,fiber.stateNode即将插入页面的dom元素。

    八、V8引擎中的AST

    js代码能在cpu上运行,主要是js引擎的功劳,V8引擎是google开发,应用在chrome浏览器和nodejs上,是一个经典的js引擎。下图可以看出,在V8引擎中,js从源代码到机器码的转译主要有三个步骤:Parser(AST) ->Ignition(Bytecode)->TurboFan(Machine Code)

    • Parser:负责将JavaScript源码转换为Abstract Syntax Tree (AST)
    • Ignition:interpreter,即解释器,负责将AST转换为Bytecode,解释执行Bytecode;同时收集TurboFan优化编译所需的信息,比如函数参数的类型
    • TurboFan:compiler,即编译器,利用Ignitio所收集的类型信息,将Bytecode转换为优化的汇编代码

    Parser-AST解析器

    • 大家应该很熟悉了,就是我们今天介绍的AST解析器

    Ignition-解释器

    • 把AST解析成一种类汇编语言,样子和汇编语言很相似,叫做Bytecode。这种语言和CPU无关,不同cpu的机器上生成的Bytecode都是相同的。

    TurboFan-编译器

    • 每种cpu的架构和指令集是不同的,对应的汇编语言会有差异。V8在这一步,针对不同的cpu,把Bytecode解析成适合不同cpu的汇编语言。V8可以支持十几种cpu的汇编语言。

     

    九、AST在日常工作中的应用

    我们在工作中,经常能遇到批量替换的问题,一般我们可能都会使用正则替换的方法,但正则替换有很多局限性,比如我们要清除代码中的console,如果有同事写成下面这样,那我们就匹配不到了

     

    console

    .log('aaa')

    这种情况下,如果面对更复杂的需求或者严谨的场景,要么我们编写更复杂的正则表达式,要么我们就不得不去硬肝 AST 操作了。

    上面已经举例了直接使用AST来清除,现在已经有了很多工具来帮助我们操作AST,如jscodeshift

     

    export default (fileInfo, api) => {

    const j = api.jscodeshift;

    const root = j(fileInfo.source)

    const callExpressions = root.find(j.CallExpression, {

    callee: {

    type: 'MemberExpression',

    object: { type: 'Identifier', name: 'console' },

    },

    }

    );

    callExpressions.remove();

    return root.toSource();

    };

    还有gogocode,这个使用起来更加方便

     

    const $ = require('gogocode')

    /** 刻意凌乱的代码 **/

    const input = `

    console

    .log(`a, b,c`);

    `

    // 关键代码

    const output = $(input).replace('console.log()', '').generate()

    console.log(output)

    一个更复杂的例子,突然有一天,为了统一代码里的各种枚举,我们需要把 text 属性更名为 name,把 value 属性更名为 id,这个用正则很难精确匹配容易误伤,操作AST树还有些麻烦,用 GoGoCode 只需要这么替换一下就行了:

     

    const list = [

    {

    text: "A策略",

    value: 1,

    tips: "Atip",

    },

    {

    text: "B策略",

    value: 2,

    tips: "Btip",

    },

    {

    text: "C策略",

    value: 3,

    tips: "Ctip",

    },

    ];

     

    const $ = require('gogocode')

    const input = `

    const list = [

    {

    text: "A策略",

    value: 1,

    tips: "Atip",

    },

    {

    text: "B策略",

    value: 2,

    tips: "Btip",

    },

    {

    text: "C策略",

    value: 3,

    tips: "Ctip",

    },

    ];

    // ts的类型标记,这种正则替换会被错误替换的,在 gogocode 里就不会

    const text: string = ''

    // 这一段因为没有 value 就不会被选择器匹配到,也不会被错误替换

    const cfg = {

    text: ''

    }

    `

    const output = $(input2).replace(

    '{ text: $_$1, value: $_$2, $$$ }',

    '{ name: $_$1, id: $_$2, $$$ }'

    ).generate();

    展开全文
  • 本文分析了PHP7新特性之抽象语法树(AST)带来的变化。分享给大家供大家参考,具体如下: 这里大部分内容参照 AST 的 RFC 文档而成:https://wiki.php.net/rfc/abstractsyntaxtree,为了易于理解从源文档中节选部分...
  • 生成命令的抽象语法树(AST)。 根据Bash引用规则的子集标记给定命令 检测格式错误的命令 , 为什么 主要是这样写的: 作为编写基于严格语法规则的解析器的练习 作为一个从头开始构建Bash外壳的大型项目的一部分 ...
  • 于Java程序的抽象语法树类(Abstract syntaxtree classes for working with Java programs)法录表(St
  • 有一个让我非常喜欢Windows PowerShell ISE的理由,就是它将它的基础脚本对象模型暴露给用户,这样就允许用户按照自己的方式和需要去自定义脚本体验。 自定义ISE的核心是$psISE对象。$psISE对象允许用户去控制ISE...
  • 用于探索由编译器生成的抽象语法树的。 安装 您可以通过pip安装最新版本: $ pip install py-solc-ast 或者克隆 repo 并使用setuptools : $ python setup.py install 用法 首先,使用将您的合约编译为。 >> >...
  • 一个用于处理抽象语法树的库。 目录 背景 抽象语法树是表示源代码的一种方式。 对于此库,它以格式表示。 例如,以下源代码: const answer = 42 具有以下表示形式: { " type " : " Program " , " body " : ...
  • php-ast 此扩展公开了PHP 7生成的抽象语法树。 这是版本1.0.x的文档。 另请参阅。目录安装Windows :下载并将其移动到PHP安装的ext/目录中。 此外,将extension=php_ast.dll添加到您的php.ini文件中。 Unix(PECL) ...
  • Android代码-可扩展Android库,用于将文本解析为抽象语法树并将这些树渲染为富文本。.zip,SimpleAST-master,gradlew.bat,gradlew,gradle.properties,simpleast-core,proguard-rules.pro,src,test,java,com,discord,...
  • hast:超文本抽象语法树格式
  • 一种基于抽象语法树的C#源代码SQL注入漏洞检测算法 应用安全
  • AST抽象语法树

    2022-02-06 12:14:43
    AST抽象语法树

    抽象语法树是什么

    抽象语法树本质上是一个JS对象,模板语法直接编译成正常的HTML语法是非常困难的,这就要借助抽象语法树过渡,让编译工作变得简单

    抽象语法树就服务于模板编译,只要涉及到翻译,一种语法翻译成另一种语法,就要借助抽象语法树

    手写实现AST抽象语法树

    定义两个栈和指针来辅助我们来实现AST抽象语法树

    • 检测到开始标记,将标记tag放入栈1,将ast语法形式的对象{'tag':tag,'children':[]}放入栈2

    • 抓取结束标记之前的文字,文字不能全为空,改变栈2的栈顶内容为抓取到的文字

    • 检测到结束标记,如果tag和栈1 的顶部内容相同,栈1出栈,栈2出栈,让栈2的children加入栈2出栈的内容

    // parse函数,主函数
    export default function parse(templateString){
      // 指针
      var index = 0;
      // 剩余字符串
      var rest = ''
      // 开始标记
      var startRegExp = /^\<([a-z]+[0-6]?)\>/;
      // 结束标记
      var endRegExp = /^\<\/([a-z]+[0-6]?)\>/
      // 抓取结束标记之前的文字
      var wordRegExp = /^([^\<]+)\<\/[a-z]+[0-6]?\>/;
      // 两个栈
      var stack1 = [];
      // 到最后stack2出栈,输出结果会为空,可以补一项
      var stack2 = [{'children':[]}];
      while(index < templateString.length-1){
        rest = templateString.substring(index);
        if(startRegExp.test(rest)){
          let tag = rest.match(startRegExp)[1]
          //将开始标记存入栈1中
          stack1.push(tag);
          // 将对象存入栈2中
          stack2.push({'tag':tag,'children':[]})
          // 指针移动标签的长度加2,因为<>占两位
          index += tag.length +2;
          // console.log('开始标记',tag)
        }else if(endRegExp.test(rest)){
          let tag = rest.match(endRegExp)[1];
          //此时,tag一定是和栈1顶部的内容相同
          let pop_tag = stack1.pop();
          if(tag === pop_tag){
            let pop_arr = stack2.pop();
            if(stack2.length > 0 ){
              stack2[stack2.length - 1].children.push(pop_arr);
            }
          }else{
            throw new Error(pop_tag + '标签没有封闭')
          }
          // console.log('结束标记' +tag)
          // 指针移动标签的长度加2,因为</>占3位
          index += tag.length +3;
        }else if(wordRegExp.test(rest)){
          // 识别是不是文字,不能全为空
          let word = rest.match(wordRegExp)[1];
          if(!/^\s+$/.test(word)){
            // 文字不全是空
            // console.log('检查到文字'+word)
            // 改变此时stack2栈顶元素中
            stack2[stack2.length-1].children.push({"text":word,"type":3})
          }
          index += word.length;
          // console.log(stack1,JSON.stringify(stack2))
        }else{
          index++;
        }
        
      }
      // 之前默认放置一项了,就要去第0项输出
      return stack2[0].children[0]
    }
    

    测试

    import parse from './parse'
    var templateString = `
      <div>
        <h3>你好</h3>
        <ul>
          <li>A</li>
          <li>B</li>
          <li>C</li>
        </ul>
      </div>
    `
    
    const ast  = parse(templateString)
    console.log(ast)

    输出结果

     接下来识别attrs,当在div放入class时,上面的代码是识别不了的,但是标签和class之间肯定有一个空格,我们就要在开始标记上识别空格

    var startRegExp = /^\<([a-z]+[0-6]?)(\s[^\<]+)?\>/;

     指针需要移动attrsString的位数,并在对象中加入attrs属性

     

     接下来定义parseAtrrsString函数将attrs处理成数组,并在attrs属性中调用函数

    stack2.push({ 'tag': tag, 'children': [] ,'attrs':parseAtrrsString(attrsString)})

    // 让attrsString变成数组返回
    export default function (attrsString) {
      if (attrsString === undefined) return []
      // 当前是否在引号内
      var isYinhao = false;
      // 断点
      var point = 0;
      // 结果数组
      var result = [];
      // 遍历attrsString,而不是用split(),因为用split(' ')会导致class="aa bb cc"这种情况出现错误
      for (let i = 0; i < attrsString.length; i++) {
        let char = attrsString[i];
        if (char === '"') {
          isYinhao = !isYinhao;
          console.log(isYinhao)
        } else if (char === ' ' && !isYinhao) {
          // 遇见了空格并且不再引号中
          // 不全是空格才push进结果数组
          if (!/^\s*$/.test(attrsString.substring(point, i))) {
            // 去掉头尾空格
            result.push(attrsString.substring(point, i).trim());
            point = i;
          }
          
        }
      }
      // 循环结束之后还剩一个属性
      result.push(attrsString.substring(point).trim())
      // 返回 name=name,value=value这种形式,需要映射
      result = result.map(item =>{
        //根据等号拆分
        const o = item.match(/^(.+)=(.+)$/);
        return {
          name:o[1],
          value:o[2]
        }
      })
      return result
    }

     输出结果

    展开全文
  • 快速React 使用react渲染抽象语法树如果您认为它有用,请★此回购★★★★ 本质上,抽象语法树很难使用。 这是一个React渲染器,使与抽象语法树进行交互并轻松渲染代码。 React AST是使用React渲染抽象语法树的最终...
  • 在计算机科学中,抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。上面这是...

    在计算机科学中,抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。

    上面这是百度百科的定义,一如既往的让人摸不着头脑。

    我们总结一下百度百科的定义:

    抽象语法树是一颗树

    树的每个节点都表示了源代码的一种 语法结构

    上面这段总结有几个关键词:抽象语法树、树、节点、源代码、语法结构,其中语法结构是比较难理解的,那么什么是语法结构呢?

    举几个简单的例子:

    //变量声明

    var a = 1;

    复制代码

    //循环

    while(true){

    console.log(1);

    }

    复制代码

    //判断

    if(true){

    console.log(1);

    }

    复制代码

    //函数声明

    function a(){

    console.log(1);

    }

    复制代码

    以上这些例子是js的语法声明(statement),这种声明就可以看成js的语法结构! 也就是说抽象语法树的每个节点都在描述这种结构。

    比如说一个节点是变量声明,那么这个节点的子节点都会去描述变量声明的具体内容:变量名是什么,变量是什么类型,变量的初始值是什么等等。

    就是这样一个一个的声明,构成了抽象语法树。

    代码执行的三个步骤

    从js程序到机器可执行的机器码需要经历两个阶段:

    语法检查

    编译运行

    语法检查又分为语法分析和词法分析,所以分成三个步骤就是:

    词法分析

    语法分析

    编译运行

    这里先简单介绍下每个阶段都干了什么活:

    第一步:词法分析,也叫做扫描scanner。它读取我们的代码,然后把它们按照预定的规则合并成一个个的标识Tokens(type 和 value )。这个阶段,它会移除空白符,注释等。最后,整个代码将被分割进一个Tokens列表(一个一维数组)。

    第二步:语法分析,也叫做解析器。它会将词法分析出来的Token数组转化成树形的表达形式。同时,验证语法,语法如果有错的话,抛出语法错误。

    第三步:编译阶段,也叫编译器。这个阶段会处理AST,生成机器可执行的机械码。

    词法分析

    先以一个简单的例子看下token序列长什么样

    var a = 1;

    复制代码

    他的token长这样

    b60414592cf6c8806907ab9dae5909e8.png

    其实就是一个一维数组,里面有一些对象用于描述每个单词。

    我整理了下常见的type:

    Keyword (关键词)

    Identifier (标识符)

    Punctuator (标点符号)

    Numberic(数字)

    String (字符串)

    Boolean(布尔)

    Null(空值)

    语法分析

    词法分析由源代码生成了Token序列,语法分析则是由Token序列生成了抽象语法树。

    还是看上一个例子:

    var a = 1;

    复制代码

    他的抽象语法树长这样:

    2b987781b83b5cb903cc1dff8f99274a.png

    1、先看最外层的三个属性

    type(表示是一段程序代码)

    body(代码的具体内容)

    sourceType(表示语言的种类)

    2、再看body里面的具体内容,body是一个数组,这是因为程序可能有多个内容块(statement),每个内容块用一个对象表示。

    3、再看每个内容块的内容

    type(表示这个内容块是干什么的)

    declarations(乘装变量内容的块,可以看到这个块也是一个数组,因为变量声明可能生命多个,所以一个生命对应一个对象 例如 var a=1,b=2;) kind(关键字)

    4、再看declarations里面对象里面的内容

    type (声明的类型是个变量)

    id(表示变量名)

    init(表示为这个变量设置的初值)

    上面提到statement,statement有很多类型,比如说变量声明,函数定义,if语句,while循环,等都是一个statement,大家如果想看更多的类型,点击这里。

    一个超级简单的例子

    好了,说了这么多,终于要写代码了。

    这个例子实现的功能:

    将源代码中的 == 变成 ===

    将源代码中的 var 变成 let

    将源代码中的 console注释掉

    这个例子用到的工具:

    Esprima (将源代码转化为ast)

    Estraverse(遍历语法树)

    Escodegen(讲语法书反编译为js代码)

    初始化一个项目

    npm init

    复制代码

    安装用到的依赖包

    npm install esprima estraverse escodegen --save

    复制代码

    新建index.js入口文件 和 originCode.js 源代码文件

    在 originCode.js 中输入要转换的源代码

    function fun() {

    var opt = 1;

    console.log(1);

    if (opt == 1) {

    console.log(2);

    }

    }

    复制代码

    在 index.js 中实现我们的功能

    let fs = require('fs');

    const esprima = require('esprima');//将JS代码转化为语法树模块

    const estraverse = require('estraverse');//JS语法树遍历各节点

    const escodegen = require('escodegen');//将JS语法树反编译成js代码模块

    /**

    * 由源代码得到抽象语法树

    */

    function getAst(jsFile) {

    let jsCode;

    return new Promise((resolve)=>{

    fs.readFile(jsFile, (error, data) => {

    jsCode = data.toString();

    resolve(esprima.parseScript(jsCode));

    });

    });

    }

    /**

    * 设置全等

    */

    function setEqual(node) {

    if (node.operator === '==') {

    node.operator = '===';

    }

    }

    /**

    * 删除console

    */

    function delConsole(node) {

    if (node.type === 'ExpressionStatement' && node.expression.type === 'CallExpression' && node.expression.callee.object.name==='console') {

    node.expression.callee.object.name = '//console';

    }

    }

    /**

    * 把var变成let

    */

    function setLet(node){

    if(node.kind === 'var'){

    node.kind = 'let';

    }

    }

    /**

    * 遍历语法树

    */

    function travel(ast){

    estraverse.traverse(ast, {

    enter: (node) => {

    setEqual(node);

    setLet(node);

    delConsole(node);

    }

    });

    }

    /**

    * 生成文件

    */

    function writeCode(file,data) {

    fs.writeFile(file,data,(error)=>{

    console.log(error);

    });

    }

    /**

    * 入口函数

    */

    function main(){

    let file = './originCode.js';

    let distFile = './distCode.js';

    getAst(file).then(function(jsCode) {

    travel(jsCode);

    // 删掉 console , 通过parseScript在生成一变ast去掉注释的内容

    // let distCode = escodegen.generate( esprima.parseScript( escodegen.generate(jsCode)));

    // 注释 console

    let distCode = escodegen.generate(jsCode);

    console.log('distcode',distCode);

    writeCode(distFile,distCode);

    });

    }

    main();

    复制代码

    然后运行我们的项目

    node index.js

    复制代码

    distCode.js的内容已经变成我们想要的了

    function fun() {

    let opt = 1;

    //console.log(1);

    if (opt === 1) {

    //console.log(2);

    }

    }

    复制代码

    展开全文
  • treehugger.js treehugger.js是用于程序处理的Javascript库。 它具有表示和操纵(分析,转换)通用方法。 它包括三个部分: 受启发的通用ASTs表示格式,可用于表示以任何语言(Java,Ruby,Javascript)编写的程序...
  • DelphiAST, Delphi抽象语法树生成器 Delphi的抽象语法树生成器使用 DelphiAST,你可以使用真正的Delphi代码并获得抽象语法树。 一个单位,但没有符号表。FreePascal和and兼容。示例输入unit Unit1;interfac
  • 【你应该了解的】抽象语法树AST

    千次阅读 2021-04-17 11:07:36
    本文介绍的抽象语法树,就是他们用到的技术,是不是应该了解一下呢?本文没有晦涩难懂的理论,也没有大段大段的代码,完全从零开始,小白阅读也无任何障碍。通过本文的阅读,您将会了解AST的基本原理以及使用方法。...
  • python抽象语法树Abstract Syntax Tree is a very strong features in Python. Python AST module allows us to interact with Python code itself and modify it. 抽象语法树是Python中非常强大的功能。 Python AST...
  • 抽象语法树什么是抽象语法树?It is a hierarchical program representation that presents source code structure according to the grammar of a programming language, each AST node corresponds to an item of ...
  • 抽象语法树(AST)

    2021-03-01 08:03:29
    抽象语法树(AST)最近在做一个类JAVA语言的编译器,整个开发过程,用抽象语法树(Abstract SyntaxTree,AST)作为程序的一种中间表示,所以首先就要学会建立相对应源代码的AST和访问AST。Eclipse AST是Eclipse JDT的一个...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 73,012
精华内容 29,204
关键字:

抽象语法树