AST 的全称是抽象语法树,名字也很抽象,给个容易理解的例子。
JavaScript
const blog = 'mxgw.info';
1 | const blog = 'mxgw.info'; |
---|
上面这个例子,是一个简单的常量定义。
当浏览器不支持 const
这种语法的时候,我们需要把他换成支持的 var
,这个时候,AST 就上场了。
这里面,每一个包含 type
的层次结构,都叫一个节点(Node)。
这里我们关注的 const
就在一个 VariableDeclaration
的节点上面,开始位置为 0。
一个个 Node 节点,组成了一份描述我们代码的树状结构,也就是 AST。
解析(parse) > 转换(transform) > 生成(generate)
解析是指接收代码,并输出 AST。
这其中又包含词法分析(Lexical Analysis)和语法分析(Syntactic Analysis)。
词法分析和语法分析在这不展开,有很多库帮我们直接拿到代码的 AST,比如 acorn 和 babylon。
转换就是对 AST 进行遍历,并在过程中对所需的节点(Node)进行修改操作。像上面案例中的 const
转 var
就是这个阶段进行的。
把修改后的 AST,变成字符串形式的代码,这里还可以顺便做一下 source maps。
1、我们要对 AST 进行深度优先的遍历,遍历每一个节点。
2、在 AST 领域,有一个叫访问者模式(visitor)的概念,用 visitor
来访问每个节点和里面的属性。
JavaScript
const MyVisitor = { VariableDeclaration: { enter() { console.log("进入函数声明节点"); }, exit() { console.log("离开函数声明节点"); } } };
12345678910 | const MyVisitor = { VariableDeclaration: { enter() { console.log("进入函数声明节点"); }, exit() { console.log("离开函数声明节点"); } }}; |
---|
在遍历的过程,我们有进入和离开两次访问节点的机会,就像入栈出栈一样。
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