Куда делся бесполезный код? Tree-shaking свертки проекта по снижению веса

внешний интерфейс rollup.js
Куда делся бесполезный код? Tree-shaking свертки проекта по снижению веса

掘金引流终版.gif

Построить запись каталога серии столбцов

Цзо Линь, фронтенд-разработчик отдела передовых технологий WeDoctor. На волне Интернета любите жизнь и технологии.

Совет. В этой статье используется средство для создания накопительных пакетов v2.47.0.

От Webpack2.x для постепенной реализации встряхивания дерева с помощью подключаемых модулей до недавно популярного инструмента построения Vite, который также использует возможности упаковки свертки, хорошо известно, что Vue и React также упаковываются с использованием свертки, особенно когда мы создаем библиотеки функций, библиотеки инструментов и другие библиотеки.При упаковке первым выбором также является сборка! Так что же это за волшебство, которое делает роллапы такими устойчивыми? Ответ может заключаться в встряхивании деревьев!

1. Поймите встряску дерева

1. Что такое встряска деревьев?

Концепция tree-shaking существует давно, но серьезно к ней стали относиться после того, как она была реализована в роллапе.Найдите источникЕсли вам интересно, давайте начнем со свертывания и встряхивания дерева, чтобы выяснить это~~

Итак, давайте начнем с того, что такое Kangkang tree-shaking?

Tree-shaking в инструменте упаковки был ранее реализован накопительным пакетом Rich_Harris.Официальное стандартное заявление: по существу устраняет бесполезный код JS. То есть при импорте модуля не импортируется весь код всего модуля, а импортируется только тот код, который мне нужен, а бесполезный код, который мне не нужен, будет "вытряхиваться".

Позже, начиная с Webpack2, Webpack также реализует функцию tree-shaking.В частности, в проекте Webpack есть входной файл, который эквивалентен стволу дерева.В входном файле есть много зависимых модулей, которые эквивалентны ветви дерева. На самом деле, хотя наш файл функций зависит от определенного модуля, он фактически использует только некоторые функции, но не все из них. Благодаря встряхиванию деревьев неиспользуемые модули встряхиваются, так что цель удаления бесполезного кода может быть достигнута.

Из этого мы знаем, что встряхивание дерева — это способ избавиться от бесполезного кода!

Но следует отметить, что хотя древовидная тряска может устранить бесполезный код, она применима только для синтаксиса модуля ES6, потому что модули ES6 используют статический анализ для буквального анализа кода. Он ничего не может сделать с модулем динамического анализа CommonJS, который должен быть выполнен, чтобы знать, на какой модуль ссылаться, но мы можем поддерживать CommonJS для ES6 через плагины, а затем реализовать встряску дерева. решение всегда сложнее, чем трудно.

Одним словом, rollup.js по умолчанию принимает стандарт модуля ES, но его можно заставить поддерживать стандарт CommonJS с помощью плагина rollup-plugin-commonjs.В настоящее время у rollup есть очевидные преимущества в сжатии размера пакета!

2. Зачем нужен Tree-shaking?

Современные веб-приложения могут быть очень большими, особенно код JavaScript, но браузеры очень требовательны к ресурсам для обработки JavaScript. Если мы сможем удалить бесполезный код и предоставить браузеру только допустимый код для обработки, это, несомненно, значительно сократит нагрузка на браузер, и в этом нам помогает древовидная тряска.

С этой точки зрения функция встряхивания дерева относится к категории оптимизации производительности.

В конце концов, сокращение бесполезного кода JavaScript в веб-проекте означает уменьшение размера файла и времени на загрузку файловых ресурсов, тем самым улучшая взаимодействие с пользователем за счет сокращения времени ожидания пользователями открытия страницы.

2. Глубокое понимание принципа Tree-shaking

Мы видели, что суть встряхивания деревьев заключается в устранении бесполезного кода js. Так что же такое мертвый код? Как избавиться от бесполезного кода? Далее, давайте начнем с DCE, чтобы раскрыть его таинственную завесу и выяснить это~

1. DCE (устранение мертвого кода)

Мертвый код на самом деле очень распространен в нашем коде, и для устранения мертвого кода есть свой технический термин — устранение мертвого кода (DCE). На самом деле компилятор может выяснить, какие коды не влияют на вывод, а затем удалить эти коды.

