源码parse成AST之后,需要进行AST的遍历和增删改(transform
)。那么transform的流程是什么样的?
babel会递归遍历AST,遍历过程中处理到不同的AST
会调用不同的visitor
函数来实现transform
。
visitor模式
visitor模式是23种经典设计模式中的访问者模式。visitor模式的思想是:当被操作的对象结构比较稳定,而操作对象的逻辑经常变化的时候,通过分离逻辑和对象结构,使得他们能够独立拓展。
如图,Element和Visitor分别代表对象结构和操作逻辑,两者可以独立拓展,在Client里面来组合两者,使用visitor操作element。这就是visitor模式。
对应到babel traverse
的实现,就是AST和visitor分离,在traverse(遍历)AST的时候调用注册的visitor来对其进行处理。 这样的AST是独立的扩展的,visitor是独立的扩展的,两者可以各自独立扩展还能结合在一起。
path和scope
path是记录遍历路径的api,它记录了父子节点的引用,还有很多增删改插AST的api: 那path大概与那些属性和方法呢?
path
path大概有下面属性方法,后面用到的时候知道是什么就可以了:
js
{
// 属性
node, // 当前AST节点
parent, // 父AST节点
parentPath, // 父AST节点的path
scope, // 作用域
hub, // 可以通过path.hub.file拿到最外层File对象,path.hub.getScope拿到最外层作用域,path.hub.getCode拿到源码字符串
container, // 当前AST节点所在的父节点属性的属性值
key, // 当前AST节点所在父节点属性的属性名或所在数组的下表
listkey, // 当前AST节点所在父节点属性的属性值为数组时listkey为该属性名,否则为undefined
// 方法
get(key), // 获取某个属性的path
set(key, node), // 设置某个属性的值
inList(), // 判断节点是否在数组中,如果container为数组,也就是有listkey的时候,返回true
getSibling(), // 获取某个下标的兄弟节点
getNextSibling(), // 获取下一个兄弟节点
getPrevSibling(), // 获取上一个兄弟节点
getAllPrevSiblings(), // 获取之前所有的兄弟节点
getAllNextSiblings(), // 获取之后所有的兄弟节点
isXxx(opts), // 判断当前节点是否是某个类型,可以传入属性和属性值进一步判断,比如path.isIdentifier({name: 'a'})
assertXxx(opts), // 同isXXX,但是不返回布尔值,而是抛出异常
find(callback),
findParent(callback),
insertBefore(nodes), // 在当前节点之前插入节点,可以是单个节点或者节点数组
insertAfter(nodes), // 在当前节点之后插入节点,可以是单个节点或者节点数组
replaceWith(replacement), // 用某个节点替换当前节点
replaceWithMultiple(nodes), // 用多个节点替换当前节点
replaceWithSourceString(replacement), // 解析源码成AST,然后替换当前节点
remove(), // 删除当前节点
traverse(ast, visitor), // 遍历当前节点的子节点,传入 visitor 和 state(state 是不同节点间传递数据的方式)
skip(), // 跳过当前节点的子节点的遍历
stop(), // 结束所有遍历
}
container 、key 、listkey 这几个属性不常用,简单介绍下:
因为AST节点要挂在父AST节点的某个属性上,那个属性的属性值就是这个AST节点的container
。
比如CallExpression有callee和argument属性,那么对于callee的AST节点来说,callee的属性值就是他的container,而callee就是它的key。
因为不是一个列表,所以listkey是undefined。
BlockStatement有body属性,是一个数组,对于数组中的每一个AST来说,这个数组就是它们的container,而listkey是body,key则是下标。
作用域 path.scope
scope是作用域信息,javascript中能生成作用域的就是模块、函数、块等,而且作用域之间会形成嵌套关系,也就是作用域链。babel在遍历的过程中会生成作用域链保存在path.scope中。
属性和方法大概有这些:
js
path.scope = {
bindings, // 当前作用域内声明的所有变量
block, // 当前作用域的block,详见上下文
parent, // 父级作用域
parentBlock, // 父级作用域的block
path, // 生成作用域的节点对应的path
references, // 所有的binding的引用对应的path,详见上下文
dump(), // 打印作用域链的所有binding到控制台
getAllBindings(), // 从当前作用域到根作用域的所有binding的合并
getBinding(name), // 查找某个binding,从当前作用域一直查到根作用域
hasBinding(name), // 从当前作用域查找 binding,可以指定是否算上全局变量,默认是 false
getOwnBinding(name), // 从当前作用域查找 binding
parentHasBinding(name), // 查找某个 binding,从父作用域查到根作用域,不包括当前作用域。可以通过 noGlobals 参数指定是否算上全局变量(比如console,不需要声明就可用),默认是 false
removeBinding(name), // 删除某个 binding
moveBindingTo(name, scope), // 把当前作用域中的某个 binding 移动到其他作用域
generateUid(name) // 生成作用域内唯一的名字,根据 name 添加下划线,比如 name 为 a,会尝试生成 _a,如果被占用就会生成 __a,直到生成没有被使用的名字
}
scope.block
能形成scope的有这些节点,这些节点也叫block节点。
js
export type Scopable =
| BlockStatement
| CatchClause
| DoWhileStatement
| ForInStatement
| ForStatement
| FunctionDeclaration
| FunctionExpression
| Program
| ObjectMethod
| SwitchStatement
| WhileStatement
| ArrowFunctionExpression
| ClassExpression
| ClassDeclaration
| ForOfStatement
| ClassMethod
| ClassPrivateMethod
| StaticBlock
| TSModuleBlock;
我们可以通过path.scope.block来拿到所在的块对应的节点,通过path.scope.parentBlock拿到父作用域对应的块节点。
一般情况下,我们不需要拿到生成作用域的块节点,只需要通过path.scope拿到作用域的信息,通过path.scope.parent拿到父作用域的信息。
scope.bindings 、scope.references
作用域中保存的是声明的变量和对应的值,每一个声明都叫做一个binding
。
比如这样的一段代码:
js
const a = 1;
他的path.scope.bindings是这样的:
js
bindings: {
a: {
constant: true,
constantViolations: [],
identifier: {type: 'Identifier', ...}
kind:'const',
path: {node,...}
referenced: false
referencePaths: [],
references: 0,
scope: ...
}
}
因为我们在当前scope中声明了a这个变量,所以bingdings中有a的binding,每一个binding都有kind,代表绑定的类型:
- var、let、const分别代表var、let、const形式声明的变量
- param代表参数的声明
- module代表import的变量的声明
binding有多种kind,代表变量是用不同的方式声明的。
binding.idetifier和binding.path,分别代表标识符、整个声明语句。
声明之后的变量会被引用和修改,binding.referenced 代表声明的变量是否被引用,binding.constant 代表变量是否被修改过。如果被引用了,就可以通过 binding.referencePaths 拿到所有引用的语句的 path。如果被修改了,可以通过 binding.constViolations 拿到所有修改的语句的 path。
path 的 api 还是比较多的,这也是 babel 最强大的地方。主要是操作当前节点、当前节点的父节点、兄弟节点,作用域,以及增删改的方法。
state
state是遍历过程中AST节点之间传递数据的方式。插件的visitor中,第一个参数是path,第二个参数就是state。
插件可以从state中拿到opts,也就是插件的配置项,也可以拿到file对象,file中有一些文件级别的信息,这个也可以从path.hub.file中拿。
js
state {
file
opts
}
可以在遍历的过程中在state中存一些状态信息,用于后续的AST处理。