如果你查看目前任何主流的项目中的 devDependencies,我们不会在生产环境用到,但是它们在开发过程中充当着重要的角色。归纳一下有:javascript转译、代码压缩、css预处理器、elint、pretiier,postcss等。所有的上述工具,不管怎样,都建立在了AST这个巨人的肩膀上,都是 AST 的运用:
同时,在业务使用 AST 可以解决一些通过常规方式处理很繁琐的问题。如通过 AST 解决识别 slot 插槽名称、$emit 事件名称、解析模板字符串并进行替换等关键逻辑问题(这些都已在我们的项目中落地)。
抽象语法树(Abstract Syntax Tree,AST)是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
AST 可以将代码转换成 JSON 语法树,基于语法树可以进行代码转换、替换等很多操作,其实AST应用非常广泛,我们开发当中使用的 less/sass、eslint、TypeScript 等很多插件都是基于 AST 实现的。
通过利用 AST 技术,不仅仅是上述的功能,在现在开发模式中,也诞生了各种各样的工具和框架和插件,很多底层多少都能看到 AST 的影子,比方说之前比较流行的 vue 转小程序,就是通过将 vue 的语法树,解析成小程序的语法树,然后在小程序上运行的。当然这样的例子还有很多...
Babel 能够转译 ECMAScript 2015+ 的代码,使它在旧的浏览器或者环境中也能够运行。可以尝试一下: https://babel.docschina.org/repl。
Babel 是一个编译器,大多数编译器的工作过程可以分为三部分:
结合上述编译过程,找到对应的 Babel 插件:
@babel/core
:用来解析 AST 以及将 AST 生成代码
解析 Parse(@babel/parser
) ==> 转换 Transform(@babel/traverse
、@babel/types
) ==> 生成 Generate(@babel/generator
)@babel/types
:构建新的 AST 节点示例:
function add (a, b) {
return a + b
}
const { parse } = require('@babel/core')
let ast = parse(`
function add (a, b) {
return a + b
}`)
javascript 对象结构:
{
"type": "Program",
"body": [{
"type": "FunctionDeclaration"
}]
}
AST树层级关系及 types 标识「astexplorer可查看」:
- FunctionDeclaration:
- id:
- Identifier:
- name: add
- params [2]
- Identifier
- name: a
- Identifier
- name: a
- body:
- BlockStatement
- body [1]
- ReturnStatement
- argument
- BinaryExpression
- operator: +
- left
- Identifier
- name: a
- right
- Identifier
- name: b
Visitors 是跨语言的 AST 遍历中使用的一种模式。简而言之,它们是一个对象,接收特定节点类型的方法。
const MyVisitor = {
// 树中的每个 Identifier 调用 Identifier() 方法
Identifier() {
console.log("Called!");
}
}
Note:
Identifier() { ... }
is shorthand forIdentifier: { enter() { ... } }
.
traverse(ast, {
...MyVisitor
})
上述 Identifier()
会被调用 5 次!
- FunctionDeclaration
- Identifier (id)
- Identifier (params[0])
- Identifier (params[1])
- BlockStatement (body)
- ReturnStatement (body)
- BinaryExpression (argument)
- Identifier (left)
- Identifier (right)
遍历树的每个分支,直到该分支结束;然后达到下一个节点。
通过上述可知,有两次访问节点的机会。
const MyVisitor = {
Identifier: {
enter() {
console.log("Entered!");
},
exit() {
console.log("Exited!");
}
}
}
Paths 是两个节点 Node 之间链接的对象表示。
{
"parent": {...},
"node": {...},
"hub": {...},
"contexts": [],
"data": {},
"shouldSkip": false,
"shouldStop": false,
"removed": false,
"state": null,
"opts": null,
"skipKeys": null,
"parentPath": null,
"context": null,
"container": null,
"listKey": null,
"inList": false,
"parentKey": null,
"key": null,
"scope": null,
"type": null,
"typeAnnotation": null
}
同时,也包含 添加,更新,移动、删除
节点相关的方法。
将 Javascript 代码转换为 AST
方法 | 说明 |
---|---|
parse(code, {sourceType: 'module|script', plugins: ['jsx']}) | https://babeljs.io/docs/en/babel-parser |
遍历 AST 树,并负责替换,删除和添加节点。
方法 | 说明 |
---|---|
traverse(ast, {XDeclaration (path) {}}) | https://babeljs.io/docs/en/babel-traverse |
构建,验证和转换AST节点的方法。https://babeljs.io/docs/en/babel-traverse
方法 | 说明 |
---|---|
builder: ["operator", "left", "right"] | t.binaryExpression("*", t.identifier("a"), t.identifier("b")); |
t.isBinaryExpression(maybeBinaryExpressionNode) | 验证 |
AST 生成 Javascript 代码
方法 | 说明 |
---|---|
generate(ast, { /* options */ }, code) | https://babeljs.io/docs/en/babel-generator |
path.get("declaration.body.body.0")
path.isReferencedIdentifier()
path.findParent((path) => path.isObjectExpression())
path.find((path) => path.isObjectExpression())
path.getFunctionParent()
path.getSibling(0)
path.skip()
path.replaceWith(t.binaryExpression("**", path.node.left, t.numberLiteral(2)))
path.replaceWithMultiple([])
path.replaceWithSourceString()
path.insertBefore/insertAfter()
path.unshiftContainer/pushContainer()
path.remove()
javascript 词法作用域。当创建引用时,无论是 variable, function, class, param, import, label 等,其都属于当前 scope。
// global scope
function scopeOne() {
// scope 1
function scopeTwo() {
// scope 2
}
}
function scopeOne() {
var one = "I am in the scope created by `scopeOne()`";
var two = "I am in the scope created by `scopeOne()`";
function scopeTwo() {
// 使用 higher scope 的变量 one
one = "I am updating the reference in `scopeOne` inside `scopeTwo`";
// 创建相同名称的变量 two
var two = "I am creating a new `two` but leaving reference in `scopeOne()` alone.";
}
}
相关 API:
path.scope.hasBinding("n")
或 path.scope.hasOwnBinding("n")
path.scope.rename("n", "x")
path.scope.parent.push({ id, init: path.node })
创建或修改节点时,可通过 https://www.babeljs.cn/docs/babel-types 进行查看相关方法!!!
代码地址:https://github.com/381510688/practice/blob/master/ast/code.js
async function test() {
let res = await Promise.resolve(123);
console.log(res);
}
const { transformFromAstSync, types: t } = require("@babel/core")
// let res = await Promise.reslove('123')
const resVar = t.VariableDeclaration('let', [
t.VariableDeclarator(
t.Identifier('res'),
t.AwaitExpression(t.CallExpression(
t.MemberExpression(t.Identifier('Promise'), t.Identifier('resolve')),
[t.numericLiteral(123)]
))
)
])
// console.log(res)
const consoleExpress = t.ExpressionStatement(t.CallExpression(
t.MemberExpression(t.Identifier('console'), t.Identifier('log')),
[t.Identifier('res')]
))
let ast = t.functionDeclaration(
t.Identifier('test'),
[],
t.BlockStatement([
resVar,
consoleExpress
]),
false,
true
)
let code = transformFromAstSync(t.program([ast]), null, {}).code
console.log(code)
在 name 为 ”o“ 的 Identifier 类型下,查找 StringLiteral 类型!
babel.traverse(ast, {
enter (path) {
if (path.isIdentifier({ name: 'o' })) {
path.parentPath.traverse({
StringLiteral (path) {
cosnt svgHtml = path.node.value
}
})
}
}
}
代码地址:https://github.com/381510688/practice/blob/master/ast/try-wrapper.js
const fs = require('fs')
const { transformFromAstSync, parse, traverse, types: t } = require('@babel/core')
const { SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION } = require('constants')
let code = fs.readFileSync('./1.js', { encoding: 'utf-8' })
let ast = parse(code)
// 判断async函数
const isAsyncFuncNode = node => {
return t.isFunctionDeclaration(node, { async: true })
|| t.isArrowFunctionExpression(node, { async: true })
|| t.isFunctionExpression(node, { async: true })
|| t.isObjectMethod(node, { async: true })
}
traverse(ast, {
AwaitExpression(path) {
// 递归向上找异步函数的 node 节点
while (path && path.node) {
let parentPath = path.parentPath
// 已经包含 try 语句则直接退出
if (t.isBlockStatement(path.node) && t.isTryStatement(parentPath.node)) {
return
}
// 确认 async function
if (t.isBlockStatement(path.node) && isAsyncFuncNode(parentPath.node)) {
// 创建 tryStatement https://www.babeljs.cn/docs/babel-types#trystatement
let tryCatchAst = t.tryStatement(
path.node,
t.catchClause(t.Identifier('e'), t.BlockStatement(parse(`console.error(e)`).program.body)),
null
)
path.replaceWithMultiple([tryCatchAst])
return
}
path = parentPath // 递归
}
}
})
let newCode = transformFromAstSync(ast, null, {}).code
console.log(newCode)
更加完整示例参考:https://github.com/yeyan1996/async-catch-loader/blob/master/src/index.js
FunctionDeclaration
, FunctionExpression
, ArrowFunctionExpression
, ObjectMethod
and ClassMethod
别名
const MyVisitor = { Function (path) {} }console.error('e')
。
// 简便写法 t.BlockStatement(parse(`console.error(e)`).program.body) // 传统写法 t.BlockStatement([ t.ExpressionStatement( t.CallExpression( t.MemberExpression(t.Identifier('console'), t.Identifier('error')), [t.Identifier('e')] ) ) ])对于了解 javascript 语言的小伙伴,肯定都可以确认的给出答案:解释型语言!!!
提出这个问题主要源于两点:
名称 | 说明 |
---|---|
解释型语言 | 程序不需要编译,程序在运行时才翻译成机器语言,每执行一次都要翻译一次(一行行地边解释边执行) |
编译型语言 | 程序在执行之前需要把程序编译成为机器语言的文件,运行时不需要重新翻译,直接使用编译的结果就行了 |
解释器:
编译器:
在函数作用域内的任何变量的声明都会被提升到顶部并且值为
undeinfed
。
上述是如何做到的?解释了两次?还是先编译后运行?
foo
=
10
这样的原子符号(atomic token)。undefined
。所以,变量提升不过是执行上下文的小把戏
整体来说,为了解决解释器的低效问题,后来的浏览器把编译器也引入进来,形成混合模式。最终,结合了解释器和编译器的两者优点。
基本思想: 在 JavaScript 引擎中增加一个监视器(也叫分析器)。监视器监控着代码的运行情况,记录代码一共运行了多少次、如何运行的等信息。如果同一行代码运行了几次,这个代码段就被标记成了 “warm”,如果运行了很多次,则被标记成 “hot”。
JIT 会增加很多多余的开销:
通过消除开销使得性能上有进一步地提升,这也是 WebAssembly 所要做的事之一。
Parser | Supported Languages | Github |
---|---|---|
acorn | esnext & JSX (using acorn-jsx) | https://github.com/acornjs/acorn |
Traversing | Github |
---|---|
recast | https://github.com/benjamn/recast |
AST 并不是一门技术,更多的是偏向于编程的思想,理解原理以及思路,可以提供更多实现的可能。其他语言同样存在类似的 Parser,如下:
语言 | Parser |
---|---|
javascript | acorn |
vue | @vue/compiler-core |
css | cssom |
json | jsonToAst |
等等,可以通过 https://astexplorer.net/ 来选择不同的语言。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。