Tree-shaking — это новая реализация DCE. Javascript отличается от традиционных языков программирования. В большинстве случаев javascript необходимо загрузить по сети, а затем выполнить. Чем меньше размер загружаемого файла, тем короче общее время выполнения, поэтому удаление бесполезного кода для уменьшения размера файла имеет больше смысла для javascript. Tree-shaking отличается от традиционного DCE: традиционный DCE устраняет невозможный код, а tree-shaking больше фокусируется на удалении неиспользуемого кода.

DCE

  • код не будет выполнен, недоступен
  • Результат выполнения кода не будет использован
  • Код будет влиять только на мертвые переменные, только на запись, но не на чтение

Традиционное скомпилированное пророчество состоит в том, что компилятор удаляет мертвый код из AST (абстрактного синтаксического дерева), просто поймите. Так как же встряхивание дерева устраняет бесполезный код javascript?

Tree-shaking больше связан с устранением модулей, на которые есть ссылки, но которые не используются.Этот принцип исключения опирается на модульную функцию ES6. Итак, давайте сначала рассмотрим функции модуля ES6:

ES6 Module

  • Появляется только как оператор на верхнем уровне модуля
  • Имя модуля импорта может быть только строковой константой.
  • привязка импорта неизменна

Зная эти предпосылки, попробуем проверить это кодом!

2. Устранение тряски дерева

Использование tree-shaking было представлено ранее. В следующем эксперименте index.js создается как входной файл, а сгенерированный код упаковывается в bundle.js. Используются другие файлы, такие как a.js и util.js. как упомянутые зависимые модули.

1) Исключите переменные

消除变量.png

Как видно из приведенного выше рисунка, определенные нами переменные b и переменная c не используются и не отображаются в упакованном файле.

2) Функция исключения

消除函数.png

Как видно из рисунка выше, методы функций util1() и util2(), которые только введены, но не используются, не упакованы.

3) Устранить классы

Когда только увеличивается ссылка, но не вызывается

消除类.png

Только ссылаясь на файл класса mix.js, но не используя какие-либо методы и переменные меню в фактическом коде, мы можем видеть на основе экспериментов, что устранение методов класса в новой версии свертки реализовано! ​

4) Побочные эффекты

Однако не все побочные эффекты устраняются накопительным пакетом. Ссылаясь на соответствующие статьи, накопительный пакет имеет большие преимущества в устранении побочных эффектов по сравнению с Webpack. Но накопительный пакет ничего не может сделать с побочными эффектами: ​

1) Метод класса в модуле не указан 2) Переменные, определенные в модулях, влияют на глобальные переменные.

Обратитесь к рисунку ниже, вы можете ясно видеть результаты, вы также можете перейти кПлатформа предоставлена ​​официальным сайтом rollupПопробуйте:副作用.png 副作用 2.png

резюме

Из приведенных выше результатов упаковки мы видим, что инструмент свертки очень легкий и лаконичный для упаковки.Остается только необходимый код от импорта зависимых модулей из входного файла до вывода упакованного файла пакета. Другими словами, нет необходимости добавлять дополнительную конфигурацию в накопительную упаковку, если ваш код соответствует спецификации синтаксиса ES6, может быть реализовано встряхивание дерева. Хороший!

Тогда встряска дерева в этом процессе упаковки, вероятно, может быть понята как имеющая следующие две ключевые реализации:

  • Введение модуля ES6 подвергается статическому анализу, что позволяет правильно определить, какой код загружается во время компиляции.
  • Проанализируйте ход программы, определите, какие переменные используются, на которые ссылаются, и упакуйте код.

Ядро tree-shaking включено в процесс анализа потока программы: на основе области видимости формируются записи объектов для функций или глобальных объектов в процессе AST, а затем сопоставляются флаги импорта во всей сформированной цепочке областей видимости объекта, и, наконец, упаковывается только совпадающий код, а код, который не используется сопоставлением, удаляется. ​

Но при этом следует обратить внимание на два момента: ​

  • Пишите как можно меньше кода с побочными эффектами, например, операции, влияющие как можно сильнее на глобальные переменные;
  • Когда создается экземпляр ссылочного класса и вызывается метод экземпляра, также возникают побочные эффекты, с которыми не может справиться свертка.

