traverse的path、scope、visitor

源码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(), // 结束所有遍历
}

containerkeylistkey 这几个属性不常用,简单介绍下:

因为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.bindingsscope.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,代表绑定的类型:

  1. var、let、const分别代表var、let、const形式声明的变量
  2. param代表参数的声明
  3. 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处理。

相关推荐
前端工作日常27 分钟前
我理解的`npm pack` 和 `npm install <local-path>`
前端
李剑一41 分钟前
说个多年老前端都不知道的标签正确玩法——q标签
前端
嘉小华1 小时前
大白话讲解 Android屏幕适配相关概念(dp、px 和 dpi)
前端
姑苏洛言1 小时前
在开发跑腿小程序集成地图时,遇到的坑,MapContext.includePoints(Object object)接口无效在组件中使用无效?
前端
奇舞精选1 小时前
Prompt 工程实用技巧:掌握高效 AI 交互核心
前端·openai
Danny_FD1 小时前
React中可有可无的优化-对象类型的使用
前端·javascript
用户757582318551 小时前
混合应用开发:企业降本增效之道——面向2025年移动应用开发趋势的实践路径
前端
P1erce1 小时前
记一次微信小程序分包经历
前端
LeeAt1 小时前
从Promise到async/await的逻辑演进
前端·javascript
等一个晴天丶1 小时前
不一样的 TypeScript 入门手册
前端