AST 基础学习以及躲坑技巧

AST 的全称是抽象语法树,名字也很抽象,给个容易理解的例子。

上面这个例子,是一个简单的常量定义。

当浏览器不支持 const 这种语法的时候,我们需要把他换成支持的 var,这个时候,AST 就上场了。

这里面,每一个包含 type 的层次结构,都叫一个节点(Node)。

这里我们关注的 const 就在一个 VariableDeclaration 的节点上面,开始位置为 0。

一个个 Node 节点,组成了一份描述我们代码的树状结构,也就是 AST。

Babel 的处理步骤

解析(parse) > 转换(transform) > 生成(generate)

解析

解析是指接收代码,并输出 AST。

这其中又包含词法分析(Lexical Analysis)和语法分析(Syntactic Analysis)。

词法分析和语法分析在这不展开,有很多库帮我们直接拿到代码的 AST,比如 acorn 和 babylon。

转换

转换就是对 AST 进行遍历,并在过程中对所需的节点(Node)进行修改操作。像上面案例中的 constvar 就是这个阶段进行的。

生成

把修改后的 AST,变成字符串形式的代码,这里还可以顺便做一下 source maps。

如何进行最复杂的转换?

1、我们要对 AST 进行深度优先的遍历,遍历每一个节点。

2、在 AST 领域,有一个叫访问者模式(visitor)的概念,用 visitor 来访问每个节点和里面的属性。

在遍历的过程,我们有进入和离开两次访问节点的机会,就像入栈出栈一样。

3、当 visitor 来访问每个节点的时候,仅有的节点信息和属性信息,不够我们做出任何决策。我们需要知道更多的信息,例如当前节点和其他节点的关系,而这种关系,就用路径(Paths)来描述。在 Babel 的 visitor 里面,拿到的参数就是路径。

到这里为止,我们就可以对我们想修改的代码,生成代码的 AST,然后遍历,使用 visitor 进行修改。

转换过程中的坑

1、状态(State)

我们想转换某个函数里面的某个变量,结果直接在 Identifier 里面转换,导致把全部的变量都给转换掉了。

正确的做法是在 FunctionDeclaration 的访问者里面通过递归来做 Identifier 转换。

2、作用域(Scopes)

除了上面通过递归方式,来减少错误的变量转换外,我们的变量还有可能是在外层函数做的定义,visitor 拿到的外层函数中的一个引用,此时贸然修改,会导致意外发生。

结语

到这里为止,AST 的基本概念都科普完了,对 AST 可以做些什么也心里有数了。

随着技术的进步和环境的复杂化,未来的 polyfill 集合一定会越来越庞大。不论是静态补丁(@babel/polyfill 或 @babel/plugin-transform-runtime)还是动态补丁(polyfill.io)都将产生大量的冗余代码。

而未来的 polyfill 应该是

1、在编译阶段就获得代码中用到的 ES5(这个下限应该要可以根据时间或者 .browserslistrc 的信息进行调整)以上的 API 集合。

2、在浏览器运行的时候,对 API 做特征检测,获得实际浏览器所需的 API 子集合。

3、向类似 polyfill.io 这种动态服务请求这个子集合的 polyfill。

其实 polyfill.io 在 2014 年就有这方面的讨论了,但我觉得脱离了第一步而直接做第二步的实施特征检测,依旧会得到一个超大集合的代码,并且是随着技术进步而越来越大。

参考

大部分内容都是从 babel-handbook 中学习的。

https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/plugin-handbook.md