Так как же генерация записей и идентификация соответствия реализованы в процессе анализа потока программы?

Далее я познакомлю вас с исходным кодом и узнаю!

3. Процесс реализации древовидной тряски

Перед реализацией встряхивания дерева в процессе синтаксического анализа мы должны сначала понять два предварительных условия:

  • Tree-shaking в свертывании использует acorn для обхода и анализа абстрактного синтаксического дерева AST.Acorn и babel имеют те же функции, но acorn более легкий.До этого также необходимо понимать рабочий процесс AST;
  • rollup использует инструмент magic-string для манипулирования строками и создания исходных карт.

流程图.png

Давайте начнем с исходного кода и подробно опишем конкретный процесс в соответствии с основным принципом встряхивания дерева:

  • На этапе rollup() исходный код анализируется, генерируется AST-дерево, просматривается каждый узел в AST-дереве и определяется, следует ли включать (отмечать, чтобы избежать повторной упаковки), если да, то отмечать, а затем генерировать куски и, наконец, экспортировать.
  • На этапе generate()/write() сбор кода выполняется в соответствии с метками, сделанными на этапе rollup(), и, наконец, генерируется фактический код.

Получите исходный код для его отладки~

// perf-debug.js
loadConfig().then(async config => // 获取收集配置
	(await rollup.rollup(config)).generate( 
		Array.isArray(config.output) ? config.output[0] : config.output
	)
);

Этот фрагмент кода может больше всего беспокоить при отладке.В одном предложении ввод упаковывается как вывод, что соответствует описанному выше процессу.

export async function rollupInternal(
	rawInputOptions: GenericConfigObject, // 传入参数配置
	watcher: RollupWatcher | null
): Promise<RollupBuild> {
	const { options: inputOptions, unsetOptions: unsetInputOptions } = await getInputOptions(
		rawInputOptions,
		watcher !== null
	);
	initialiseTimers(inputOptions);

	const graph = new Graph(inputOptions, watcher); // graph 包含入口以及各种依赖的相互关系,操作方法,缓存等,在实例内部实现 AST 转换,是 rollup 的核心

	const useCache = rawInputOptions.cache !== false; // 从配置中取是否使用缓存
	delete inputOptions.cache;
	delete rawInputOptions.cache;

	timeStart('BUILD', 1);

	try {
    // 调用插件驱动器方法,调用插件和提供插件环境上下文等
		await graph.pluginDriver.hookParallel('buildStart', [inputOptions]); 
		await graph.build();
	} catch (err) {
		const watchFiles = Object.keys(graph.watchFiles);
		if (watchFiles.length > 0) {
			err.watchFiles = watchFiles;
		}
		await graph.pluginDriver.hookParallel('buildEnd', [err]);
		await graph.pluginDriver.hookParallel('closeBundle', []);
		throw err;
	}

	await graph.pluginDriver.hookParallel('buildEnd', []);

	timeEnd('BUILD', 1);

	const result: RollupBuild = {
		cache: useCache ? graph.getCache() : undefined,
		closed: false,
		async close() {
			if (result.closed) return;

			result.closed = true;

			await graph.pluginDriver.hookParallel('closeBundle', []);
		},
		// generate - 将遍历标记处理过作为输出的抽象语法树生成新的代码
		async generate(rawOutputOptions: OutputOptions) {
			if (result.closed) return error(errAlreadyClosed());
      // 第一个参数 isWrite 为 false
			return handleGenerateWrite(
				false,
				inputOptions,
				unsetInputOptions,
				rawOutputOptions as GenericConfigObject,
				graph
			);
		},
		watchFiles: Object.keys(graph.watchFiles),
		// write - 将遍历标记处理过作为输出的抽象语法树生成新的代码
		async write(rawOutputOptions: OutputOptions) {
			if (result.closed) return error(errAlreadyClosed());
      // 第一个参数 isWrite 为 true
			return handleGenerateWrite(
				true,
				inputOptions,
				unsetInputOptions,
				rawOutputOptions as GenericConfigObject,
				graph
			);
		}
	};
	if (inputOptions.perf) result.getTimings = getTimings;
	return result;
}

