AST、Babel、依赖
Babel
Babel 的原理
Babel 转换 JS 代码可以分成以下三个大步骤:
parse
: 把代码 code 变成 ASTtraverse
: 遍历 AST 进行修改generate
: 把 AST 变成代码 code2
即,code --(1)-> ast --(2)-> ast2 --(3)-> code2
一个简单的示例:手动把 let 变成 var
let_to_var.ts
将 code 中的 let 全部变成 var
1 | import { parse } from "@babel/parser" |
执行命令 node -r ts-node/register let_to_var.ts
,结果如下:
如果你想用 Chrome 查看 AST,可以添加 --inspect-brk
选项,node -r ts-node/register --inspect-brk let_to_var.ts
。
为什么必须要用 AST
为什么必须要用 AST 来做一层转换,直接将 code 字符串中的 let 替换为 var 不就行了吗?
- 你很难用正则表达式来替换,正则很容易把
let a = 'let'
变成var a = 'var'
- 你需要识别每个单词的意思,才能做到只修改用于声明变量的 let
- 而 AST 能明确地告诉你每个 let 的意思
自动把代码转为 ES5
上面的例子中,我们是手动的把 code 中的 let
转换为了 var
,如何将这一操作自动化呢?
使用 @babel/core
和 @babel/preset-env
即可
1 | // to_es5.ts |
运行结果如下:
@babel/preset-env
内置了很多转换规则,比如把所有 ES6+ 代码转换为 ES5 代码的规则
代码不应该是字符串,而是应该放到文件中
上面的例子都是以字符串的形式,而在我们平时的开发中,代码都是分散在一个个的文件中的,此时就需要我们引入 Node 的文件模块。
在根目录下创建 test.js 文件
1 | // test.js |
创建 file_to_es5.ts 文件
1 | // file_to_es5.ts |
执行 node -r ts-node/register file_to_es5.ts
,就会得到 test.es5.js
文件
1 | // test.es5.js |
如何把 ./test.js 改为任意文件?
Babel 除了转换 JS 语法,还能做啥?
还可以用来分析 JS 文件的依赖关系(即答)
创建一个 project_1 目录,project_1 目录中有三个 JS 文件
1 | // project_1/a.js |
1 | // deps_1.ts |
运行结果如下:
deps_1.ts
的思路
- 调用
collectCodeAndDeps('index.js')
,代码中会有更多细节 - 先把
depRelation['index.js']
初始化为{ deps: [], code: 'index.js的源码' }
- 然后把
index.js
源码 code 变成 AST - 遍历 AST,看看 import 了哪些依赖,假设依赖了 a.js 和 b.js
- 把 a.js 和 b.js 写到
depRelation['index.js'].deps
里 - 最终得到的 depRelation 就收集了 index.js 的依赖
启发:用哈希表来存储文件依赖;哈希表是数据结构中的术语,在 JS 中一个对象就可以看作一个哈希表;这是计数排序的基本操作
升级:依赖的依赖
如何处理更深层的嵌套关系?
三层依赖关系
- index -> a -> dir/a2 -> dir/dir_in_dir/a3
- index -> b -> dir/b2 -> dir/dir_in_dir/b3
- 文件我已经创建好了,放在 project_2 目录里了
思路
- collectCodeAndDeps 太长了,缩写为 collect
- 调用 collect(‘index.js’)
- 发现依赖 ‘./a.js’ 于是调用 collect(‘a.js’)
- 发现依赖 ‘./dir/a2.js’ 于是调用 collect(‘dir/a2.js’)
- 发现依赖 ‘./dir_in_dir/a3.js’ 于是调用 collect(‘dir/dir_in_dir/a3.js’)
- 没有更多依赖了,a.js 这条线结束,发现下一个依赖 ‘./b.js’
- 以此类推,其实就是递归
给 collect 加递归
用递归来获取嵌套依赖
递归存在 call stack 溢出的风险,比如嵌套层数超过 20000 时,程序直接报错
再复杂一点:循环依赖
1 | // project_3/a.js |
依赖关系(project_3)
- index -> a -> b
- index -> b -> a
求值
- a.value = b.value + 1
- b.value = a.value + 1
运行代码
- node -r ts-node/register deps_3.ts
- 报错:调用栈 溢出了
- 为什么:分析过程 a -> b -> a -> b -> a -> b -> … 把调用栈撑满了
不能循环依赖吗?
并不,我们需要一些小技巧
避免重复进入同一个文件
思路:
- 一旦发现这个 key 已经在 keys 里了,就 return
- 这样分析过程就不是 a -> b -> a -> b -> … 而是 a -> b -> return
- 注意我们只需要分析依赖,不需要执行代码,所以这样是可行的
- 由于我们的分析不需要执行代码,所以叫做静态分析
- 但如果我们执行代码,就会发现还是出现了循环
执行 index.js
- 发现报错:不能在 ‘a’ 初始化之前访问 a
- 原因:执行过程 a -> b -> a 此处报错,因为 node 发现计算 a 的时候又要计算 a
结论
模块间可以循环依赖
- a 依赖 b,b 依赖 a
- a 依赖 b,b 依赖 c,c 依赖 a
但不能有逻辑漏洞
- a.value = b.value + 1
- b.value = a.value + 1
那能不能写出一个没有逻辑漏洞的循环依赖?当然可以。
合法的循环依赖(没有逻辑漏洞)
1 | // project_5/a.js |
有的循环依赖有问题,有的循环依赖没有问题;所以最好别用循环依赖,以防万一
总结
AST 相关
- parse: 把代码 code 变成 AST
- traverse: 遍历 AST 进行修改
- generate: 把 AST 变成代码 code2
工具
- babel 可以把高级代码翻译为 ES5
- @babel/parser
- @babel/traverse
- @babel/generator
- @babel/core 包含前三者
- @babel/preset-env 内置很多规则
代码技巧
- 使用哈希表来存储数据
- 通过检测 key 来避免重复
循环依赖
- 有的循环依赖可以正常执行
- 有的循环依赖不可以
- 但都可以做静态分析