rollup 源码解析 - watch 监听

文章目录

  • rollup watch 实现流程
  • watch
    • WatchEmitter 实现
  • watchInternal
    • Watcher 管理整个 watch 阶段
    • Task 运行任务
    • FileWatcher 实现文件监听

rollup watch 实现流程

  • 每一个配置了 watch 的配置项都会变成一个 Task 任务,每个任务通过 FileWatcher 即 chokidar 进行监听,需要监听的文件依赖有两种
    • 一种是文件自身 import 的依赖,会被放进 dependencies 属性里
    • 一种是文件在被插件处理的过程中通过 this.addWatchFile 时,watch 的文件会放进该模块的 transformDependencies 属性里
      • 插件里调用 this.emitFile 生成的文件会放进该模块的 transformFiles 属性里
  • 每个 Task 通过 Watcher 进行管理

rollup 打包结果其中一个文件数据结构:

{
  "assertions": {},
  "ast": {
    // 当前模块 ast 
  },
  "code": "import _createClass from \"@babel/runtime/helpers/createClass\";\nimport _classCallCheck from \"@babel/runtime/helpers/classCallCheck\";\nimport _defineProperty from \"@babel/runtime/helpers/defineProperty\";\nimport \"core-js/modules/es7.array.includes.js\";\nimport a from \"a-test\";\nimport foo from \"./foo.js\";\nimport packageJosn from \"./package.json\";\nvar b = require(\"path\");\nvar fn = function fn() {\n  console.log(\"index fn243\", a);\n  console.log(packageJosn.version);\n  console.log(b);\n};\nfn();\nvar A = /*#__PURE__*/_createClass(function A() {\n  _classCallCheck(this, A);\n  _defineProperty(this, \"a\", void 0);\n  this.a = \"ww2\";\n});\nvar aa = new A();\nvar res = [];\nconsole.log(res.includes(\"ff\"));\nconsole.log(foo);\nexport default fn;",
  "customTransformCache": false,
  "dependencies": [
      "/Users/xxx/Desktop/demo/node_modules/@babel/runtime/helpers/esm/createClass.js",
      "/Users/xxx/Desktop/demo/node_modules/@babel/runtime/helpers/esm/classCallCheck.js",
      "/Users/xxx/Desktop/demo/node_modules/@babel/runtime/helpers/esm/defineProperty.js",
      "core-js/modules/es7.array.includes.js",
      "a-test",
      "/Users/xxx/Desktop/demo/foo.js",
      "/Users/xxx/Desktop/demo/package.json"
  ],
  "id": "/Users/xxx/Desktop/demo/index.js",
  "meta": {
      "commonjs": {
          "hasDefaultExport": true,
          "isCommonJS": false
      }
  },
  "moduleSideEffects": true,
  "originalCode": "import a from \"a-test\";\nimport foo from \"./foo.js\";\nimport packageJosn from \"./package.json\";\nconst b = require(\"path\");\n\nconst fn = () => {\n  console.log(\"index fn243\", a);\n  console.log(packageJosn.version);\n  console.log(b);\n};\nfn();\n\nclass A {\n  a;\n  constructor() {\n    this.a = \"ww2\";\n  }\n}\n\nconst aa = new A();\n\nlet res = [];\nconsole.log(res.includes(\"ff\"));\n\nconsole.log(foo);\n\nexport default fn;\n",
  "originalSourcemap": null,
  "resolvedIds": {
      "./package.json": {
          "assertions": {},
          "external": false,
          "id": "/Users/xxx/Desktop/demo/package.json",
          "meta": {},
          "moduleSideEffects": true,
          "resolvedBy": "node-resolve",
          "syntheticNamedExports": false
      },
      "core-js/modules/es7.array.includes.js": {
          "assertions": {},
          "external": true,
          "id": "core-js/modules/es7.array.includes.js",
          "meta": {},
          "moduleSideEffects": true,
          "resolvedBy": "rollup",
          "syntheticNamedExports": false
      },
      "./foo.js": {
          "assertions": {},
          "external": false,
          "id": "/Users/xxx/Desktop/demo/foo.js",
          "meta": {},
          "moduleSideEffects": true,
          "resolvedBy": "node-resolve",
          "syntheticNamedExports": false
      },
      "a-test": {
          "assertions": {},
          "external": true,
          "id": "a-test",
          "meta": {},
          "moduleSideEffects": true,
          "resolvedBy": "rollup",
          "syntheticNamedExports": false
      },
      "@babel/runtime/helpers/defineProperty": {
          "assertions": {},
          "external": false,
          "id": "/Users/xxx/Desktop/demo/node_modules/@babel/runtime/helpers/esm/defineProperty.js",
          "meta": {},
          "moduleSideEffects": true,
          "resolvedBy": "node-resolve",
          "syntheticNamedExports": false
      },
      "@babel/runtime/helpers/createClass": {
          "assertions": {},
          "external": false,
          "id": "/Users/xxx/Desktop/demo/node_modules/@babel/runtime/helpers/esm/createClass.js",
          "meta": {},
          "moduleSideEffects": true,
          "resolvedBy": "node-resolve",
          "syntheticNamedExports": false
      },
      "@babel/runtime/helpers/classCallCheck": {
          "assertions": {},
          "external": false,
          "id": "/Users/xxx/Desktop/demo/node_modules/@babel/runtime/helpers/esm/classCallCheck.js",
          "meta": {},
          "moduleSideEffects": true,
          "resolvedBy": "node-resolve",
          "syntheticNamedExports": false
      }
  },
  "sourcemapChain": [
      {
          "version": 3,
          "names": [
              "a",
              "foo",
              "packageJosn",
              "b",
              "require",
              "fn",
              "console",
              "log",
              "version",
              "A",
              "_createClass",
              "_classCallCheck",
              "_defineProperty",
              "aa",
              "res",
              "includes"
          ],
          "sources": [
              "index.js"
          ],
          "sourcesContent": [
              "import a from \"a-test\";\nimport foo from \"./foo.js\";\nimport packageJosn from \"./package.json\";\nconst b = require(\"path\");\n\nconst fn = () => {\n  console.log(\"index fn243\", a);\n  console.log(packageJosn.version);\n  console.log(b);\n};\nfn();\n\nclass A {\n  a;\n  constructor() {\n    this.a = \"ww2\";\n  }\n}\n\nconst aa = new A();\n\nlet res = [];\nconsole.log(res.includes(\"ff\"));\n\nconsole.log(foo);\n\nexport default fn;\n"
          ],
          "mappings": [
            // ...
          ],
   
  "syntheticNamedExports": false,
  "transformDependencies": [
      "./index2.ts"
  ],
  "transformFiles": [
      {
          "type": "asset",
          "source": "export default what"
      }
  ]
}

watch

  • 当 rollup 配置了 watch 监听文件更改后打包时,会调用下面的函数
  • WatchEmitter 和 node emiter 实现类似,都是发布订阅
    • WatchEmitter 是暴露给开发者可以在 rollup watch 各个阶段订阅
    • watchInternal 是 rollup 真正内部进行监听的方法,会在各个阶段调用 WatchEmitter 发布
export default function watch(configs: RollupOptions[] | RollupOptions): RollupWatcher {
	const emitter = new WatchEmitter() as RollupWatcher; // 对外暴露给用户注册 watch 过程相关的事件订阅器
	
	// 真正内部实现监听的方法
	watchInternal(configs, emitter).catch(error => {
		handleError(error);
	});

	return emitter; // 对外暴露给用户注册的和 watch 相关的 emitter 事件 ,注册的事件会在 watchInternal 中运行各个阶段触发
}

WatchEmitter 实现

import type { AwaitedEventListener, AwaitingEventEmitter } from '../rollup/types';

export class WatchEmitter<T extends { [event: string]: (...parameters: any) => any }>
	implements AwaitingEventEmitter<T>
{
	private currentHandlers: {
		[K in keyof T]?: AwaitedEventListener<T, K>[];
	} = Object.create(null);
	private persistentHandlers: {
		[K in keyof T]?: AwaitedEventListener<T, K>[];
	} = Object.create(null);

	// Will be overwritten by Rollup
	async close(): Promise<void> {}

	emit<K extends keyof T>(event: K, ...parameters: Parameters<T[K]>): Promise<unknown> {
		return Promise.all(
			[...this.getCurrentHandlers(event), ...this.getPersistentHandlers(event)].map(handler =>
				handler(...parameters)
			)
		);
	}

	off<K extends keyof T>(event: K, listener: AwaitedEventListener<T, K>): this {
		const listeners = this.persistentHandlers[event];
		if (listeners) {
			// A hack stolen from "mitt": ">>> 0" does not change numbers >= 0, but -1
			// (which would remove the last array element if used unchanged) is turned
			// into max_int, which is outside the array and does not change anything.
			listeners.splice(listeners.indexOf(listener) >>> 0, 1);
		}
		return this;
	}

	on<K extends keyof T>(event: K, listener: AwaitedEventListener<T, K>): this {
		this.getPersistentHandlers(event).push(listener);
		return this;
	}

	onCurrentRun<K extends keyof T>(event: K, listener: AwaitedEventListener<T, K>): this {
		this.getCurrentHandlers(event).push(listener);
		return this;
	}

	once<K extends keyof T>(event: K, listener: AwaitedEventListener<T, K>): this {
		const selfRemovingListener: AwaitedEventListener<T, K> = (...parameters) => {
			this.off(event, selfRemovingListener);
			return listener(...parameters);
		};
		this.on(event, selfRemovingListener);
		return this;
	}

	removeAllListeners(): this {
		this.removeListenersForCurrentRun();
		this.persistentHandlers = Object.create(null);
		return this;
	}

	removeListenersForCurrentRun(): this {
		this.currentHandlers = Object.create(null);
		return this;
	}

	private getCurrentHandlers<K extends keyof T>(event: K): AwaitedEventListener<T, K>[] {
		return this.currentHandlers[event] || (this.currentHandlers[event] = []);
	}

	private getPersistentHandlers<K extends keyof T>(event: K): AwaitedEventListener<T, K>[] {
		return this.persistentHandlers[event] || (this.persistentHandlers[event] = []);
	}
}

watchInternal

const optionsList = await Promise.all(ensureArray(configs).map(config => mergeOptions(config))); // 和内置 options 合并
const watchOptionsList = optionsList.filter(config => config.watch !== false); // 只拿到配置了 watch 的 options 配置
if (watchOptionsList.length === 0) {
	return error(
		errorInvalidOption(
			'watch',
			URL_WATCH,
			'there must be at least one config where "watch" is not set to "false"'
		)
	);
}
await loadFsEvents(); // 加载 mac 下的监听文件变化的第三方库:fsevents
const { Watcher } = await import('./watch'); // 真正内部实现监听的 watcher
new Watcher(watchOptionsList, emitter);

Watcher 管理整个 watch 阶段

  • 每个 rollup 的打包配置项对应一个 Task 任务,Watcher 类的作用就是管理每一个 Task 的运行
export class Watcher {
	readonly emitter: RollupWatcher;

	private buildDelay = 0;
	private buildTimeout: NodeJS.Timer | null = null;
	private closed = false;
	private readonly invalidatedIds = new Map<string, ChangeEvent>(); // 收集 watch 过程每一次变化的文件 id 及其对应的事件
	private rerun = false;
	private running = true;
	private readonly tasks: Task[];
	// optionsList 所有配置了 watch 的 option; emitter 暴露给用户注册 watch 过程相关事件的订阅器
	constructor(optionsList: readonly MergedRollupOptions[], emitter: RollupWatcher) {
		this.emitter = emitter;
		emitter.close = this.close.bind(this);
		this.tasks = optionsList.map(options => new Task(this, options)); // 每一个配置了 watch 的 option 都对应一个 Task,Task 内通过 FileWatcher 实现了通过 chokidar 进行监听
		for (const { watch } of optionsList) {
			// 每次重新运行的防抖时间
			if (watch && typeof watch.buildDelay === 'number') {
				this.buildDelay = Math.max(this.buildDelay, watch.buildDelay!);
			}
		}
		// 初始执行
		process.nextTick(() => this.run());
	}

	async close(): Promise<void> {
		if (this.closed) return;
		this.closed = true;
		if (this.buildTimeout) clearTimeout(this.buildTimeout);
		for (const task of this.tasks) {
			task.close();
		}
		await this.emitter.emit('close');
		this.emitter.removeAllListeners();
	}
	
	// 文件变化后调用此函数重新打包
	invalidate(file?: { event: ChangeEvent; id: string }): void {
		if (file) {
			const previousEvent = this.invalidatedIds.get(file.id);
			const event = previousEvent ? eventsRewrites[previousEvent][file.event] : file.event;
			// 为每一个变换了的文件 id 设置对应的事件 "delete" | "add" ...
			if (event === 'buggy') {
				//TODO: throws or warn? Currently just ignore, uses new event
				this.invalidatedIds.set(file.id, file.event);
			} else if (event === null) {
				this.invalidatedIds.delete(file.id);
			} else {
				this.invalidatedIds.set(file.id, event);
			}
		}
		if (this.running) {
			this.rerun = true;
			return;
		}

		if (this.buildTimeout) clearTimeout(this.buildTimeout);
		// 定时器防抖触发 rollup 重新 run
		this.buildTimeout = setTimeout(async () => {
			this.buildTimeout = null;
			try {
				// 文件变化后触发对应emitter
				await Promise.all(
					[...this.invalidatedIds].map(([id, event]) => this.emitter.emit('change', id, { event }))
				);
				this.invalidatedIds.clear();
				await this.emitter.emit('restart');
				this.emitter.removeListenersForCurrentRun(); // 取消当前过程注册的所有事件
				// 重新打包
				this.run();
			} catch (error: any) {
				this.invalidatedIds.clear();
				await this.emitter.emit('event', {
					code: 'ERROR',
					error,
					result: null
				});
				await this.emitter.emit('event', {
					code: 'END'
				});
			}
		}, this.buildDelay);
	}

	private async run(): Promise<void> {
		this.running = true;
		await this.emitter.emit('event', {
			code: 'START'
		});
		// 根据配置项重新运行 rollup
		for (const task of this.tasks) {
			await task.run();
		}

		this.running = false;
		await this.emitter.emit('event', {
			code: 'END'
		});
		if (this.rerun) {
			this.rerun = false;
			this.invalidate();
		}
	}
}

Task 运行任务

  • 每一个配置了 watch 的 rollup 选项都会变成一个单独的 Task,每一个Task 都管理每一次运行任务
export class Task {
	cache: RollupCache = { modules: [] };
	watchFiles: string[] = [];

	private closed = false;
	private readonly fileWatcher: FileWatcher; // 真正去实现监听的 watcher
	private filter: (id: string) => boolean; // 过滤监听
	private invalidated = true;
	private readonly options: MergedRollupOptions;
	private readonly outputFiles: string[];
	private readonly outputs: OutputOptions[];
	private skipWrite: boolean;
	private watched = new Set<string>(); // 收集每次监听的文件 id
	private readonly watcher: Watcher; // watcher 为管理 Task 的 Watcher

	constructor(watcher: Watcher, options: MergedRollupOptions) {
		this.watcher = watcher; // watcher 为管理 Task 的Watcher
		this.options = options;
		// 是否跳过输出文件
		this.skipWrite = Boolean(options.watch && options.watch.skipWrite);
		this.outputs = this.options.output;
		// 解析输出文件地址的路径
		this.outputFiles = this.outputs.map(output => {
			if (output.file || output.dir) return resolve(output.file || output.dir!);
			return undefined as never;
		});

		const watchOptions: WatcherOptions = this.options.watch || {};
		// 创建 watch 中配置了 exclude、include 相关的过滤方法
		this.filter = createFilter(watchOptions.include, watchOptions.exclude);
		this.fileWatcher = new FileWatcher(this, {
			...watchOptions.chokidar,
			disableGlobbing: true,
			ignoreInitial: true
		});
	}

	close(): void {
		this.closed = true;
		this.fileWatcher.close();
	}
	// chokidar 监听到文件变化后触发 ,id 变化的文件 id; event 对应的文件变化类型; isTransformDependency 当前模块在插件中额外依赖的 id
	invalidate(id: string, details: { event: ChangeEvent; isTransformDependency?: boolean }): void {
		this.invalidated = true;
		// 当前模块在 rollup 打包时插件中添加的依赖模块
		// 当依赖模块更新后,该模块缓存失效清空
		if (details.isTransformDependency) {
			for (const module of this.cache.modules) {
				// 插件里调用 this.addWatchFile 时,watch的文件会放进该模块的 transformDependencies 属性里
				// 插件里调用 this.emitFile 生成的文件会放进该模块的 transformFiles 属性里
				if (!module.transformDependencies.includes(id)) continue;
				// effective invalidation
				module.originalCode = null as never; // 清除缓存的内容,后续重新生成
			}
		}
		this.watcher.invalidate({ event: details.event, id });
	}

	async run(): Promise<void> {
		if (!this.invalidated) return; // 是否在运行中
		this.invalidated = false;

		const options = {
			...this.options,
			cache: this.cache
		};

		const start = Date.now();

		await this.watcher.emitter.emit('event', {
			code: 'BUNDLE_START',
			input: this.options.input,
			output: this.outputFiles
		});
		let result: RollupBuild | null = null;

		try {
			result = await rollupInternal(options, this.watcher.emitter); // 到此和非 watch 模式下一致
			if (this.closed) {
				return;
			}
			// 打包完成,获取到所有需要监听变化的文件 id
			this.updateWatchedFiles(result);
			this.skipWrite || (await Promise.all(this.outputs.map(output => result!.write(output))));
			await this.watcher.emitter.emit('event', {
				code: 'BUNDLE_END',
				duration: Date.now() - start,
				input: this.options.input,
				output: this.outputFiles,
				result
			});
		} catch (error: any) {
			if (!this.closed) {
				if (Array.isArray(error.watchFiles)) {
					for (const id of error.watchFiles) {
						this.watchFile(id);
					}
				}
				if (error.id) {
					this.cache.modules = this.cache.modules.filter(module => module.id !== error.id);
				}
			}
			await this.watcher.emitter.emit('event', {
				code: 'ERROR',
				error,
				result
			});
		}
	}
	
	// 添加打包过程需要的所有监听文件、清除不需要的监听文件
	private updateWatchedFiles(result: RollupBuild) {
		const previouslyWatched = this.watched;
		this.watched = new Set();
		// 返回本次打包运行监听的文件 id
		this.watchFiles = result.watchFiles;
		// 缓存打包结果
		this.cache = result.cache!;

		for (const id of this.watchFiles) {
			this.watchFile(id);
		}

		// 获取缓存文件的 id
		for (const module of this.cache.modules) {
			// 在插件中添加的需要观测的依赖
			for (const depId of module.transformDependencies) {
				this.watchFile(depId, true);
			}
		}
		// 取消监听上一轮不需要监听的文件 id
		for (const id of previouslyWatched) {
			if (!this.watched.has(id)) {
				this.fileWatcher.unwatch(id);
			}
		}
	}

	private watchFile(id: string, isTransformDependency = false) {
		if (!this.filter(id)) return;
		this.watched.add(id); // 收集每次监听的文件 id

		if (this.outputFiles.includes(id)) {
			throw new Error('Cannot import the generated bundle');
		}

		// this is necessary to ensure that any 'renamed' files
		// continue to be watched following an error
		this.fileWatcher.watch(id, isTransformDependency);
	}
}

FileWatcher 实现文件监听

import { platform } from 'node:os';
import chokidar, { type FSWatcher } from 'chokidar';
import type { ChangeEvent, ChokidarOptions } from '../rollup/types';
import type { Task } from './watch';

export class FileWatcher {
	private readonly chokidarOptions: ChokidarOptions;
	private readonly task: Task;
	private readonly transformWatchers = new Map<string, FSWatcher>(); // 收集所有监听插件中添加的依赖文件的 chokdir watcher
	private readonly watcher: FSWatcher; // 正常的 Task 对应的 chokidar watcher

	constructor(task: Task, chokidarOptions: ChokidarOptions) {
		this.chokidarOptions = chokidarOptions; // 用户传递的 chokidar 配置
		this.task = task; // 每一个带有 watch 的 option 对应的 Task
		this.watcher = this.createWatcher(null); // 通过 chokidar 监听的实例
	}

	close(): void {
		this.watcher.close();
		for (const watcher of this.transformWatchers.values()) {
			watcher.close();
		}
	}

	unwatch(id: string): void {
		this.watcher.unwatch(id);
		const transformWatcher = this.transformWatchers.get(id);
		if (transformWatcher) {
			this.transformWatchers.delete(id);
			transformWatcher.close();
		}
	}

	watch(id: string, isTransformDependency: boolean): void {
	     // 如果是插件中添加的依赖文件,单独生成一个 chokidar watcher
		if (isTransformDependency) {
			const watcher = this.transformWatchers.get(id) ?? this.createWatcher(id);
			watcher.add(id);
			this.transformWatchers.set(id, watcher);
		} else {
			this.watcher.add(id);
		}
	}
	// transformWatcherId 区分是否是插件中添加的依赖发生了变化
	private createWatcher(transformWatcherId: string | null): FSWatcher {
		const task = this.task;
		const isLinux = platform() === 'linux';
		const isTransformDependency = transformWatcherId !== null;
		// chokidar 监听到文件变化时触发的回调
		const handleChange = (id: string, event: ChangeEvent) => {
			const changedId = transformWatcherId || id;
			if (isLinux) {
				// unwatching and watching fixes an issue with chokidar where on certain systems,
				// a file that was unlinked and immediately recreated would create a change event
				// but then no longer any further events
				watcher.unwatch(changedId);
				watcher.add(changedId);
			}
			// 重新运行打包
			task.invalidate(changedId, { event, isTransformDependency });
		};
		// 通过 chokidar 进行监听
		const watcher = chokidar
			.watch([], this.chokidarOptions)
			.on('add', id => handleChange(id, 'create'))
			.on('change', id => handleChange(id, 'update'))
			.on('unlink', id => handleChange(id, 'delete'));
		return watcher;
	}
}

你可能感兴趣的:(cli,工程化原理,前端,javascript,java)