Конечно, из этого фрагмента кода ничего не видно.Давайте интерпретируем исходный код, чтобы разобраться в процессе упаковки накопительных пакетов и изучить конкретную реализацию tree-shaking, чтобы упростить его.грубыйЧтобы непосредственно понять процесс упаковки, мы проигнорируем конфигурацию подключаемого модуля в исходном коде и проанализируем только основной процесс реализации функционального процесса.

1. Модульный анализ

Получить абсолютный путь к файлу

Адрес файла разрешается методом resolveId(), и получается абсолютный путь к файлу, получение абсолютного пути — наша основная цель, более детальная обработка здесь разбираться не будет.

export async function resolveId(
	source: string,
	importer: string | undefined,
	preserveSymlinks: boolean,) {
	// 不是以 . 或 / 开头的非入口模块在此步骤被跳过
	if (importer !== undefined && !isAbsolute(source) && source[0] !== '.') return null;
  // 调用 path.resolve,将合法文件路径转为绝对路径
	return addJsExtensionIfNecessary(
		importer ? resolve(dirname(importer), source) : resolve(source),
		preserveSymlinks
	);
}

// addJsExtensionIfNecessary() 实现
function addJsExtensionIfNecessary(file: string, preserveSymlinks: boolean) {
	let found = findFile(file, preserveSymlinks);
	if (found) return found;
	found = findFile(file + '.mjs', preserveSymlinks);
	if (found) return found;
	found = findFile(file + '.js', preserveSymlinks);
	return found;
}

// findFile() 实现
function findFile(file: string, preserveSymlinks: boolean): string | undefined {
	try {
		const stats = lstatSync(file);
		if (!preserveSymlinks && stats.isSymbolicLink())
			return findFile(realpathSync(file), preserveSymlinks);
		if ((preserveSymlinks && stats.isSymbolicLink()) || stats.isFile()) {
			const name = basename(file);
			const files = readdirSync(dirname(file));

			if (files.indexOf(name) !== -1) return file;
		}
	} catch {
		// suppress
	}
}

стадия свертки()

На этапе rollup() выполняется много работы, включая сбор и стандартизацию конфигураций, анализ файлов и компиляцию исходного кода для создания AST, создание модулей и анализ зависимостей и, наконец, создание фрагментов. Чтобы выяснить, где именно работает встряска дерева, нам нужно проанализировать код для более внутренней обработки.

Во-первых, найдите определение его модуля, начиная с абсолютного пути к входному файлу, и получите все операторы зависимостей входного модуля и верните все содержимое.

private async fetchModule(
	{ id, meta, moduleSideEffects, syntheticNamedExports }: ResolvedId,
	importer: string | undefined, // 导入此模块的引用模块
	isEntry: boolean // 是否入口路径
): Promise<Module> { 
  ...
   // 创建 Module 实例
	const module: Module = new Module(
		this.graph, // Graph 是全局唯一的图,包含入口以及各种依赖的相互关系,操作方法,缓存等
		id,
		this.options,
		isEntry,
		moduleSideEffects, // 模块副作用
		syntheticNamedExports,
		meta
	);
	this.modulesById.set(id, module);
	this.graph.watchFiles[id] = true;
	await this.addModuleSource(id, importer, module);
	await this.pluginDriver.hookParallel('moduleParsed', [module.info]);
	await Promise.all([
	  // 处理静态依赖
		this.fetchStaticDependencies(module),
		// 处理动态依赖
		this.fetchDynamicDependencies(module)
	]);
	module.linkImports();
  // 返回当前模块
	return module;
}

СоответственноfetchStaticDependencies(module),а такжеfetchDynamicDependencies(module)Зависимые модули далее обрабатываются и возвращают содержимое зависимых модулей.

