graph
graph 直接翻译过来就是“图”,但是放在 rollup 中我更愿意把它称作为 rollup 的“依赖图谱”。因为 graph 保存了源代码中的各个 module 信息(包括源代码和模块 id 等等),将 module 按照引入顺序进行排序。
在介绍 graph 之前我们先简单回顾下 getInputOptions 方法是如何合并用户传入的 inputOptions 的。在 src/rollup/rollup.ts 文件中:
const { options: inputOptions, unsetOptions: unsetInputOptions } = await getInputOptions(
rawInputOptions,
watcher !== null
);
async function getInputOptions(
rawInputOptions: InputOptions,
watchMode: boolean
): Promise<{ options: NormalizedInputOptions, unsetOptions: Set<string> }> {
if (!rawInputOptions) {
throw new Error('You must supply an options object to rollup');
}
const rawPlugins = getSortedValidatedPlugins(
'options',
await normalizePluginOption(rawInputOptions.plugins)
);
const { options, unsetOptions } = await normalizeInputOptions(
await rawPlugins.reduce(applyOptionHook(watchMode), Promise.resolve(rawInputOptions))
);
normalizePlugins(options.plugins, ANONYMOUS_PLUGIN_PREFIX);
return { options, unsetOptions };
}
function applyOptionHook(watchMode: boolean) {
return async (inputOptions: Promise<RollupOptions>, plugin: Plugin): Promise<InputOptions> => {
const handler = 'handler' in plugin.options! ? plugin.options.handler : plugin.options!;
return (
(await handler.call({ meta: { rollupVersion, watchMode } }, await inputOptions)) ||
inputOptions
);
};
}
function normalizePlugins(plugins: readonly Plugin[], anonymousPrefix: string): void {
for (const [index, plugin] of plugins.entries()) {
if (!plugin.name) {
plugin.name = `${anonymousPrefix}${index + 1}`;
}
}
}
getInputOptions 方法首先会检查用户是否传入了 InputOptions,如果没有则会抛出一个错误提示:'You must supply an options object to rollup'。这句话的意思是告诉你必须要提供一个 options 给 rollup。因为 rollup 打包需要知道入口文件是哪个。
接着设置 plugin.name,调用 normalizeInputOptions 方法合并配置信息。最后返回了 options 和 unsetOptions。
normalizeInputOptions 方法就是最终合并配置的地方, 代码在 src/utils/options/normalizeInputOptions.ts 目录下:
export async function normalizeInputOptions(config: InputOptions): Promise<{
options: NormalizedInputOptions;
unsetOptions: Set<string>;
}> {
// These are options that may trigger special warnings or behaviour later
// if the user did not select an explicit value
const unsetOptions = new Set<string>();
const context = config.context ?? 'undefined';
const onwarn = getOnwarn(config);
const strictDeprecations = config.strictDeprecations || false;
const maxParallelFileOps = getmaxParallelFileOps(config, onwarn, strictDeprecations);
const options: NormalizedInputOptions & InputOptions = {
acorn: getAcorn(config) as unknown as NormalizedInputOptions['acorn'],
acornInjectPlugins: getAcornInjectPlugins(config),
cache: getCache(config),
context,
experimentalCacheExpiry: config.experimentalCacheExpiry ?? 10,
external: getIdMatcher(config.external),
inlineDynamicImports: getInlineDynamicImports(config, onwarn, strictDeprecations),
input: getInput(config),
makeAbsoluteExternalsRelative: config.makeAbsoluteExternalsRelative ?? 'ifRelativeSource',
manualChunks: getManualChunks(config, onwarn, strictDeprecations),
maxParallelFileOps,
maxParallelFileReads: maxParallelFileOps,
moduleContext: getModuleContext(config, context),
onwarn,
perf: config.perf || false,
plugins: await normalizePluginOption(config.plugins),
preserveEntrySignatures: config.preserveEntrySignatures ?? 'exports-only',
preserveModules: getPreserveModules(config, onwarn, strictDeprecations),
preserveSymlinks: config.preserveSymlinks || false,
shimMissingExports: config.shimMissingExports || false,
strictDeprecations,
treeshake: getTreeshake(config)
};
//...
return { options, unsetOptions };
}
弄清楚了合并配置的过程,接着我们继续分析 new Graph(inputOptions, watcher) 逻辑。代码在 src/Graph.ts :
export default class Graph {
readonly acornParser: typeof acorn.Parser;//使用acornParser解析ast
readonly cachedModules = new Map<string, ModuleJSON>();//缓存的modules,提升性能
readonly deoptimizationTracker = new PathTracker();
entryModules: Module[] = []; //入口模块
readonly fileOperationQueue: Queue;
readonly moduleLoader: ModuleLoader; //模块加载器
readonly modulesById = new Map<string, Module | ExternalModule>(); //使用Map结构来保存modules
needsTreeshakingPass = false;
phase: BuildPhase = BuildPhase.LOAD_AND_PARSE; // 构建的 phase 标志
readonly pluginDriver: PluginDriver; // 插件驱动
readonly scope = new GlobalScope(); // 作用域
readonly watchFiles: Record<string, true> = Object.create(null);
watchMode = false;
private readonly externalModules: ExternalModule[] = [];//外部的modules
private implicitEntryModules: Module[] = [];//隐式入口模块
private modules: Module[] = []; //保存的模块
private declare pluginCache?: Record<string, SerializablePluginCache>;
constructor(private readonly options: NormalizedInputOptions, watcher: RollupWatcher | null) {
//初始化的时候option.cache = undefined
if (options.cache !== false) {
if (options.cache?.modules) {
for (const module of options.cache.modules) this.cachedModules.set(module.id, module);
}
this.pluginCache = options.cache?.plugins || Object.create(null);
// increment access counter
for (const name in this.pluginCache) {
const cache = this.pluginCache[name];
for (const value of Object.values(cache)) value[0]++;
}
}
//...
//初始化插件
this.pluginDriver = new PluginDriver(this, options, options.plugins, this.pluginCache);
//使用acorn解析ast,并且扩展用户自定义配置
this.acornParser = acorn.Parser.extend(...(options.acornInjectPlugins as any[]));
//初始化 moduleLoader
this.moduleLoader = new ModuleLoader(this, this.modulesById, this.options, this.pluginDriver);
//初始化任务队列
this.fileOperationQueue = new Queue(options.maxParallelFileOps);
}
//...
}
new Graph()
初始化 Graph 的时候定义了非常多的属性和方法。再看到 constructor 内部主要做了以下几件事情:
- 初始化 this.pluginCache
- 初始化插件 this.pluginDriver
- 初始化 this.acornParser
- 初始化模块加载器 this.moduleLoader
- 初始化任务队列 this.fileOperationQueue
完成一系列的初始化工作之后在 rollupInternal 方法内部紧接着执行了如下两行代码:
await graph.pluginDriver.hookParallel('buildStart', [inputOptions]);
//...
await graph.build();
graph.pluginDriver.hookParallel('buildStart', [inputOptions]) 是调用 buildStart 钩子。为了保持主线逻辑的纯粹,我们这一章节忽略插件的逻辑。稍后我们会在专门的插件系统章节进行详细的讲解。
接着来到 graph.build(); 的方法:
//...
async build(): Promise<void> {
//...
/**
* generateModuleGraph 方法主要做了以下事情:
* 1、通过 input 配置找出入口模块(entryModules)
* 2、从 entryModules 分析、读取所有依赖模块并生成 Module 实例
* 3、设置各模块的 dependences 和依赖模块的 importers
* 4、创建全局作用域、模块作用域
* 5、添加 watchFiles
*/
await this.generateModuleGraph();
//...
/**
* sortModules主要做了两件事情:
* 1、按照引入顺序排序模块
* 2、绑定node.variable。即变量的引用信息
*/
this.sortModules();
//...
//遍历所有的ast.node并且修改node.included的值
this.includeStatements();
//...
}
//...
总结
graph.build 方法内部主要做了三件事情:
- 调用 this.generateModuleGraph 方法生成依赖图谱。具体是通过 options.input 的值找出入口模块(entryModules),模块 id 就是文件的绝对路径。从 entryModules 分析、读取所有依赖模块(依赖模块实际上是根据 import 语句来判断)并生成 Module 实例、设置各模块的 dependences 和依赖模块的 importers、创建全局作用域、模块作用域、添加 watchFiles 等等
- 调用 this.sortModules 方法对模块进行排序。绑定 ast.node.variable,即变量的引用信息。
- 调用 this.includeStatements 方法对所有的 ast.node 进行 included 标记。如果 included = true 代表该节点会被最终的 bundle 包含进来,否则将会使用 MagicString 对其进行删除或者替换操作。这个过程就是 tree-shaking 的工作原理。
当然,判断 included 的值的时候需要判断该节点是否是 hasEffects 。判断 hasEffects 的逻辑通俗的来讲就是判断节点是否调用全局的方法(例如 console.log)或者修改了全局变量或者其他变量或者方法等等。
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 源码分析文章)