javascript魔术师——babel

2.png

什么是babel

babel是一个编译器,负责将一些未纳入标准的js语法转换成符合当前浏览器(或node)的标准
可以毫不避讳的说,babel的出现让前端开发进入了新时期,新阶段

为了理解其独一无二的作用,下面我们看个例子

5.png
一个简单的方法将n=>n**2 转换成Math.pow(n,2) —— 正则匹配法
6.png

7.png

8.png
但是对于下面这种格式的情形,正则表达式就出问题了,虽然它可以在字符串的世界里可以无敌,却识别不了词法语法
9.png

独门秘技抽象语法树

4.png

语法树的构建

11.png
12.png
13.png
14.png
15.png

结点替换

16.png
17.png

18.png
19.png

复杂的使用完全可以使用语法树进行拆解,字符串类型直接被识别到了 不进行转换,nice!!

20.png

BinaryExpression的结点树替换成CallExpression,再把树通过babel/generator生成为代码,nice!!

plugins

74.png)75.png)76.png

前置知识

66.png
入口文件及对应的接收者

67.png

结点校验

68.png

结点构建

69.png

template

用types来构建复杂树非常反人类,所以template出现了
70.png
71.png)72.png)73.png

nodePath

84.png)85.png)86.png

Scope 88.png

如何创建插件

77.png
78.png)79.png)80.png)82.png)83.png

例子

最简陋的实现

以我们代码中常用到的可选链插件为例,我们来重新实现一下这个插件

c1.png

c2.png

c3.png
虽然简陋但是实现了最简单的效果
但是再加一个属性,就立刻跪了
c4.png

c5.png

修复正常.符号也会触发?.转换问题

很显然,b.c不需要判断b,所以我们继续完善

c9.png
再试运行一次,差强人意

c10.png

但是如果再加个可选值
c11.png
就会出现问题
c12.png

存储计算结果

a==null?undefined:a.b被重复声明了两次,这样会造成一个重复执行三目运算,显然是不合理的
所以我们再加以优化,如下所示
c13.png
我们先生成一个当前作用域里独一无二的变量并注入到作用域,再边进行三目运算,边进行结果赋值,对运算结果进行存储

c14.png

你以为这就完事大吉了,可还有个问题呢
c15.png

防止undefined被声明 如let undefined = 2

为了防止真的有大哥对undefined进行赋值,我们要用babel的api在作用域中对undefined值进行build
c17.png
结果
c18.png

添加&&的实现方式

还没完 我们再玩一下,babel的可传入option,构造另一种形式
c20.png

参数传入
c21.png
运行结果
c22.png

c23.png
一个简单的插件就完成了。

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

module.exports = function plugin(
{ types: t, template },
{ loose = false } = {}
) {
const buildLoose = template.expression(`
(%%tmp%% = %%obj%%) && %%expr%%
`);

return {
name: "optional-chaining",
visitor: {
OptionalMemberExpression(path) {
let {object, property} = path.node
if (path.node.optional) {
let tmp = path.scope.generateUidIdentifier("obj");
path.scope.push({id: tmp});
let undef = path.scope
.buildUndefinedNode();
if(loose){
path.replaceWith(buildLoose({tmp,obj:object,expr:template.expression.ast`${tmp}.${property}`}))
return
}
path.replaceWith(
template.expression.ast`(${tmp} =${object})== null?${undef}:${tmp}.${property}`
)
} else {
path.replaceWith(
template.expression.ast`${object}.${property}`
)
}
}
}
}
};

另一种实现

这是原作者的实现,但是理解起来比较复杂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
module.exports = function plugin(
{ types: t, template },
{ loose = false } = {}
) {
const buildLoose = template.expression(`
(%%tmp%% = %%obj%%) && %%expr%%
`);

return {
name: "optional-chaining",
visitor: {
OptionalMemberExpression(path) {
//#region 另一种实现方式
let originalPath = path;
let tmp =
path.scope.generateUidIdentifier("obj");
path.scope.push({id: tmp});

while (!path.node.optional) {
path = path.get("object");
}

let {object} = path.node;

let memberExpr = tmp;
do {
memberExpr = t.memberExpression(
memberExpr,
path.node.property,
path.node.computed,
);

if (path === originalPath) {
break;
}

path = path.parentPath;
} while (true);
if (loose) {
path.replaceWith(
buildLoose({
tmp,
obj: object,
expr: memberExpr,
})
)
} else {
let undef =
path.scope
.buildUndefinedNode();

originalPath.replaceWith(
template.expression.ast`
(${tmp} = ${object}) == null
? ${undef}
: ${memberExpr}
`
)
}
}
//#endregion
}
}
};

代码仓库地址
21.png

转换过程

22.png

所用模块

23.png
24.png

js到语法树——解析

25.png

词法分析

26.png
27.png)28.png)29.png)30.png

语法分析

31.png)32.png)33.png
35.png
36.png)37.png

语义分析

38.png)39.png)40.png)41.png

语法树的遍历——深度优先遍历

42.png)43.png)44.png

一个例子fn(3)->[6,fn]

45.png

知识补充:

二叉树的遍历一般使用循环或者递归进行。

以递归为例

深度优先遍历,会优先遍历left结点,没有left结点,再遍历right结点。

递是函数的入栈,归是函数的出栈,从首节点往下递,从尾结点网上归。

46.png)47.png)48.png)49.png)50.png)51.png)52.png)53.png
54.png

当CallExpression出栈之后,进行结点替换操作
55.png

56.png)57.png)58.png)59.png)60.png)61.png)62.png)63.png

结语

一句话与大家共勉:认识是不断螺旋上升的过程,理论的形成也是基于当时的时空,在不断变化的环境中,不断实践才能出真知。

参考

视频地址
原作者github

查看评论