make 阶段:walk
上一节中,我们探讨了单一 Module 的 Build 和 Parse 过程。webpack 的 make 过程是从 EntryModule 开始的。我们知道,文件系统本身是文件树,ModuleDependence 本身也是 DependenceTree,因此从 EntryModule 开始的 make 过程必然是一个递归的过程。这本节内容中,我们来探讨 ModuleTree 的 make 过程。
# 目录
上一节在 JavascriptParser
parser.pase
函数的内容中,我们了解到对 Module 的 AST,有如下的过程:
if (this.hooks.program.call(ast, comments) === undefined) {
// preWalk AST
this.preWalkStatements(ast.body);
// blockPreWalk
this.blockPreWalkStatements(ast.body);
// Walk AST
this.walkStatements(ast.body);
}
2
3
4
5
6
7
8
这里针对 ast.body
有 preWalk
和 walk
的过程。所谓 preWalk
就是在 walk
之前的处理工作。walk 的过程就是对 AST 进行遍历查找依赖的过程。
# ParserHook.program
这个 Hook 为什么叫 program,事情上 acorn 根据 ESTree
的规范 parse 出来的 AST 顶层对象就叫 Program。如下:
{
"type": "Program",
"start": 0,
"end": 25,
"body": [
{
"type": "FunctionDeclaration",
"start": 0,
"end": 24,
"id": {
"type": "Identifier",
"start": 9,
"end": 18,
"name": "printTips"
},
"expression": false,
"generator": false,
"async": false,
"params": [],
"body": {
"type": "BlockStatement",
"start": 21,
"end": 24,
"body": []
}
}
],
"sourceType": "module"
}
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
参考:
既然 ParserHook.program
在此时触发,那么我们就来看下有哪些插件在此注册了 Callback。事实上,这样的插件有:
- CompatibilityPlugin
- DefinePlugin
- UseStrictPlugin
- HarmonyDetectionParserPlugin
- InnerGraphPlugin
- SideEffectsFlagPlugin
现在我们只能深入这些插件来查看他们针对 AST 做了哪些事情。现在有一个疑点,在 lib/WebpackOptionsApply.js
中注册这些插件时,只传入了 compiler,那么插件中 parser 是怎么获取的呢?我们能在这些插件中找到这样的代码:
normalModuleFactory.hooks.parser
.for("javascript/auto")
.tap("CompatibilityPlugin", handler);
normalModuleFactory.hooks.parser
.for("javascript/dynamic")
.tap("CompatibilityPlugin", handler);
normalModuleFactory.hooks.parser
.for("javascript/esm")
.tap("CompatibilityPlugin", handler);
2
3
4
5
6
7
8
9
原来这些插件是在 NormalModuleFactoryHook.parser 钩子中获得 parser 的,并在 handler 中在向 ParserHook.program
注入处理逻辑。
# UseStrictPlugin
parser.hooks.program.tap("UseStrictPlugin", (ast) => {
const firstNode = ast.body[0];
// 如果是 use strict
if (
firstNode &&
firstNode.type === "ExpressionStatement" &&
firstNode.expression.type === "Literal" &&
firstNode.expression.value === "use strict"
) {
// Remove "use strict" expression. It will be added later by the renderer again.
// This is necessary in order to not break the strict mode when webpack prepends code.
// @see https://github.com/webpack/webpack/issues/1970
// 删除 use strict 语句,添加 ConstDependency
const dep = new ConstDependency("", firstNode.range);
dep.loc = firstNode.loc;
parser.state.module.addPresentationalDependency(dep);
parser.state.module.buildInfo.strict = true;
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# HarmonyDetectionParserPlugin
parser.hooks.program.tap("HarmonyDetectionParserPlugin", (ast) => {
// 如果 Module 是 javascript/esm,则严格使用 HarmonyCompatibilityDependency
const isStrictHarmony = parser.state.module.type === "javascript/esm";
// 如果包含 import/export 语句,也需要使用 HarmonyCompatibilityDependency
const isHarmony =
isStrictHarmony ||
ast.body.some(
(statement) =>
statement.type === "ImportDeclaration" ||
statement.type === "ExportDefaultDeclaration" ||
statement.type === "ExportNamedDeclaration" ||
statement.type === "ExportAllDeclaration"
);
// 添加 HarmonyCompatibilityDependency
if (isHarmony) {
const module = parser.state.module;
const compatDep = new HarmonyCompatibilityDependency();
compatDep.loc = {
start: {
line: -1,
column: 0,
},
end: {
line: -1,
column: 0,
},
index: -3,
};
module.addPresentationalDependency(compatDep);
// ......
parser.scope.isStrict = true;
}
});
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
ParserHook.program 时主要针对 AST 中是否有 'use strict' 语句和 import/export
语句,分别向 Module 添加 ConstDependency 和 HarmonyCompatibilityDependency。
# preWalkStatements
深度遍历(DFS)AST,触发 ParserHook.preStatement,并修改变量声明语句的 scope。
// Pre walking iterates the scope for variable declarations
preWalkStatements(statements) {
for (let index = 0, len = statements.length; index < len; index++) {
const statement = statements[index];
this.preWalkStatement(statement);
}
}
preWalkStatement(statement) {
this.statementPath.push(statement);
// 触发 ParserHook.preStatement => call
if (this.hooks.preStatement.call(statement)) {
this.prevStatement = this.statementPath.pop();
return;
}
switch (statement.type) {
case "BlockStatement":
this.preWalkBlockStatement(statement);
break;
case "DoWhileStatement":
this.preWalkDoWhileStatement(statement);
break;
case "ForInStatement":
this.preWalkForInStatement(statement);
break;
case "ForOfStatement":
this.preWalkForOfStatement(statement);
break;
case "ForStatement":
this.preWalkForStatement(statement);
break;
case "FunctionDeclaration":
this.preWalkFunctionDeclaration(statement);
break;
case "IfStatement":
this.preWalkIfStatement(statement);
break;
case "LabeledStatement":
this.preWalkLabeledStatement(statement);
break;
case "SwitchStatement":
this.preWalkSwitchStatement(statement);
break;
case "TryStatement":
this.preWalkTryStatement(statement);
break;
case "VariableDeclaration":
this.preWalkVariableDeclaration(statement);
break;
case "WhileStatement":
this.preWalkWhileStatement(statement);
break;
case "WithStatement":
this.preWalkWithStatement(statement);
break;
}
this.prevStatement = this.statementPath.pop();
}
// ......
_preWalkVariableDeclaration(statement, hookMap) {
for (const declarator of statement.declarations) {
switch (declarator.type) {
case "VariableDeclarator": {
// 触发 ParserHook.preDeclarator => call
if (!this.hooks.preDeclarator.call(declarator, statement)) {
this.enterPattern(declarator.id, (name, decl) => {
let hook = hookMap.get(name);
if (hook === undefined || !hook.call(decl)) {
hook = this.hooks.varDeclaration.get(name);
if (hook === undefined || !hook.call(decl)) {
this.defineVariable(name);
}
}
});
}
break;
}
}
}
}
defineVariable(name) {
const oldInfo = this.scope.definitions.get(name);
// ......
// 收集变量和 scope 到 this.scope.definitions
this.scope.definitions.set(name, this.scope);
}
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
# defineVariable
# blockPreWalkStatements
遍历 AST,触发 ParserHook.blockPreStatement,查找 block 变量声明(import/export),以执行不同的操作。
// Block pre walking iterates the scope for block variable declarations
blockPreWalkStatements(statements) {
for (let index = 0, len = statements.length; index < len; index++) {
const statement = statements[index];
this.blockPreWalkStatement(statement);
}
}
blockPreWalkStatement(statement) {
this.statementPath.push(statement);
// 触发 ParserHook.blockPreStatement => call
if (this.hooks.blockPreStatement.call(statement)) {
this.prevStatement = this.statementPath.pop();
return;
}
switch (statement.type) {
case "ImportDeclaration":
// 触发 ParserHook.import
// 触发 ParserHook.importSpecifier
this.blockPreWalkImportDeclaration(statement);
break;
case "ExportAllDeclaration":
// 触发 ParserHook.exportImport
// 触发 ParserHook.exportImportSpecifier
this.blockPreWalkExportAllDeclaration(statement);
break;
case "ExportDefaultDeclaration":
// 触发 ParserHook.exportSpecifier
this.blockPreWalkExportDefaultDeclaration(statement);
break;
case "ExportNamedDeclaration":
// 触发 ParserHook.exportImport 或者 ParserHook.export
// 触发 ParserHook.exportDeclaration
// 触发 ParserHook.exportImportSpecifier 或者 ParserHook.exportSpecifier
this.blockPreWalkExportNamedDeclaration(statement);
break;
case "VariableDeclaration":
this.blockPreWalkVariableDeclaration(statement);
break;
case "ClassDeclaration":
this.blockPreWalkClassDeclaration(statement);
break;
}
this.prevStatement = this.statementPath.pop();
}
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
# walkStatements
walk 阶段很重要的作用就是通过遍历 AST,触发 Parser 上的各种 Hook,因此在 ParserHook 上注册的各种插件就会被触发。在这些插件当中,很重要的就是收集依赖的插件。
这个遍历的过程十分繁琐,下面我们从插件的角度反向探讨 Hook 的触发时机和产生的依赖。
# HarmonyImportDependencyParserPlugin
关注 hook 和 parser.state.module.addDependency
的关系。源码位置 lib/dependencies/HarmonyImportDependencyParserPlugin.js
。
ParserHook | addDependency | Hook 触发时机 |
---|---|---|
import | HarmonyImportSideEffectDependency | blockPreWalkStatement => blockPreWalkImportDeclaration |
binaryExpression | HarmonyEvaluatedImportSpecifierDependency | walkArrayExpression...... => walkExpressions => walkExpression => walkBinaryExpression |
expression | HarmonyImportSpecifierDependency | walkExpressions => walkExpression => walkMetaProperty |
expressionMemberChain | HarmonyImportSpecifierDependency | walkExpressions => walkExpression => walkMemberExpression |
callMemberChain | HarmonyImportSpecifierDependency | walkExpressions => walkExpression => walkCallExpression |
# HarmonyExportDependencyParserPlugin
ParserHook | addDependency | Hook 触发时机 |
---|---|---|
exportImport | HarmonyImportSideEffectDependency | blockPreWalkStatements => blockPreWalkStatement => blockPreWalkExportNamedDeclaration |
exportExpression | HarmonyExportExpressionDependency | walkStatements => walkStatement => walkExportDefaultDeclaration |
exportSpecifier | HarmonyExportImportedSpecifierDependency | blockPreWalkStatements => blockPreWalkStatement => blockPreWalkExportDefaultDeclaration |
exportImportSpecifier | HarmonyExportImportedSpecifierDependency 或 HarmonyExportImportedSpecifierDependency | blockPreWalkStatements => blockPreWalkStatement => blockPreWalkExportAllDeclaration |
在代码中搜索 .addDependency(dep)
,如果代码是在 ParserHook 中执行的,就是在处理 Module 依赖的插件。
// lib/DependenciesBlock.js
addDependency(dependency) {
this.dependencies.push(dependency);
}
2
3
4
注意 NormalModule 继承自 Module,Module 继承自 DependenciesBlock。因此 每个 Module 都有一个 dependencies 数组。