0.前置知识
0.1 访问者模式
背景:访问者模式是行为型设计模式的一种,主要作用场景是:「 处理前端DOM树操作 」,因为前端的DOM中树结构非常的深,并且树中的各个节点的类型不同(但是节点类型范围就几种),所以当我们要对DOM树进行操作的时候常常要写很多代码。这个时候我们就可以通过访问者模式,将操作行为与被操作对象隔离开,进而将这部分代码抽象出来。
访问者(Visitor) ,当我们有一个对象集合,集合中的元素类型是不一样的,但类型是相对固定的,例如只有3种不同的类型,但是可能有30个元素。如果我们希望对集合中的所有元素进行某种操作,从接口的角度来看,由于类型不一致,我们很难通过一个统一的接口来遍历集合元素并对其进行操作。这时我们可以考虑使用访问者模式,它将 「获取某个元素」和对「元素的操作行为」进行了分离。
使用场景: 当我们需要使用Babel的插件的时候,常常会用到访问者模式。我们编写的Babel插件其实也是通过定义一个实例化visitor对象处理一系列的AST节点来完成我们对代码的修改操作
// 例如做项目,需求文档
// 项目经理访问需求文档的是想了解项目边界
// 开发人员访问需求文档的是想知道有哪些功能需要开发、难点与重点
// 测试人员访问需求文档是想评估测试用例多少
// 不同的立场返回项目的信息会不同,但是需求文档结构还是那么需求文档。
// 定义被访问者
class Element {
accept(visitor) {
visitor.visit(this);
}
}
// 定义访问者
class Visitor {
visit(element) {
// 处理元素
}
}
// 定义具体的元素
class ConcreteElement extends Element {
// 具体的实现
}
// 定义具体的访问者
class ConcreteVisitor extends Visitor {
visit(element) {
// 处理具体的元素
}
}
// 使用访问者模式
const element = new ConcreteElement();
const visitor = new ConcreteVisitor();
element.accept(visitor);
// 本质上就是通过被访问者通过accpet方法
// 1.将访问者实例作为参数传入element.accept(visitor)
// 2.然后在accept函数中调用visitor.visit(this)方法,被访问者实例给访问者visit方法参数,这样访问者就可以在方法中获取目标元素整体了。
// 实际上,但我们需要给一个对象的方法参数传入另一个对象实例的时候,就可以使用这种方法
下面是访问者模式实例:
老师家访学生,不同科目的老师就是访问者,通过学生的描述,老师对同一个学生做出一个判断
// 元素类
class Student {
constructor(name, chinese, math, english) {
this.name = name
this.chinese = chinese
this.math = math
this.english = english
}
accept(visitor) {
visitor.visit(this)
}
}
// 访问者类
class ChineseTeacher {
visit(student) {
console.log(`语文${student.chinese}`)
}
}
class MathTeacher {
visit(student) {
console.log(`数学${student.math}`)
}
}
class EnglishTeacher {
visit(student) {
console.log(`英语${student.english}`)
}
}
// 实例化元素类
const student = new Student('张三', 90, 80, 60)
// 实例化访问者类
const chineseTeacher = new ChineseTeacher()
const mathTeacher = new MathTeacher()
const englishTeacher = new EnglishTeacher()
// 接受访问
student.accept(chineseTeacher)
student.accept(mathTeacher)
student.accept(englishTeacher)
知识点1: 行为型设计模式:当为了完成某项功能,需要多个类协作才能完成的模式就是行为型模式,例如观察者模式需要监听者和触发者
知识点2:
juejin.cn/post/721502…
0.2 JS语法单元种类:
- 关键字:
const、let、var等 - 标识符:可能是一个变量,也可能是if/else关键字,或者true/false常量
- 运算符
- 数字
- 空格
- 注释
1.一个完整的编译器的编译过程一定存在三个阶段:解析、转化处理、生成。其中我们一般需要做的就是处理转化这个阶段,将拆解后的AST的原代码,按照我们的逻辑进行增删改查。最后生成另一种形式的代码,说白了,就是将解析得到的AST树进行修改,变成新的AST树,然后生成新的代码
大概流程如下所示:
1.1 解析(一共两个步骤:词法分析、语法分析)
掌握:解析阶段的两个步骤,每个步骤的大概过程(词法分析怎么拆分token,token表现形式是什么样子,语法分析的产物是什么样子)
解析主要就是对代码进行拆分,一个关键字都通过词法分析、语法分析确定作用
(1)词法分析
词法分析主要通过分词器tokenizer,将源码拆分为tokens。例如:
对(add 2 (subtract 4 2)) 进行词法分析后得到(如何判别是不是token:代码中两边可以用空格间隔的都可以看作一个token,例如看见一行语句就能知道有多少个token,说明你真正知道token的识别),token的表现形式是对象形式:{ type , value }
[
{ type: "paren", value: "(" }, // 左括号,左括号两边都可以用空格间隔
{ type: "name", value: "add" }, // add变量,注意这里不能把add拆成a d d 三个字符,这是整体,代码中两边可以用空格间隔的都可以看作一个token
{ type: "number", value: "2" },
{ type: "paren", value: "(" },
{ type: "name", value: "subtract" },
{ type: "number", value: "4" },
{ type: "number", value: "2" },
{ type: "paren", value: ")" },
{ type: "paren", value: ")" },
];
(2)语法分析
语法分析就是将词法分析得到的一系列tokens,整理组合成与语法相关联的表达式形式,语法分析得到的产物就是AST树。例如(add 2 (subtract 4 2))得到上述的词法分析tokens数组,现在进行语法解析后得到的AST:
{
type: 'Program',
body: [{
type: 'CallExpression',
name: 'add',
params:
[{
type: 'NumberLiteral',
value: '2',
},
{
type: 'CallExpression',
name: 'subtract',
params: [{
type: 'NumberLiteral',
value: '4',
}, {
type: 'NumberLiteral',
value: '2',
}]
}]
}]
}
1.2 修改
2.Babel插件的编写
我们在编写babel插件的时候一般是通过函数返回一个对象,但是这个对象必须存在一个visitor属性,因为babel插件的接口规范如下:
export default function(api, options, dirname) { // babel插件接受三个参数
// 这个函数接受三个参数
// 1. api types (@babel/types)、traverse (@babel/traverse)、template(@babel/template) 等实用方法
// 2.options:插件参数
// 3.dirname:目录名
return {
visitor: {
StringLiteral(path, state) {},
}
};
};
// 在返回值对象中,visitor的对象的属性都是方法,并且方法名必须是AST的DOM节点类型,这样在遍历对应的节点类型时,就会调用visitor中对应节点类型的访问函数(这些访问函数就是我们需要加的处理逻辑)。
// 注意:visitor的函数名必须和节点类型相同,这是babel自定义插件的基本遵循规则
下面举一个例子,熟悉一下babel自定义插件的语法:
如最后附录所示,visitor中的 CallExpression 表示当遍历到AST中的调用表达式CallExpression节点时,使用visitor中的CallExpression处理函数处理节点(CallExpression表示嗲用表达式,通常为一个函数的调用,例如console.log())
const generate = require('@babel/generator').default;
const t = require('@babel/types')
export default function ({types}) {
return {
visitor: {
// 我们的访问者代码将放在这里
// 这里的CallExpression就相当于一种类型的visit函数
CallExpression(path, state) { // path表示当前节点的连接对象, state
const calleeName = generate(path.node.callee).code;
// path.node:访问当前节点
// 或者:if(t.isMemberExpression(path.node.callee) && path.node.callee.object.name === 'console' && ['log', 'info', 'error', 'debug'].includes(path.node.callee.property.name)) {
if (targetCalleeName.includes(calleeName)) {
const { line, column } = path.node.loc.start;
path.node.arguments.unshift(
types.stringLiteral(`loc:[${line},${column}]`) // 增加打印行,列
);
}
}
},
};
}
// 转化前
const str = "babel";
console.log(str);
// babel转换后
const str = "babel";
console.log("loc:[2,0]", str);
解析:
step1: 我们的目标是console.log,那么对应的AST树的节点是callExpression调用表达式,如console.log()/Math.random()/fn()等全局函数方法或者自定义函数的调用。
step2: 因此我们下一步就要从调用表达式的几种类型中,找出console类型。path.node表示当前遍历到的 node 节点,可通过它访问节点上的属性;在callExpression节点中存在callee 属性表示调用的函数名,arguments 表示调用的函数参数
(如果callee函数名为MemberExpression(成员表达式),则表示调用的是console/Math这类的全局方法,并且成员表达式会存在object属性,object中的name为'console'或者'math',如果callee函数名为Identifier(标识符),则表示调用的是自定的函数名
step3:而在console.log()中增加行,列信息,也就是console.log(str),变为console.log(行,列,str),这就等价于在console.log()函数中增加两个参数,对应到AST树上就是在
arguments数组中新增两个节点,且节点类型为字面量类型
// console.log(str)的AST树的JSON格式
{
"type": "Program",
"start": 0,
"end": 18,
"body": [
{
"type": "ExpressionStatement",
"start": 0,
"end": 17,
"expression": {
"type": "CallExpression", // 函数调用表达式
"start": 0,
"end": 16,
"callee": { // callee表示调用的函数名
"type": "MemberExpression", // 成员表达式
"start": 0,
"end": 11,
"object": {
"type": "Identifier",
"start": 0,
"end": 7,
"name": "console"
},
"property": {
"type": "Identifier",
"start": 8,
"end": 11,
"name": "log"
},
"computed": false,
"optional": false
},
"arguments": [ // arguments 表示调用的函数参数
{
"type": "Identifier",
"start": 12,
"end": 15,
"name": "str"
}
],
"optional": false
}
}
],
"sourceType": "module"
}
注意:visit函数的两个参数path对象和state对象
(1) path对象
除了能在 Path 对象上访问到当前 AST 节点、父级 AST 节点、父级 Path 对象,还能访问到添加、更新、移动和删除节点等其他方法,这些方法提高了我们对 AST 增删改的效率(2) state对象
state对象主要是解决AST各个节点类型相互独立,无法处理依赖关系的问题,而设立的参数。例如,当我们处理某一节点类型的时候需要依赖其他节点类型的处理结果,但由于 visitor 属性之间互不关联,因此需要 state 帮助我们在不同的 visitor 之间传递状态。
将状态直接设置到 this 上,Babel 会给 visitor 上的每个方法绑定 this。在 Babel 插件中,this 通常会被用于传递状态:从 pre 到 visitor 再到 post, 例如:export default function({ types: t }) { return { pre(state) { this.cache = new Map(); }, visitor: { StringLiteral(path) { this.cache.set(path.node.value, 1); } }, post(state) { console.log(this.cache); } }; }
3.Babel的配置
3.1 两种配置 babel 的文件
babel 有两种并行的配置文件:.babelrc和babel.config.js
1.项目范围的配置
babel.config.js 文件,具有不同的拓展名(json、js、html) babel.config.js 是按照 commonjs 导出对象,可以写js的逻辑。
2.相对文件的配置
.babelrc 文件,具有不同的拓展名
参考文献:juejin.cn/post/701967…
3.2 Babel 配置项
附录: