生成 chunks
生成 chunk 的核心逻辑:
async function generateChunks(bundle, getHashPlaceholder) {
const {
experimentalMinChunkSize,
inlineDynamicImports,
manualChunks,
preserveModules
} = this.outputOptions;
const manualChunkAliasByEntry =
typeof manualChunks === 'object'
? await this.addManualChunks(manualChunks)
: this.assignManualChunks(manualChunks);
const snippets = getGenerateCodeSnippets(this.outputOptions);
const includedModules = getIncludedModules(this.graph.modulesById);
const inputBase = commondir(
getAbsoluteEntryModulePaths(includedModules, preserveModules)
);
const externalChunkByModule = getExternalChunkByModule(
this.graph.modulesById,
this.outputOptions,
inputBase
);
const chunks = [];
const chunkByModule = new Map();
for (const { alias, modules } of inlineDynamicImports
? [{ alias: null, modules: includedModules }]
: preserveModules
? includedModules.map(module => ({ alias: null, modules: [module] }))
: getChunkAssignments(
this.graph.entryModules,
manualChunkAliasByEntry,
experimentalMinChunkSize,
this.inputOptions.onLog
)) {
// 根据 modules 的 execIndex(执行顺序) 进行从小到大排序。
sortByExecutionOrder(modules);
const chunk = new Chunk(
modules,
this.inputOptions,
this.outputOptions,
this.unsetOptions,
this.pluginDriver,
this.graph.modulesById,
chunkByModule,
externalChunkByModule,
this.facadeChunkByModule,
this.includedNamespaces,
alias,
getHashPlaceholder,
bundle,
inputBase,
snippets
);
chunks.push(chunk);
}
for (const chunk of chunks) {
chunk.link();
}
const facades = [];
for (const chunk of chunks) {
facades.push(...chunk.generateFacades());
}
return [...chunks, ...facades];
}
举一个例子
,假设依赖图关系如下:
rollup 配置:
// rollup.config.js
export default {
input: {
X: './X.js',
Y: './Y.js'
},
output: [
{
dir: 'dist/esm',
format: 'es',
entryFileNames: '[name].js',
manualChunks: {
common1: ['D'],
common2: ['C', 'F']
}
}
],
treeshake: false,
plugins: []
};
准备好前提样例的场景下,可以分为以下几个部分来深入了解 chunk
的实现原理。
处理手动分块逻辑
对于对象和函数的两种不同的传参方式,进行不同的处理
const manualChunkAliasByEntry =
typeof manualChunks === 'object'
? await this.addManualChunks(manualChunks)
: this.assignManualChunks(manualChunks);
通过以上方式,我们可以得到一个 Map<Module, string>
的结构,表示每个模块对应的自定义 chunk
的名称。仅仅只是做了一层映射和获取,并没有进行任何的合并逻辑。需要注意的是,模块若不存在于依赖图中,那么就要重新调用 fetchModule
方法,获取最新的 module
对象。
由于生成
chunks
是在await bundle.generate(isWrite);
阶段,也就是说依赖关系图已经构建完成了,如果自定义的模块是依赖关系图的子集,那么就可以直接使用缓存返回已经解析过的模块。
那么可以获取到的结构如下:
// rollup 配置
const rollupConfig = {
manualChunks: {
common1: ['./D.js'],
common2: ['./C.js', './F.js']
}
};
// 最终结果
const manualChunkAliasByEntry = {
ModuleD: 'common1',
ModuleC: 'common2',
ModuleF: 'common2'
};
紧接着在 getChunkDefinitionsFromManualChunks
方法中,通过 addStaticDependenciesToManualChunk
来 BFS
遍历以下条件
if (
!(
dependency instanceof ExternalModule ||
modulesInManualChunks.has(dependency)
)
) {
modulesToHandle.add(dependency);
}
并将其添加到 modulesInManualChunks
中。也就是说,modulesInManualChunks
中会记录所有手动分块的模块,以及它们的 非外部模块 的 静态依赖模块。
const chunkDefinitions = [
{
alias: 'common1',
modules: [ModuleD]
},
{
alias: 'common2',
modules: [ModuleC, ModuleG, ModuleF]
}
];
const modulesInManualChunks = new Set([ModuleD, ModuleC, ModuleF, ModuleG]);
分析依赖关系图
analyzeModuleGraph(entries)
方法中,以用户配置的入口作为 BFS
遍历的起点,在此为 X
和 Y
模块。通过 BFS
遍历所有的入口模块,收集模块与入口模块之间的对应关系。需要注意的是,若在静态模块中动态加载模块,检测到动态模块依赖则将动态依赖模块添加到 allEntries
中作为新的入口模块,等待后续的 BFS
遍历。通过关系的确定最后可以获取到 allEntries
、dependentEntriesByModule
、dynamicallyDependentEntriesByDynamicEntry
、dynamicImportsByEntry
四种数据结构。下面一一讲解这四种数据结构。
allEntries
所有的入口模块,包含用户自定义的入口模块和动态模块。在这个例子中可以获取到
jsconst allEntries = [ModuleX, ModuleY, ModuleC, ModuleE, ModuleH];
C、E、H 是动态模块,是在 BFS 遍历过程中,检测到动态模块依赖后,将动态依赖模块添加到
allEntries
中作为最新的入口模块。dependentEntriesByModule
存储的是所有模块(包含用户自定义的入口模块和动态模块)与之对应的依赖入口模块索引的映射关系。
入口模块索引解释
0
对应的是 入口模块ModuleX
1
对应的是 入口模块ModuleY
2
对应的是 动态模块ModuleC
3
对应的是 动态模块ModuleE
4
对应的是 动态模块ModuleH
jsconst dependentEntriesByModule = new Map([ [ModuleX, new Set([0])], [ModuleA, new Set([0])], [ModuleB, new Set([0, 1])], [ModuleD, new Set([0])], [ModuleY, new Set([1])], [ModuleC, new Set([1, 2])], [ModuleF, new Set([1])], [ModuleG, new Set([1, 2])], [ModuleE, new Set([3])], [ModuleH, new Set([4])] ]);
简要说明
- 从例子的依赖关系图可以得知,ModuleX, ModuleY, ModuleC, ModuleE, ModuleH 都是入口模块,所以依赖入口模块索引下标为
0, 1, 2, 3, 4
。 - 对于 ModuleB,有两个可达入口模块,分别是 ModuleX 和 ModuleY,所以依赖入口模块索引下标为
0, 1
。 - ModuleC 自身就是一个依赖模块入口,同时 ModuleC 还作为 ModuleY 的静态依赖项,所以依赖入口模块索引下标为
1, 2
。 - ModuleG 作为 ModuleC 的唯一静态依赖项,所以依赖入口模块与 ModuleC 相同,即为
1, 2
。
dynamicallyDependentEntriesByDynamicEntry
建立动态模块的入口索引与其对应的动态模块的依赖方的依赖入口模块索引的映射关系。
jsconst dynamicallyDependentEntriesByDynamicEntry = new Map([ [2, new Set([0])], [3, new Set([0, 1])], [4, new Set([1, 2])] ]);
简要说明
从例子的依赖关系图可以得知,ModuleC 作为动态依赖的加载方为 ModuleA,ModuleA的依赖入口模块为ModuleX,映射关系就为
[2(Module C的入口模块索引下标), {0(Module X的入口模块索引下标)}]
。同理,ModuleE 作为动态依赖的加载方为 ModuleB、ModuleY,ModuleB的依赖入口模块为ModuleX和ModuleY,ModuleY的依赖入口模块为 Module Y,那么并集为
{0, 1}
,映射关系就为[3(Module E的入口模块索引下标), {0(Module X的入口模块索引下标), 1(Module Y的入口模块索引下标)}]
。同理,ModuleH 作为动态依赖的加载方为 ModuleC。ModuleC 的依赖入口模块为 ModuleC 和 ModuleY,映射关系就为
[4(Module H的入口模块索引下标), {1(Module Y的入口模块索引下标), 2(Module C的入口模块索引下标)}]
。
dynamicImportsByEntry
存储的是所有入口模块和与其对应的所有需要加载的动态依赖模块的映射关系。
jsconst dynamicImportsByEntry = new Map([ [0, new Set([2, 3])], [1, new Set([4])] ]);
简要说明
- 所有 ModuleX 作为入口模块的链路中,需要加载的动态模块为 ModuleC(X → A → C) 和 ModuleE(X → B → E)。
- 所有 ModuleY 作为入口模块的链路中,需要加载的动态模块为 ModuleE(Y → E) 和 ModuleC(Y → C)。需要注意的是,ModuleE 已经作为 ModuleX 的动态依赖模块,所以这里不再重复添加。
jsfor (const [entryIndex, entry] of allEntries.entries()) { entryIndexByModule.set(entry, entryIndex); if (dynamicEntryModules.has(entry)) { dynamicEntries.add(entryIndex); } }
合并 module 为 chunk
const chunkAtoms = getChunksWithSameDependentEntries(
getModulesWithDependentEntries(
dependentEntriesByModule,
modulesInManualChunks
)
);
过滤掉自定义的
chunk
所包含的所有依赖模块从
getModulesWithDependentEntries
的实现可以发现,投喂给getChunksWithSameDependentEntries
的模块不会包含modulesInManualChunks
中的任何一个模块。也就是说自定义的chunk
所包含的所有依赖模块后续均不会重复打包在其他chunk
中。jsfunction* getModulesWithDependentEntries( dependentEntriesByModule, modulesInManualChunks ) { for (const [module, dependentEntries] of dependentEntriesByModule) { if (!modulesInManualChunks.has(module)) { yield { dependentEntries, modules: [module] }; } } }
合并模块为
chunk
在
getChunksWithSameDependentEntries
中,通过chunkSignature
(即是否拥有相同的入口模块依赖) 来判断两个module
是否可以合并,若两个模块的chunkSignature
相同,则表示这两个模块可以合并到同一个chunk
中。jsfunction getChunksWithSameDependentEntries(modulesWithDependentEntries) { const chunkModules = Object.create(null); for (const { dependentEntries, modules } of modulesWithDependentEntries) { let chunkSignature = 0n; for (const entryIndex of dependentEntries) { chunkSignature |= 1n << BigInt(entryIndex); } (chunkModules[String(chunkSignature)] ||= { dependentEntries: new Set(dependentEntries), modules: [] }).modules.push(...modules); } return Object.values(chunkModules); }
简要说明
BigInt 是 JavaScript 中的一种数据类型,用于表示任意大的整数。在 JavaScript 中,Number 类型可以表示的最大整数是 2^53 - 1(即 9007199254740991),而 BigInt 可以表示任意大的整数。chunkSignature 是
BigInt
类型,其中每一位代表着allEntries
中的一个入口模块。- ModuleX 只有一个入口模块为
0
,那么chunkSignature
为1n
(即 01) - ModuleA 只有一个入口模块为
0
,那么chunkSignature
为1n
(即 01) - ModuleB 有两个入口模块为
0
和1
,那么chunkSignature
为1n | 2n = 3n
(即 11) - ModuleH 只有一个入口模块为
4
,那么chunkSignature
为16n
(即 10000)
由上可知
ModuleX
与ModuleA
拥有相同的chunkSignature
,表示可以合并到同一个chunk
中。因此最后可以得到
jsconst chunkAtoms = [ { dependentEntries: new Set([0]), modules: [ModuleX, ModuleA] }, { dependentEntries: new Set([1]), modules: [ModuleY] }, { dependentEntries: new Set([0, 1]), modules: [ModuleB] }, { dependentEntries: new Set([3]), modules: [ModuleE] }, { dependentEntries: new Set([4]), modules: [ModuleH] } ];
- ModuleX 只有一个入口模块为
统计入口模块与关联的 chunks 的关系
通过上述处理的 chunkAtoms
和 allEntries
,可以得到 staticDependencyAtomsByEntry
结构。
const staticDependencyAtomsByEntry = [5n, 6n, 0n, 8n, 16n];
简要说明
这是对 chunkAtoms
的一次归纳整理,做了一层入口模块与所关联的 chunk
的映射关系。
ModuleX 关联的 chunks 包含了 chunkAtoms 中的第一个 chunk 和第三个 chunk。
第一个chunk:
{
dependentEntries: new Set([0]),
modules: [ModuleX, ModuleA]
}
第三个chunk:
{
dependentEntries: new Set([0, 1]),
modules: [ModuleB]
}
那么 ModuleX 的关联chunk可以表示为 5n
(即:101
)。那么就可以很容易的得出每一个入口依赖模块关联 chunk,下面将其进行可视化。
依赖关系图可视化:
统计动态依赖加载时已经加载的 chunks
alreadyLoadedAtomsByEntry
是对于动态依赖的处理方式,我们知道动态依赖的加载是异步的,那么正常情况下在加载动态依赖的时候,当前的静态模块就已经加载完成。这里就是来统计当前的动态依赖模块加载的时候,已经加载了哪些 chunks
。
const alreadyLoadedAtomsByEntry = [0n, 0n, 5n, 4n, 4n];
简要说明
通过 staticDependencyAtomsByEntry
可以得到如下的依赖关系图(入口与关联的 chunk 的映射关系)
ModuleC 是在 ModuleA 中作为动态依赖加载的,ModuleA 的依赖入口模块为 ModuleX,由上述依赖关系图可以得知,ModuleX 关联的 chunk 包含了
chunkAtoms
中的第一个 chunk 和第三个 chunk,所以alreadyLoadedAtomsByEntry[2(ModuleC的入口模块索引下标)] = 5n(即:101)
。ModuleE 是在 ModuleB 和 ModuleY 中作为动态依赖加载的,ModuleB 的依赖入口模块为 ModuleX 和 ModuleY,ModuleY 的依赖入口模块为 ModuleY。由上述依赖关系图可以得知,ModuleX 关联的 chunk 包含了
chunkAtoms
中的第一个 chunk 和第三个 chunk,ModuleY 关联的 chunk 包含了chunkAtoms
中的第二个 chunk 和第三个 chunk,所以alreadyLoadedAtomsByEntry[3(ModuleE的入口模块索引下标)] = 5n(即:101) & 6n(即:110) = 4n(即:100)
,使用&
运算符来计算两个集合的交集的原因是,ModuleE 加载的同时,交集的 chunks 必定是已经加载完成。ModuleH 是在 ModuleC 中作为动态依赖加载的,ModuleC 的依赖入口模块为 ModuleY 和 ModuleC,由上述依赖关系图可以得知,ModuleY 关联的 chunk 为
6n
,ModuleC 没有关联任何 chunk,但是由一中可知,加载 ModuleC 时,已经加载了 5n(即:101),所以alreadyLoadedAtomsByEntry[4(ModuleH的入口模块索引下标)] = 6n(即:110) & 5n(即:101) = 4n(即:100)
。
删除不必要的依赖入口模块
紧接着,removeUnnecessaryDependentEntries
方法中,通过 alreadyLoadedAtomsByEntry
来删除 chunkAtoms
中不必要的依赖入口模块。
/**
* This removes all unnecessary dynamic entries from the dependentEntries in its
* first argument if a chunk is already loaded without that entry.
*/
function removeUnnecessaryDependentEntries(
chunkAtoms,
alreadyLoadedAtomsByEntry
) {
// Remove entries from dependent entries if a chunk is already loaded without
// that entry.
let chunkMask = 1n;
for (const { dependentEntries } of chunkAtoms) {
for (const entryIndex of dependentEntries) {
if (
(alreadyLoadedAtomsByEntry[entryIndex] & chunkMask) ===
chunkMask
) {
dependentEntries.delete(entryIndex);
}
}
chunkMask <<= 1n;
}
}
也就是说,若 chunk 对应的依赖入口模块 A 加载的时候,当前这个 chunk 已经加载了,那么就删除当前这个 chunk 的依赖入口模块 A。本质上就是存在循环依赖的情况,删除重复的依赖入口模块。
// Entry Module A
console.log('ModuleA');
import('./ModuleA_B').then(() => {
console.log('Dynamic Module A_B');
});
// Entry Module B
console.log('ModuleB');
import('./ModuleA_B').then(() => {
console.log('Dynamic Module A_B');
});
// Dynamic Entry Module A_B
import './ModuleA';
console.log('Dynamic Module A_B');
构建 chunks
接下来,通过 getChunksWithSameDependentEntriesAndCorrelatedAtoms
方法来生成最终的 chunks。
function getChunksWithSameDependentEntriesAndCorrelatedAtoms(
chunkAtoms,
staticDependencyAtomsByEntry,
alreadyLoadedAtomsByEntry,
minChunkSize
) {
const chunksBySignature = Object.create(null);
const chunkByModule = new Map();
const sizeByAtom = [];
let sideEffectAtoms = 0n;
let atomMask = 1n;
for (const { dependentEntries, modules } of chunkAtoms) {
let chunkSignature = 0n;
let correlatedAtoms = -1n;
for (const entryIndex of dependentEntries) {
chunkSignature |= 1n << BigInt(entryIndex);
// Correlated atoms are the atoms that are guaranteed to be loaded as
// well when a given atom is loaded. It is the intersection of the already
// loaded modules of each chunk merged with its static dependencies.
correlatedAtoms &=
staticDependencyAtomsByEntry[entryIndex] |
alreadyLoadedAtomsByEntry[entryIndex];
}
const chunk = (chunksBySignature[String(chunkSignature)] ||= {
containedAtoms: 0n,
correlatedAtoms,
dependencies: new Set(),
dependentChunks: new Set(),
dependentEntries: new Set(dependentEntries),
modules: [],
pure: true,
size: 0
});
let atomSize = 0;
let pure = true;
for (const module of modules) {
// 模块与 chunk 的映射关系
chunkByModule.set(module, chunk);
// Unfortunately, we cannot take tree-shaking into account here because
// rendering did not happen yet, but we can detect empty modules
if (module.isIncluded()) {
pure &&= !module.hasEffects();
// we use a trivial size for the default minChunkSize to improve
// performance
atomSize += minChunkSize > 1 ? module.estimateSize() : 1;
}
}
if (!pure) {
// 记录当前模块具有副作用
sideEffectAtoms |= atomMask;
}
// 统计各个 chunk 的模块大小
sizeByAtom.push(atomSize);
chunk.containedAtoms |= atomMask;
chunk.modules.push(...modules);
// 记录当前 chunk 是否具有副作用
chunk.pure &&= pure;
// 记录当前 chunk 的模块大小
chunk.size += atomSize;
atomMask <<= 1n;
}
const chunks = Object.values(chunksBySignature);
sideEffectAtoms |= addChunkDependenciesAndGetExternalSideEffectAtoms(
chunks,
chunkByModule,
atomMask
);
return { chunks, sideEffectAtoms, sizeByAtom };
}
在 addChunkDependenciesAndGetExternalSideEffectAtoms
中,实现的功能是创建 chunk 依赖关系图,并统计外部模块的副作用。
function addChunkDependenciesAndGetExternalSideEffectAtoms(
chunks,
chunkByModule,
nextAvailableAtomMask
) {
const signatureByExternalModule = new Map();
// 统计外部依赖模块的副作用
let externalSideEffectAtoms = 0n;
for (const chunk of chunks) {
const { dependencies, modules } = chunk;
for (const module of modules) {
for (const dependency of module.getDependenciesToBeIncluded()) {
if (dependency instanceof ExternalModule) {
if (dependency.info.moduleSideEffects) {
const signature = getOrCreate(
signatureByExternalModule,
dependency,
() => {
const signature = nextAvailableAtomMask;
nextAvailableAtomMask <<= 1n;
externalSideEffectAtoms |= signature;
return signature;
}
);
chunk.containedAtoms |= signature;
chunk.correlatedAtoms |= signature;
}
} else {
// 构建 chunk 依赖关系图
const dependencyChunk = chunkByModule.get(dependency);
if (dependencyChunk && dependencyChunk !== chunk) {
chunk.dependencies.add(dependencyChunk);
dependencyChunk.dependentChunks.add(chunk);
}
}
}
}
}
return externalSideEffectAtoms;
}
优化 chunks
进一步,通过 getOptimizedChunks
方法来优化 chunks。
function compareChunkSize({ size: sizeA }, { size: sizeB }) {
return sizeA - sizeB;
}
function getPartitionedChunks(chunks, minChunkSize) {
const smallChunks = [];
const bigChunks = [];
for (const chunk of chunks) {
(chunk.size < minChunkSize ? smallChunks : bigChunks).push(chunk);
}
if (smallChunks.length === 0) {
return null;
}
// 从小到大排序
smallChunks.sort(compareChunkSize);
bigChunks.sort(compareChunkSize);
return {
big: new Set(bigChunks),
small: new Set(smallChunks)
};
}
function getOptimizedChunks(
chunks,
minChunkSize,
sideEffectAtoms,
sizeByAtom,
log
) {
timeStart('optimize chunks', 3);
const chunkPartition = getPartitionedChunks(chunks, minChunkSize);
if (!chunkPartition) {
timeEnd('optimize chunks', 3);
return chunks; // the actual modules
}
if (minChunkSize > 1) {
log(
'info',
parseAst_js.logOptimizeChunkStatus(
chunks.length,
chunkPartition.small.size,
'Initially'
)
);
}
mergeChunks(chunkPartition, minChunkSize, sideEffectAtoms, sizeByAtom);
if (minChunkSize > 1) {
log(
'info',
parseAst_js.logOptimizeChunkStatus(
chunkPartition.small.size + chunkPartition.big.size,
chunkPartition.small.size,
'After merging chunks'
)
);
}
timeEnd('optimize chunks', 3);
return [...chunkPartition.small, ...chunkPartition.big];
}
此处涉及到了 chunks 的合并算法。
chunks 的合并算法
概论介绍
- 依赖入口点: 入口模块集合 沿着依赖路径加载到 当前模块,则 入口模块集合 为 当前模块 的 依赖入口点。
- 副作用
- 指那些可能影响全局状态的操作,如全局函数调用、全局变量修改或可能抛出的错误。
- 算法将代码块分为纯粹的(无副作用)和非纯粹的(有副作用)。
- 相关副作用:
- 加载一个代码块时必然已经触发的所有副作用。
- 是所有依赖该代码块的入口点所加载的代码块的交集。
- 依赖副作用:
- 直接加载一个代码块时触发的副作用。
- 包括该代码块自身的副作用及其直接依赖的副作用。
举个例子:
在上述图中:
- X 和 Y 是入口点
- 红色的块 (A, B, D, F, G, H) 表示有副作用的代码块
- 绿色的块 (C, E) 表示纯代码块(无副作用)
现在,让我们分析一下代码块 A 的相关副作用:
A 的依赖入口点:
- A 可以通过入口点 X 或 Y 被加载。所以 A 的依赖入口点是 ({X, Y})。
确定相关副作用:
- 当通过 X 加载 A 时,会加载: A, B, C, F, G
- 当通过 Y 加载 A 时,会加载: A, D, E, F, H
相关副作用的计算:
- A 的相关副作用是在任何可能的加载路径中(X 或 Y)都会出现的有副作用的代码块的集合。
- 这是上述两种加载路径的交集: ({A, F})。
因此,A 的相关副作用是 ({A, F})。这意味着无论何时 A 被加载,F 也一定会被加载,并且它们的副作用一定会被触发。
算法目标: 尝试通过合并小代码块到其他代码块中来消除小代码块。
合并的安全性考虑: 合并必须确保不会触发不应该被触发的副作用(全局函数调用、全局变量修改或可能抛出的错误等)。
合并规则:
- 如果代码块
A
的 依赖副作用 是另一个代码块B
的 相关副作用 的 子集, 则可以合并。 - 如果代码块
A
的 依赖入口点 是代码块B
的 子集, 且A
的 依赖副作用 是B
的 相关副作用 的子集, 则可以合并。
- 如果代码块
合并算法的两个阶段
- 第一轮:
- 尝试将小代码块
A
合并到其他代码块B
中,条件是A
的 依赖入口点 是B
的子集,且A
的 依赖副作用 是B
的 相关副作用 的子集。
- 尝试将小代码块
- 第二轮:
- 对于剩余的小代码块,寻找符合规则(
3-1
)的任意合并机会,从最小的代码块开始。
- 对于剩余的小代码块,寻找符合规则(
- 第一轮:
额外考虑
合并时还需考虑避免加载过多额外代码。理想情况是小代码块的 依赖入口点 是另一个代码块 依赖入口点 的子集,这样可以确保在加载小代码块时,另一个代码块已经在内存中了。
让我们分析以下例子:
代码块定义:
- A, B, D, F: 有副作用
- C, E: 纯代码块(无副作用)
依赖关系:
- X -> A, B, C
- Y -> A, D, E
- A -> F
现在,让我们逐个分析每个代码块的相关概念:
A:
- 依赖入口点:
{X, Y}
- 相关副作用:
{A, F}
- 依赖副作用:
{A, F}
- 依赖入口点:
B:
- 依赖入口点:
{X}
- 相关副作用:
{B}
- 依赖副作用:
{B}
- 依赖入口点:
C:
- 依赖入口点:
{X}
- 相关副作用:
{}
(纯代码块) - 依赖副作用:
{}
- 依赖入口点:
D:
- 依赖入口点:
{Y}
- 相关副作用:
{D}
- 依赖副作用:
{D}
- 依赖入口点:
E:
- 依赖入口点:
{Y}
- 相关副作用:
{}
(纯代码块) - 依赖副作用:
{}
- 依赖入口点:
F:
- 依赖入口点:
{X, Y}
(因为A依赖F,而X和Y都依赖A) - 相关副作用:
{A, F}
- 依赖副作用:
{F}
- 依赖入口点:
现在,让我们考虑可能的合并:
合并F到A:
- 可以安全合并,因为F的依赖入口点({X, Y})与A的依赖入口点({X, Y})相同
- F的依赖副作用({F})是A的相关副作用({A, F})的子集
合并B到A:
- 不能安全合并,因为虽然B的依赖入口点({X})是A的依赖入口点({X, Y})的子集
- 但B的依赖副作用({B})不是A的相关副作用({A, F})的子集
合并C到B:
- 可以安全合并,因为C的依赖入口点({X})与B的依赖入口点({X})相同
- C是纯代码块,没有副作用,所以它的依赖副作用({})必定是B的相关副作用({B})的子集
合并E到D:
- 可以安全合并,原因与合并C到B类似
合并D到A:
- 不能安全合并,因为虽然D的依赖入口点({Y})是A的依赖入口点({X, Y})的子集
- 但D的依赖副作用({D})不是A的相关副作用({A, F})的子集
合并结果:
- 我们可以安全地将F合并到A中,因为F总是随A加载,且F的副作用已经包含在A的相关副作用中。
- 我们可以安全地将C合并到B中,以及将E合并到D中,因为它们是纯代码块,不会引入新的副作用。
- 我们不能将B或D合并到A中,因为这可能会在只需要A的场景下错误地触发B或D的副作用。
总结
通常情况下,算法试图在保证应用正确性(特别是关于副作用)的同时,通过智能地合并小的代码块来优化整体加载性能。