技术学习:AST 前言 前段时间在开发CMS更新模态框的需求,有做到将md文件解析“tokens”再将“tokens”转换为json的步骤,组内交流说业界有md转json的工具 想着自己原来只是简单了解过AST
,而AST
在我们前端开发中运用的还比较多,于是去系统学习一下(然后就有了这篇文章)
概念 AST
是Abstract Syntax Tree(抽象语法树)
的简称抽象: 源代码语法结构的一种抽象语法树: 树状形式表现(树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构)
可以说AST
是前端工程化绕不过的一个名词,为什么呢?
1 2 3 Most people don't really have to think about compilers in their day jobs. However, compilers are all around you, tons of the tools you use are based on concepts borrowed from compilers.
涉及到工程化诸多环节的应用 ,比如:
Typescript => Javascript (typescript)
SASS/LESS => CSS (sass/less)
ES6+ => ES5 (babel)
Javascript 代码进行格式化 (eslint/prettier)
识别 React 项目中的 JSX (不是js原生的写法)
Vue SFC(单文件组件single file component)
js uglify
Tree Shaking(通过 AST 将用不到的函数进行移除,从而减小打包体积)
Vue SFC
js uglify
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 1. 代码精简function sum (first, second ) { return first + second; } function sum (first,second ) {return first+second}function s (x,y ) {return x+y}2. 合并声明const a = 3 ;const b = 4 ;const a = 3 , b = 4 ;3. 布尔简化!b && !c && !d && !e !(b||c||d||e) 4. 预计算const ONE_YEAR = 365 * 24 * 60 * 60 const ONE_YAAR = 31536000 function hello ( ) { console .log('hello, world' ) } hello() console .log('hello, world' )
在语言转换的过程中,实质上就是对其 AST 的操作
Code -> AST(Parse)解析
AST -> AST(Transform)转换另外一种语言的AST
如将ts转化为ts的ast,再将ts的ast转化为js的ast
AST -> Code(Generate)生成代码
以下是一段代码和它转换的AST
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 const a = 4 { "type" : "Program" , "start" : 0 , "end" : 11 , "body" : [ { "type" : "VariableDeclaration" , "start" : 0 , "end" : 11 , "declarations" : [ { "type" : "VariableDeclarator" , "start" : 6 , "end" : 11 , "id" : { "type" : "Identifier" , "start" : 6 , "end" : 7 , "name" : "a" }, "init" : { "type" : "Literal" , "start" : 10 , "end" : 11 , "value" : 4 , "raw" : "4" } } ], "kind" : "const" } ], "sourceType" : "module" }
不同的语言拥有不同的解析器: 比如 Javascript
(Babel)的解析器和 CSS
的解析器(postcss)就完全不同 对相同的语言,也存在诸多的解析器,也就会生成多种 AST: 如babel
与 espree
AstExplorer
AST生成 AST的生成是个复杂度极高过程,AST 的生成这一步骤被称为解析(Parser)
可以理解为把一门编程语言转成另一门编程语言的过程,一般是指高级语言到低级语言
解析步骤分为两个阶段:
词法分析 (Lexical Analysis)
语法分析 (Syntactic Analysis)
词法分析 词法 :词法组成语言的单词, 是语言中最小单元词法分析 :将代码转化为 Token
流,维护一个关于 Token
的数组
可以理解成代码中一系列独立的单词,var,for ,if,while等 词法分析的过程就是读取代码,识别每一个单词及其种类,将它们按照预定的规则合并成一个个的标识,也叫 token 同时,它会移除空白符,注释等,最终产出一个token数组。 即词法分析阶段把会字符串形式的代码转换为 令牌(tokens) 流
类比于开发CMS更新模态框时将md文档内容拆出一块块小块
1 2 3 4 5 6 7 8 9 10 11 const a = 10 ;[ { type : "KEYWORD_CONST" , value : "const" }, { type : "VARIABLE" , value : "a" }, { type : "OPERATOR_EQUAL" , value : "=" }, { type : "INTEGER" , value : "10" } ... ]
应用 :
代码检查,如 eslint
。例如:判断是否以分号结尾,则判断是否含有分号的 token
(AST相同)
语法高亮,如 highlight.js / prism.js
使代码高亮
模板语法,如 ejs
等模板,参考hexo 中使用到的ejs
语法分析 语法 :描述逻辑的格式(源程序是按照一定的格式组织的描述逻辑的文本)语法分析 :根据词法,将 Token
流转化为结构化的 AST
(树形的数据结构),方便操作 类比于CMS更新模态框需求将一个个小块转化为json
源码学习 ❌ 看babel
的源码了解编译器工作原理,很多人都会望而却步。
✅ babel
的维护者之一 James Kyle 有一个最小编译器的开源项目 the-super-tiny-compiler
,截止目前超过21.5k stars。项目去掉注释大约200行代码,代码虽少,但足以展现编译器的很多要点,通过学习这个项目,可以对编译原理有一个较系统的理解
the-super-tiny-compiler 链接:the-super-tiny-compiler
all written in jsLisp
语言风格函数调用 => 转换成C
语言风格(不包含所有语法) 比如假设我们有add
和subtract
两个函数,两种语言的风格如下:
Lisp风格
C风格
2 + 2
(add 2 2)
add(2, 2)
4 - 2
(subtract 4 2)
subtract(4, 2)
2 + (4 - 2)
(add 2 (subtract 4 2))
add(2, subtract(4, 2))
回顾:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function compiler (input ) { let tokens = tokenizer(input); let ast = parser(tokens); let newAst = transformer(ast); let output = codeGenerator(newAst); return output; }
1 2 3 4 5 6 7 8 9 10 11 [ { 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 : ')' }, ]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 function tokenizer (input ) { let current = 0 ; let tokens = []; while (current < input.length) { let char = input[current]; if (char === '(' ) { tokens.push({ type: 'paren' , value: '(' , }); current++; continue ; } if (char === ')' ) { tokens.push({ type: 'paren' , value: ')' , }); 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 ; } if (char === '"' ) { let value = '' ; char = input[++current]; while (char !== '"' ) { value += char; char = input[++current]; } char = input[++current]; tokens.push({ type : 'string' , 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 ('I dont know what this character is: ' + char); } return tokens; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 { 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' , }] }] }] }
step2 tokens => parser => ast 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 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 === 'string' ) { current++; return { type: 'StringLiteral' , value: 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; }
访问者模式 定义: 你可以只修改 Visitor
本身完成新操作的定义,而不需要修改原本对象 类似于polyfill
实现 在使用的时候以目标浏览器版本作为访问者,实现对应访问者的功能
转换需要深度优先遍历AST:
Program 类型 - 从 AST 的根节点开始 CallExpression (add) - 进入 Program 节点 body 属性的第一个子元素 NumberLiteral (2) - 进入 CallExpression (add) 节点 params 属性的第一个子元素 CallExpression (subtract) - 进入 CallExpression (add) 节点 params 属性的第二个子元素 NumberLiteral (4) - 进入 CallExpression (subtract) 节点 params 属性的第一个子元素 NumberLiteral (2) - 进入 CallExpression (subtract) 节点 params 属性的第二个子元素
可以使用访问者模式:
创建一个类似下面的“访问者”对象,以提供访问各种数据类型的方法
1 2 3 4 var visitor = { NumberLiteral() {}, CallExpression() {}, }
当遍历AST
的时,一旦匹配到特定类型的节点,就调用访问者提供的方法。同时为了保证访问者能够拿到当前节点信息,我们需要将当前节点和父节点传入(有点类似react fiber
结构中断续传的实现)
1 2 3 4 var visitor = { NumberLiteral(node, parent) {}, CallExpression(node, parent) {}, }
存在进入叶子节点,退出(exit)分支的行为。即对树深度遍历时,每个节点会存在两种操作,一种是enter/exit
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 - Program - CallExpression - NumberLiteral - CallExpression - NumberLiteral - NumberLiteral -> Program (enter) -> CallExpression (enter) -> Number Literal (enter) <- Number Literal (exit) -> Call Expression (enter) -> Number Literal (enter) <- Number Literal (exit) -> Number Literal (enter) <- Number Literal (exit) <- CallExpression (exit) <- CallExpression (exit) <- Program (exit)
改造数据结构为
1 2 3 4 5 6 7 8 9 10 const visitor = { NumberLiteral: { enter(node, parent) {}, exit(node, parent) {}, }, CallExpression: { enter(node, parent) {}, exit(node, parent) {}, }, }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 function traverser (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' : case 'StringLiteral' : break ; default : throw new TypeError (node.type); } if (methods && methods.exit) { methods.exit(node, parent); } } traverseNode(ast, null ); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 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, }); }, }, StringLiteral: { enter(node, parent) { parent._context.push({ type: 'StringLiteral' , 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; }
step4 newAst => generator => output 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 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; case 'StringLiteral' : return '"' + node.value + '"' ; default : throw new TypeError (node.type); } }
end
Babel 再来简单了解一下babel
:
@babel/parser
@babel/traverse和@babel/types
@babel/generate
利用上面所学的知识,实现对var => let的转换
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 const parser = require ('@babel/parser' );const traverse = require ('@babel/traverse' );const generator = require ('@babel/generator' );const code = `const a = 1 var b = 2 let c = 3 var d = 4` ;const transToLet = code => { const ast = parser.parse(code); const visitor = { VariableDeclaration(path) { if (path.node.type === 'VariableDeclaration' ) { if (path.node.kind === 'var' ) { path.node.kind = 'let' ; } } }, }; traverse.default(ast, visitor); const newCode = generator.default(ast, {}, code).code; return newCode; }; console .log(transToLet(code))
总结
了解AST
概念及运用范围
对于大多数编译器的架构和原理有了系统的认知
对于编译器涉及到的访问者模式值得借鉴
利用the-super-tiny-compiler
所学简单使用babel转换