private fetchResolvedDependency(
	source: string,
	importer: string,
	resolvedId: ResolvedId
): Promise<Module | ExternalModule> {
	if (resolvedId.external) {
		const { external, id, moduleSideEffects, meta } = resolvedId;
		if (!this.modulesById.has(id)) {
			this.modulesById.set(
				id,
				new ExternalModule( // 新建外部 Module 实例
					this.options,
					id,
					moduleSideEffects,
					meta,
					external !== 'absolute' && isAbsolute(id)
				)
			);
		}

		const externalModule = this.modulesById.get(id);
		if (!(externalModule instanceof ExternalModule)) {
			return error(errInternalIdCannotBeExternal(source, importer));
		}
	  // 返回依赖的模块内容
		return Promise.resolve(externalModule);
	} else {
    // 存在导入此模块的外部引用,则递归获取这个入口模块所有的依赖语句
		return this.fetchModule(resolvedId, importer, false);
	}
}

Каждый файл является модулем, и каждый модуль будет иметь экземпляр модуля. В экземпляре модуля код файла модуля просматривается и анализируется в синтаксическом дереве AST с помощью метода синтаксического анализа желудя.

const ast = this.acornParser.parse(code, {
	...(this.options.acorn as acorn.Options),
	...options
});

Наконец, проанализируйте и установите источник в текущий модуль, завершите преобразование из файла в модуль и проанализируйте узел дерева ES и различные типы синтаксических деревьев, содержащихся в нем.

setSource({
	alwaysRemovedCode,
	ast,
	code,
	customTransformCache,
	originalCode,
	originalSourcemap,
	resolvedIds,
	sourcemapChain,
	transformDependencies,
	transformFiles,
	...moduleOptions
}: TransformModuleJSON & {
	alwaysRemovedCode?: [number, number][];
	transformFiles?: EmittedFile[] | undefined;
}) {
	this.info.code = code;
	this.originalCode = originalCode;
	this.originalSourcemap = originalSourcemap;
	this.sourcemapChain = sourcemapChain;
	if (transformFiles) {
		this.transformFiles = transformFiles;
	}
	this.transformDependencies = transformDependencies;
	this.customTransformCache = customTransformCache;
	this.updateOptions(moduleOptions);

	timeStart('generate ast', 3);

	this.alwaysRemovedCode = alwaysRemovedCode || [];
	if (!ast) {
		ast = this.tryParse();
	}
	this.alwaysRemovedCode.push(...findSourceMappingURLComments(ast, this.info.code));

	timeEnd('generate ast', 3);

	this.resolvedIds = resolvedIds || Object.create(null);

	this.magicString = new MagicString(code, {
		filename: (this.excludeFromSourcemap ? null : fileName)!, // 不包括 sourcemap 中的辅助插件
		indentExclusionRanges: []
	});
	for (const [start, end] of this.alwaysRemovedCode) {
		this.magicString.remove(start, end);
	}

	timeStart('analyse ast', 3);
  // ast 上下文环境,包装一些方法,比如动态导入、导出等,东西很多,大致看一看
	this.astContext = {
		addDynamicImport: this.addDynamicImport.bind(this), // 动态导入
		addExport: this.addExport.bind(this),
		addImport: this.addImport.bind(this),
		addImportMeta: this.addImportMeta.bind(this),
		code,
		deoptimizationTracker: this.graph.deoptimizationTracker,
		error: this.error.bind(this),
		fileName,
		getExports: this.getExports.bind(this),
		getModuleExecIndex: () => this.execIndex,
		getModuleName: this.basename.bind(this),
		getReexports: this.getReexports.bind(this),
		importDescriptions: this.importDescriptions,
		includeAllExports: () => this.includeAllExports(true), // include 相关方法标记决定是否 tree-shaking
		includeDynamicImport: this.includeDynamicImport.bind(this), // include...
		includeVariableInModule: this.includeVariableInModule.bind(this), // include...
		magicString: this.magicString,
		module: this,
		moduleContext: this.context,
		nodeConstructors,
		options: this.options,
		traceExport: this.getVariableForExportName.bind(this),
		traceVariable: this.traceVariable.bind(this),
		usesTopLevelAwait: false,
		warn: this.warn.bind(this)
	};

	this.scope = new ModuleScope(this.graph.scope, this.astContext);
	this.namespace = new NamespaceVariable(this.astContext, this.info.syntheticNamedExports);
  // 实例化 Program,将 ast 上下文环境赋给当前模块的 ast 属性上
	this.ast = new Program(ast, { type: 'Module', context: this.astContext }, this.scope);
	this.info.ast = ast;

	timeEnd('analyse ast', 3);
}

