includeStatements
tree-shaking 本来做的是删除代码的意思。我们需要注意的是 rollup 在删除无用代码之前是先将代码标记为“included” ,最后调用 includeStatements() 将需要保留的代码打包到 chunks 中。接下来我们继续分析 this.includeStatements() 的逻辑。
class Graph {
//...
async build(): Promise<void> {
//...
//遍历所有的ast.node并且修改node.included的值
this.includeStatements();
//...
}
private includeStatements(): void {
for (const module of [...this.entryModules, ...this.implicitEntryModules]) {
//标记模块 isExecuted = true;
markModuleAndImpureDependenciesAsExecuted(module);
}
if (this.options.treeshake) {
let treeshakingPass = 1;
do {
timeStart(`treeshaking pass ${treeshakingPass}`, 3);
this.needsTreeshakingPass = false;
for (const module of this.modules) {
if (module.isExecuted) {
if (module.info.moduleSideEffects === 'no-treeshake') {
module.includeAllInBundle();
} else {
module.include();
}
}
}
if (treeshakingPass === 1) {
// 仅需在第一次的时候将模块内的导出语句包含进来
for (const module of [...this.entryModules, ...this.implicitEntryModules]) {
//module.preserveSignature => 'exports-only'
if (module.preserveSignature !== false) {
module.includeAllExports(false);
this.needsTreeshakingPass = true;
}
}
}
timeEnd(`treeshaking pass ${treeshakingPass++}`, 3);
} while (this.needsTreeshakingPass); //this.needsTreeshakingPass为true的时候才会继续执行treeShaking逻辑
} else {
for (const module of this.modules) module.includeAllInBundle();
}
//...
}
}
includeStatements 内部首先会遍历所有入口模块执行 markModuleAndImpureDependenciesAsExecuted(module); 将 module.isExecuted 修改为 true; 接着 if 语句判断是否存在 this.options.treeshake ,是则执行 module.include() 逻辑,否则执行 module.includeAllInBundle()。也就是将所有代码都打包输出。
当然,我们使用 rollup 打包的时候程序帮我们默认开启了 treeshake 。 因此我们看到 if 代码块内部的逻辑,首先定义了 treeshakingPass=1,并且执行过 do 语句之后 treeshakingPass 会执行 ++ 的操作,因此 module.includeAllExports(false) 的逻辑只会执行一次。do...while 语句表示 do 里面的逻辑至少会被执行一次。for (const module of this.modules) 的内部首先判断了 module.isExecuted 并且 module.info.moduleSideEffects !== 'no-treeshake' 才会执行 module.include() 逻辑。因为在执行 markModuleAndImpureDependenciesAsExecuted(module) 的时候就已经将 modulel.isExecuted 设置为 true 了。并且 module.info.moduleSideEffects 默认是一个对象。所以默认情况下会执行 module.include() 逻辑。
module.include 代码定义在 src/Module.ts 中:
class Module {
//...
include(): void {
//context => {"brokenFlow":0,"includedCallArguments":{},"includedLabels":{}}
const context = createInclusionContext();
if (this.ast!.shouldBeIncluded(context)) this.ast!.include(context, false);
}
}
module.include 方法内部首先执行 const context = createInclusionContext() 会得到这么一个对象‘{"brokenFlow":0,"includedCallArguments":{},"includedLabels":{}}’。接着判断 this.ast.shouldBeIncluded(context)为 true 才去执行 this.ast.include(context, false) 方法。this.ast.include 实际上调用的就是 NodeBase.shouldBeIncluded 方法。
NodeBase.shouldBeIncluded
//src/ast/nodes/shared/Node.ts
class NodeBase extends ExpressionEntity implements ExpressionNode {
//...
shouldBeIncluded(context: InclusionContext): boolean {
return this.included || (!context.brokenFlow && this.hasEffects(createHasEffectsContext()));
}
//...
}
这里的 shouldBeIncluded 方法内部 this.included 中的 this 就是 Program,默认值为 false。 context.brokenFlow 为 0,this.hasEffects(createHasEffectsContext()) 实际上就是执行 Program.hasEffects 方法,它接收一个由 createHasEffectsContext() 方法创建的一个对象类型的参数。
createHasEffectsContext()
//src/ast/ExecutionContext.ts
export function createHasEffectsContext(): HasEffectsContext {
return {
accessed: new PathTracker(), //被访问过的实体会存放在这里
assigned: new PathTracker(), //被赋值调用过的实体会存放在这里
brokenFlow: BROKEN_FLOW_NONE, //0
called: new DiscriminatedPathTracker(), //被调用过的实体会存放到这里
ignore: {
breaks: false,
continues: false,
labels: new Set(),
returnYield: false
},
includedLabels: new Set(),
instantiated: new DiscriminatedPathTracker(), //实例
replacedVariableInits: new Map()
};
}
Program.hasEffects 内部其实就是遍历 Program.body 对其所有的子节点调用 node.hasEffects(context) 方法来判断是否返回了 true。如果是的话才会调用 this.ast.include(context, false) 将 Program.included 设置为 true。Program.included 为 true 就意味着这个 module 的代码块需要打包到输出的 bundle。
//src/ast/nodes/Program.ts
export default class Program extends NodeBase {
//...
hasEffects(context: HasEffectsContext): boolean {
// 设置 hasCachedEffect 缓存
if (this.hasCachedEffect) return true;
for (const node of this.body) {
//遍历所有子节点进行hasEffects判断,如果返回了true,程序就会将Program.included设置为true。
if (node.hasEffects(context)) {
return (this.hasCachedEffect = true);
}
}
return false;
}
//...
}
总结
通过本章节的分析,我们知道 Program.shouldBeIncluded 其实取决于它内部所有的子节点是否有任意一个节点调用 hasEffects 返回 true。如果返回了 true 则证明 Program 需要打包到输出到 bundle。但是具体哪些节点需要被包含到最终 bundle 里面则需要进一步判断 node.hasEffects(createHasEffectsContext()) 的返回值了。下一章节我们继续分析 node.hasEffects() 在不同的节点类型时是如何实现的。
rollup 揭秘相关文章
- rollup 技术揭秘系列一 准备篇(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列二 源码目录结构及打包入口分析(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列三 rollup 函数(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列四 graph.build(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列五 构建依赖图谱(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列六 模块排序(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列七 includeStatements(可能是全网最系统性的 rollup 源码分析文章
- rollup 技术揭秘系列八 node.hasEffects(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列九 module.include()(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列十 includeStatements 总结(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列十一 rollup 打包配置选项整理(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列十二 handleGenerateWrite(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列十三 bundle.generate(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列十四 renderChunks(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列十五 renderModules(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列十六 Rollup 插件开发指南(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列十七 rollup-cli 的开发(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列十八 Rollup 打包流程示意图(可能是全网最系统性的 rollup 源码分析文章)