js 运行过程分析
# js 运行过程分析
# 程序运行的概念
JS可以在浏览器中运行,运行在V8引擎中执行过程 了解运行过程前,先弄清编程中的几个概念。
- 编译器(Compiler):源代码在运行之前编译成计算机能执行的机器码,由于要编译完所有源代码后在执行,所以编译器需要更多的内存存储机器码,但执行快;如【java、c】等语言
- 解释器(Interpreter):将源代码在运行时逐行解释执行,由于是一边解释一边执行,故启动快,执行慢;如【javascript、python】
- 抽象语法树(AST):解析器(Parser) 将源代码进行词法分析、语法分析后生成的抽象语法树,想要看生成的结果请戳:astexplorer.net/
- 字节码(Bytecode):又称作中间代码,在JS解析中就是从AST -> 字节码 -> 机器码,字节码是后面才被V8引擎引入的,主要目的是为了解决机器码带来的内存占用问题;
- 即时编译器(JIT):简单的理解就是一段代码被解释器执行多次之后就会变成热点代码(HotSpot),热点代码会被编译器直接编译成机器码,当代码再次执行时直接运行机器码,从而达到提高性能的目的,这种编译器和解释器混合使用的技术被叫做即时编译。
# V8执行一段JS代码的过程图
# JS即时编译器的运行过程
图片绘制参考来源:极客时间-浏览器工作原理与实践 (opens new window)
# 实现一个简易 Tree Shaking 脚本
ESM规范基于静态运行,可以实现tree shaking;commonjs【node环境】规范是动态运行,无法实现tree shaking; 通过 acorn 库,实现一个 treeShaking 的脚本。acorn 库实现解析 AST 的入口框架,可以将源代码parse为AST对象。 经过acorn的parse之后,body为生成的ast对象
- 1.首先设置一段要被treeShaking的代码,将这段代码添加到test.js文件中
function add(a, b) {
return a + b;
}
function multiple(a, b) {
return a * b;
}
let firstOp = 9;
let secondOp = 10;
if (firstOp > 0) {
const aaa = 'AAA';
}
add(firstOp, secondOp);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
看下图,了解整体架构流程
- 创建JSEmitter类,用于根据 AST 产出 JavaScript 代码
class JSEmitter {
// 访问变量声明,以下都是工具方法
visitVariableDeclaration(node) {
let str = ''
str += node.kind + ' '
str += this.visitNodes(node.declarations)
return str + '\n'
}
visitVariableDeclarator(node, kind) {
let str = ''
str += kind ? kind + ' ' : str
str += this.visitNode(node.id)
str += '='
str += this.visitNode(node.init)
return str + ';' + '\n'
}
visitIdentifier(node) {
return node.name
}
visitLiteral(node) {
return node.raw
}
visitBinaryExpression(node) {
let str = ''
str += this.visitNode(node.left)
str += node.operator
str += this.visitNode(node.right)
return str + '\n'
}
visitFunctionDeclaration(node) {
let str = 'function '
str += this.visitNode(node.id)
str += '('
for (let param = 0; param < node.params.length; param++) {
str += this.visitNode(node.params[param])
str += ((node.params[param] == undefined) ? '' : ',')
}
str = str.slice(0, str.length - 1)
str += '){'
str += this.visitNode(node.body)
str += '}'
return str + '\n'
}
visitBlockStatement(node) {
let str = ''
str += this.visitNodes(node.body)
return str
}
visitCallExpression(node) {
let str = ''
const callee = this.visitIdentifier(node.callee)
str += callee + '('
for (const arg of node.arguments) {
str += this.visitNode(arg) + ','
}
str = str.slice(0, str.length - 1)
str += ');'
return str + '\n'
}
visitReturnStatement(node) {
let str = 'return ';
str += this.visitNode(node.argument)
return str + '\n'
}
visitExpressionStatement(node) {
return this.visitNode(node.expression)
}
visitNodes(nodes) {
let str = ''
for (const node of nodes) {
str += this.visitNode(node)
}
return str
}
// 根据类型,执行相关处理函数
visitNode(node) {
let str = ''
switch (node.type) {
case 'VariableDeclaration':
str += this.visitVariableDeclaration(node)
break;
case 'VariableDeclarator':
str += this.visitVariableDeclarator(node)
break;
case 'Literal':
str += this.visitLiteral(node)
break;
case 'Identifier':
str += this.visitIdentifier(node)
break;
case 'BinaryExpression':
str += this.visitBinaryExpression(node)
break;
case 'FunctionDeclaration':
str += this.visitFunctionDeclaration(node)
break;
case 'BlockStatement':
str += this.visitBlockStatement(node)
break;
case "CallExpression":
str += this.visitCallExpression(node)
break;
case "ReturnStatement":
str += this.visitReturnStatement(node)
break;
case "ExpressionStatement":
str += this.visitExpressionStatement(node)
break;
}
return str
}
// 入口
run(body) {
let str = ''
str += this.visitNodes(body)
return str
}
}
module.exports = JSEmitter
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
- 创建treeShaking.js文件
const acorn = require('acorn');
const l = console.log;
const JSEmitter = require('./js-emitter');
const fs = require('fs');
// 获取命令行参数
const args = process.argv[2]; // 被编译的文件
const buffer = fs.readFileSync(args).toString();
const body = acorn.parse(buffer, {
ecmaVersion: 'latest',
sourceType: 'module',
}).body;
const jsEmitter = new JSEmitter();
let decls = new Map();
// 添加被使用的代码
let calledDecls = [];
let code = [];
// 遍历处理
body.forEach(function (node) {
if (node.type == 'FunctionDeclaration') {
const code = jsEmitter.run([node]);
decls.set(jsEmitter.visitNode(node.id), code);
return;
}
// 是表达式类型
if (node.type == 'ExpressionStatement') {
// 是函数调用
if (node.expression.type == 'CallExpression') {
const callNode = node.expression;
calledDecls.push(jsEmitter.visitIdentifier(callNode.callee));
const args = callNode.arguments;
for (const arg of args) {
// 对调用参数进行设置
if (arg.type == 'Identifier') {
calledDecls.push(jsEmitter.visitNode(arg));
}
}
}
}
if (node.type == 'VariableDeclaration') {
const kind = node.kind;
for (const decl of node.declarations) {
decls.set(
jsEmitter.visitNode(decl.id),
jsEmitter.visitVariableDeclarator(decl, kind)
);
}
return;
}
if (node.type == 'Identifier') {
// Identifier标识符
calledDecls.push(node.name);
}
// 上面 node.type == 'ExpressionStatement' 时,并没有return
// 所以会将 node.type为ExpressionStatement的AST节点,添加到code中
code.push(jsEmitter.run([node])); //会添加'add(firstOp,secondOp);\n'
});
// 生成 code
code = calledDecls
.map((c) => {
return decls.get(c);
})
.concat([code.filter(Boolean)])
.join('');
fs.writeFileSync('test.shaked.js', code);
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
文件通过 process.argv 获取到目标文件,对于目标文件通过 fs.readFileSync() 方法读出字符串形式的内容 buffer,对于这个 buffer变量,我们使用 acorn.parse进行解析,并对产出body内容进行遍历。
- decls——Map 类型,存储所有的函数或变量声明类型节点
- calledDecls——数组类型,存储了代码中真正使用到的数或变量声明
- code——数组类型,存储了AST节点为ExpressionStatement或未被匹配到节点的的代码
- 经过treeShaking后的代码
function add(a,b){return a+b
}
let firstOp=9;
let secondOp=10;
add(firstOp,secondOp);
1
2
3
4
5
6
2
3
4
5
6
备注:在vscode中可以i添加调试debugger,launch.json 文件
{
"version": "0.2.0",
"configurations": [
{
"command": "npm start",
"name": "断点调试",
"request": "launch",
"type": "node-terminal"
}
]
}
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
编辑 (opens new window)
上次更新: 2024/12/28, 08:35:49