2. Может ли модуль разметки работать с Tree-shaking

Продолжайте обрабатывать текущий модуль и вводите модуль и узел дерева es в соответствии с состоянием isExecuted и соответствующей конфигурацией treeshakingy.Если isExecuted равно true, это означает, что этот модуль был добавлен в результат, и он не нужен. будут добавлены снова в будущем Наконец, все необходимые модули собраны в соответствии с isExecuted.Таким образом достигается встряска дерева.

// 以标记声明语句为例,includeVariable()、includeAllExports()方法不一一列出
private includeStatements() {
	for (const module of [...this.entryModules, ...this.implicitEntryModules]) {
		if (module.preserveSignature !== false) {
			module.includeAllExports(false);
		} else {
			markModuleAndImpureDependenciesAsExecuted(module);
		}
	}
	if (this.options.treeshake) {
		let treeshakingPass = 1;
		do {
			timeStart(`treeshaking pass ${treeshakingPass}`, 3);
			this.needsTreeshakingPass = false;
			for (const module of this.modules) {
        // 根据 isExecuted 进行标记
				if (module.isExecuted) {
					if (module.info.hasModuleSideEffects === 'no-treeshake') {
						module.includeAllInBundle();
					} else {
						module.include(); // 标记
					}
				}
			}
			timeEnd(`treeshaking pass ${treeshakingPass++}`, 3);
		} while (this.needsTreeshakingPass);
	} else {
		for (const module of this.modules) module.includeAllInBundle();
	}
	for (const externalModule of this.externalModules) externalModule.warnUnusedImports();
	for (const module of this.implicitEntryModules) {
		for (const dependant of module.implicitlyLoadedAfter) {
			if (!(dependant.info.isEntry || dependant.isIncluded())) {
				error(errImplicitDependantIsNotIncluded(dependant));
			}
		}
	}
}

module.include включает внутренний узел дерева ES.Поскольку начальное включение NodeBase является ложным, существует второе условие оценки: имеет ли текущий узел побочные эффекты. Имеет ли это побочные эффекты — реализация различных подклассов узлов, наследуемых от NodeBase, и влияет ли это на общую ситуацию. Различные типы узлов es в сводке реализуют разные реализации hasEffects. В процессе непрерывной оптимизации обрабатываются побочные эффекты ссылок на классы, а классы, на которые ссылаются, но не используются, исключаются. Это можно комбинировать с встряхиванием дерева в главе 2. Исключить дальнейшее понимание.

include(): void { /  include()实现
	const context = createInclusionContext();
	if (this.ast!.shouldBeIncluded(context)) this.ast!.include(context, false);
}

3. метод treeshakeNode()

В исходном коде есть такой метод, как treeshakeNode() для удаления бесполезного кода, и он также четко отмечен при вызове --- для предотвращения повторного объявления одной и той же переменной/узла и для отметки того, был ли код узла изменен. включен включенным, в случае встряхивания дерева, а также предоставляет метод removeAnnotations() для удаления избыточного кода аннотаций.

// 消除无用节点
export function treeshakeNode(node: Node, code: MagicString, start: number, end: number) {
	code.remove(start, end);
	if (node.annotations) {
		for (const annotation of node.annotations) {
			if (!annotation.comment) {
				continue;
			}
			if (annotation.comment.start < start) {
				code.remove(annotation.comment.start, annotation.comment.end);
			} else {
				return;
			}
		}
	}
}
// 消除注释节点
export function removeAnnotations(node: Node, code: MagicString) {
	if (!node.annotations && node.parent.type === NodeType.ExpressionStatement) {
		node = node.parent as Node;
	}
	if (node.annotations) {
		for (const annotation of node.annotations.filter((a) => a.comment)) {
			code.remove(annotation.comment!.start, annotation.comment!.end);
		}
	}
}

Время вызова метода treeshakeNode() очень важно! встряхивание дерева и рекурсивный рендеринг перед рендерингом.

render(code: MagicString, options: RenderOptions, nodeRenderOptions?: NodeRenderOptions) {
		const { start, end } = nodeRenderOptions as { end: number; start: number };
		const declarationStart = getDeclarationStart(code.original, this.start);

		if (this.declaration instanceof FunctionDeclaration) {
			this.renderNamedDeclaration(
				code,
				declarationStart,
				'function',
				'(',
				this.declaration.id === null,
				options
			);
		} else if (this.declaration instanceof ClassDeclaration) {
			this.renderNamedDeclaration(
				code,
				declarationStart,
				'class',
				'{',
				this.declaration.id === null,
				options
			);
		} else if (this.variable.getOriginalVariable() !== this.variable) {
			// tree-shaking 以防止重复声明变量
			treeshakeNode(this, code, start, end);
			return;
      // included 标识做 tree-shaking
		} else if (this.variable.included) {
			this.renderVariableDeclaration(code, declarationStart, options);
		} else {
			code.remove(this.start, declarationStart);
			this.declaration.render(code, options, {
				isCalleeOfRenderedParent: false,
				renderedParentType: NodeType.ExpressionStatement
			});
			if (code.original[this.end - 1] !== ';') {
				code.appendLeft(this.end, ';');
			}
			return;
		}
		this.declaration.render(code, options);
	}

Подобных мест несколько, и в этих местах сияет древотрясение!

// 果然我们又看到了 included
...
if (!node.included) {
  treeshakeNode(node, code, start, end);
  continue;
}
...
if (currentNode.included) {
	currentNodeNeedsBoundaries
		 ? currentNode.render(code, options, {
	  	end: nextNodeStart,
		  start: currentNodeStart
		 })
   : currentNode.render(code, options);
} else {
   treeshakeNode(currentNode, code, currentNodeStart!, nextNodeStart);
}
...

4. Сгенерировать код (строку) через чанки и записать в файл

На этапе generate()/write() обработанный и сгенерированный код записывается в файл, а метод handleGenerateWrite() внутренне генерирует экземпляр пакета для обработки.

async function handleGenerateWrite(...) {
  ...
	// 生成 Bundle 实例,这是一个打包对象,包含所有的模块信息
	const bundle = new Bundle(outputOptions, unsetOptions, inputOptions, outputPluginDriver, graph);
	// 调用实例 bundle 的 generate 方法生成代码
	const generated = await bundle.generate(isWrite);
	if (isWrite) {
		if (!outputOptions.dir && !outputOptions.file) {
			return error({
				code: 'MISSING_OPTION',
				message: 'You must specify "output.file" or "output.dir" for the build.'
			});
		}
		await Promise.all(
		   // 这里是关键:通过 chunkId 生成代码并写入文件
			Object.keys(generated).map(chunkId => writeOutputFile(generated[chunkId], outputOptions))
		);
		await outputPluginDriver.hookParallel('writeBundle', [outputOptions, generated]);
	}
	return createOutput(generated);
}

резюме

Одним словом, это: начиная с файла ввода, выясняя все переменные, которые он читает, выясняя, где переменная определена, включая оператор определения и отбрасывая нерелевантный код, мы получаем то, что мы хотим желаемый результат.

Суммировать

Эта статья основана на интерпретации принципа древовидности в процессе упаковки исходного кода свертки.На самом деле можно обнаружить, что для простого процесса упаковки исходный код не выполняет дополнительных загадочных операций над кодом, но использует теги обхода только для сбора и слияния.Упакуйте вывод собранного кода и включите помеченный узел treeshakeNode, чтобы избежать дублирования объявлений.

Конечно, самое главное — это внутренний статический анализ и сбор зависимостей, этот процесс более сложен в управлении, но суть на самом деле в обходе узлов: поиске переменных, от которых зависит текущий узел, переменных, к которым обращаются, и объявления операторы этих переменных.

Как легковесный и быстрый инструмент упаковки, rollup имеет большие преимущества в плане удобства упаковки библиотек инструментов функций. Благодаря тому, что он предпочитает обработку кода, исходный код намного легче, чем Webpack, но я все же думаю, что чтение исходного кода — это скучный процесс...

но! Если это просто для понимания принципа, вы можете сначала сосредоточиться только на основном процессе кода и поместить детали углов в конце, что может улучшить приятное впечатление от чтения и ускорить темп. исходный код! ​

использованная литература

高血压.gif