webpack 源码分析(二)

接着上一节内容,我们分析到applyWebpackOptionsBaseDefaults,我们继续分析下一行代码。

const compiler = new Compiler(options.context, options);

我们先来想一下这两个参数,一个是context全局上下文参数,第二个是options,经过处理过的config参数

先说一下Complier对应源码在哪里,对应lib/compiler.js文件

webpack 源码中这个类有1100行代码,确实很多,我看着也烦的慌,但是我们慢慢来看

因为我们这里没有调用compiler任何的方法,所以我们现在没必要看他上面的方法

class Compiler {
  	constructor(context, options = /** @type {WebpackOptions} */ ({})) {
		this.hooks = Object.freeze({
			/** @type {SyncHook<[]>} */
			initialize: new SyncHook([]),

			/** @type {SyncBailHook<[Compilation], boolean | undefined>} */
			shouldEmit: new SyncBailHook(["compilation"]),
			/** @type {AsyncSeriesHook<[Stats]>} */
			done: new AsyncSeriesHook(["stats"]),
			/** @type {SyncHook<[Stats]>} */
			afterDone: new SyncHook(["stats"]),
			/** @type {AsyncSeriesHook<[]>} */
			additionalPass: new AsyncSeriesHook([]),
			/** @type {AsyncSeriesHook<[Compiler]>} */
			beforeRun: new AsyncSeriesHook(["compiler"]),
			/** @type {AsyncSeriesHook<[Compiler]>} */
			run: new AsyncSeriesHook(["compiler"]),
			/** @type {AsyncSeriesHook<[Compilation]>} */
			emit: new AsyncSeriesHook(["compilation"]),
			/** @type {AsyncSeriesHook<[string, AssetEmittedInfo]>} */
			assetEmitted: new AsyncSeriesHook(["file", "info"]),
			/** @type {AsyncSeriesHook<[Compilation]>} */
			afterEmit: new AsyncSeriesHook(["compilation"]),

			/** @type {SyncHook<[Compilation, CompilationParams]>} */
			thisCompilation: new SyncHook(["compilation", "params"]),
			/** @type {SyncHook<[Compilation, CompilationParams]>} */
			compilation: new SyncHook(["compilation", "params"]),
			/** @type {SyncHook<[NormalModuleFactory]>} */
			normalModuleFactory: new SyncHook(["normalModuleFactory"]),
			/** @type {SyncHook<[ContextModuleFactory]>}  */
			contextModuleFactory: new SyncHook(["contextModuleFactory"]),

			/** @type {AsyncSeriesHook<[CompilationParams]>} */
			beforeCompile: new AsyncSeriesHook(["params"]),
			/** @type {SyncHook<[CompilationParams]>} */
			compile: new SyncHook(["params"]),
			/** @type {AsyncParallelHook<[Compilation]>} */
			make: new AsyncParallelHook(["compilation"]),
			/** @type {AsyncParallelHook<[Compilation]>} */
			finishMake: new AsyncSeriesHook(["compilation"]),
			/** @type {AsyncSeriesHook<[Compilation]>} */
			afterCompile: new AsyncSeriesHook(["compilation"]),

			/** @type {AsyncSeriesHook<[]>} */
			readRecords: new AsyncSeriesHook([]),
			/** @type {AsyncSeriesHook<[]>} */
			emitRecords: new AsyncSeriesHook([]),

			/** @type {AsyncSeriesHook<[Compiler]>} */
			watchRun: new AsyncSeriesHook(["compiler"]),
			/** @type {SyncHook<[Error]>} */
			failed: new SyncHook(["error"]),
			/** @type {SyncHook<[string | null, number]>} */
			invalid: new SyncHook(["filename", "changeTime"]),
			/** @type {SyncHook<[]>} */
			watchClose: new SyncHook([]),
			/** @type {AsyncSeriesHook<[]>} */
			shutdown: new AsyncSeriesHook([]),

			/** @type {SyncBailHook<[string, string, any[]], true>} */
			infrastructureLog: new SyncBailHook(["origin", "type", "args"]),

			// TODO the following hooks are weirdly located here
			// TODO move them for webpack 5
			/** @type {SyncHook<[]>} */
			environment: new SyncHook([]),
			/** @type {SyncHook<[]>} */
			afterEnvironment: new SyncHook([]),
			/** @type {SyncHook<[Compiler]>} */
			afterPlugins: new SyncHook(["compiler"]),
			/** @type {SyncHook<[Compiler]>} */
			afterResolvers: new SyncHook(["compiler"]),
			/** @type {SyncBailHook<[string, Entry], boolean>} */
			entryOption: new SyncBailHook(["context", "entry"])
		});

		this.webpack = webpack;

		/** @type {string=} */
		this.name = undefined;
		/** @type {Compilation=} */
		this.parentCompilation = undefined;
		/** @type {Compiler} */
		this.root = this;
		/** @type {string} */
		this.outputPath = "";
		/** @type {Watching} */
		this.watching = undefined;

		/** @type {OutputFileSystem} */
		this.outputFileSystem = null;
		/** @type {IntermediateFileSystem} */
		this.intermediateFileSystem = null;
		/** @type {InputFileSystem} */
		this.inputFileSystem = null;
		/** @type {WatchFileSystem} */
		this.watchFileSystem = null;

		/** @type {string|null} */
		this.recordsInputPath = null;
		/** @type {string|null} */
		this.recordsOutputPath = null;
		this.records = {};
		/** @type {Set<string | RegExp>} */
		this.managedPaths = new Set();
		/** @type {Set<string | RegExp>} */
		this.immutablePaths = new Set();

		/** @type {ReadonlySet<string>} */
		this.modifiedFiles = undefined;
		/** @type {ReadonlySet<string>} */
		this.removedFiles = undefined;
		/** @type {ReadonlyMap<string, FileSystemInfoEntry | "ignore" | null>} */
		this.fileTimestamps = undefined;
		/** @type {ReadonlyMap<string, FileSystemInfoEntry | "ignore" | null>} */
		this.contextTimestamps = undefined;
		/** @type {number} */
		this.fsStartTime = undefined;

		/** @type {ResolverFactory} */
		this.resolverFactory = new ResolverFactory();

		this.infrastructureLogger = undefined;

		this.options = options;

		this.context = context;

		this.requestShortener = new RequestShortener(context, this.root);

		this.cache = new Cache();

		/** @type {Map<Module, { buildInfo: object, references: WeakMap<Dependency, Module>, memCache: WeakTupleMap }> | undefined} */
		this.moduleMemCaches = undefined;

		this.compilerPath = "";

		/** @type {boolean} */
		this.running = false;

		/** @type {boolean} */
		this.idle = false;

		/** @type {boolean} */
		this.watchMode = false;

		this._backCompat = this.options.experiments.backCompat !== false;

		/** @type {Compilation} */
		this._lastCompilation = undefined;
		/** @type {NormalModuleFactory} */
		this._lastNormalModuleFactory = undefined;

		/** @private @type {WeakMap<Source, { sizeOnlySource: SizeOnlySource, writtenTo: Map<string, number> }>} */
		this._assetEmittingSourceCache = new WeakMap();
		/** @private @type {Map<string, number>} */
		this._assetEmittingWrittenFiles = new Map();
		/** @private @type {Set<string>} */
		this._assetEmittingPreviousFiles = new Set();
	}
}

就算是现在这个样子,我们看上去也十分烦躁,一个构造函数这么多东西,其实没什么东西

其实就没几个有用的信息this.hooks = Object.freeze({xxxxxxx}),这是webpackcompiler的钩子,里面有initialize,done,run等钩子,是用tapable实现的,这不就是我们所有的面试题嘛,webpack内部使用了tapable,那什么是tapable,我这里不过多介绍,我觉得大家可以自己去学习一下,我简单教一下大家它是什么

const syncHook = new SyncHook(["author", "age"]);
syncHook.tap("监听器1", (name, age) => {
  console.log("监听器1:", name, age);
});
syncHook.call("will", "18");

看代码,其实tapable就是一个eventEmit,通过tap来监听事件,如on('xxx',callback),通过call去触发事件,同emit('xxxx',...arguments),只是tapable是他的升级版本,他有同步,异步,串行,并行的模式,可以理解成eventEmit pro max 版本

接下来,我们继续往下看,this.webpack = webpack 让compiler绑定webpack,this.name = undefined; this.parentCompilation = undefined; this.root = this; this.outputPath = ""; this.watching = undefined; 。。。。。。 其实就是复了一个初始值,用的时候再说就好,到现在为止,这些值没有多大意义。

我们继续往下看 new NodeEnvironmentPlugin({ infrastructureLogging: options.infrastructureLogging }).apply(compiler);

这个可以说一说,看名字node环境插件,看起来就不是随随便便没有用的插件,那我们接着看

class NodeEnvironmentPlugin {

	constructor(options) {
		this.options = options;
	}

	apply(compiler) {
		const { infrastructureLogging } = this.options;
		compiler.infrastructureLogger = createConsoleLogger({
			level: infrastructureLogging.level || "info",
			debug: infrastructureLogging.debug || false,
			console:
				infrastructureLogging.console ||
				nodeConsole({
					colors: infrastructureLogging.colors,
					appendOnly: infrastructureLogging.appendOnly,
					stream: infrastructureLogging.stream
				})
		});
		compiler.inputFileSystem = new CachedInputFileSystem(fs, 60000);
		const inputFileSystem = compiler.inputFileSystem;
		compiler.outputFileSystem = fs;
		compiler.intermediateFileSystem = fs;
		compiler.watchFileSystem = new NodeWatchFileSystem(
			compiler.inputFileSystem
		);
		compiler.hooks.beforeRun.tap("NodeEnvironmentPlugin", compiler => {
			if (compiler.inputFileSystem === inputFileSystem) {
				compiler.fsStartTime = Date.now();
				inputFileSystem.purge();
			}
		});
	}
}

我们来看代码,constructor没有可说的,就一行,给options赋值传递的是之前处理的infrastructureLogging

我们直接看apply就好,第一眼看上去这是啥 createConsoleLogger CachedInputFileSystem 又有各种流,这都是个啥,其实很简单,我来带着分析

首先参数compiler 就是我们刚才new的compiler

我们来看 createConsoleLogger

先来分析一下参数,因为我们默认不会处理这个日志的参数,所以就是上一篇文章给我们复的默认值,即

level : info debug : false console : nodeConsole({{color:true,appendOnly:false,stream:process.stderr}})

那我们先看nodeConsole

代码,我仍这里了,但是我觉得没必要仔细看,我就大概讲一下,做了什么,首先做了相关格式的优化,核心就是stream.write(''),在process.stderr中写入内容,这样就输出到终端上面日志了,代码我扔到了下面,不感兴趣的可以过掉,感兴趣的可以看一下

module.exports = ({ colors, appendOnly, stream }) => {
	let currentStatusMessage = undefined;
	let hasStatusMessage = false;
	let currentIndent = "";
	let currentCollapsed = 0;

	const indent = (str, prefix, colorPrefix, colorSuffix) => {
		if (str === "") return str;
		prefix = currentIndent + prefix;
		if (colors) {
			return (
				prefix +
				colorPrefix +
				str.replace(/\n/g, colorSuffix + "\n" + prefix + colorPrefix) +
				colorSuffix
			);
		} else {
			return prefix + str.replace(/\n/g, "\n" + prefix);
		}
	};

	const clearStatusMessage = () => {
		if (hasStatusMessage) {
			stream.write("\x1b[2K\r");
			hasStatusMessage = false;
		}
	};

	const writeStatusMessage = () => {
		if (!currentStatusMessage) return;
		const l = stream.columns || 40;
		const args = truncateArgs(currentStatusMessage, l - 1);
		const str = args.join(" ");
		const coloredStr = `\u001b[1m${str}\u001b[39m\u001b[22m`;
		stream.write(`\x1b[2K\r${coloredStr}`);
		hasStatusMessage = true;
	};

	const writeColored = (prefix, colorPrefix, colorSuffix) => {
		return (...args) => {
			if (currentCollapsed > 0) return;
			clearStatusMessage();
			const str = indent(
				util.format(...args),
				prefix,
				colorPrefix,
				colorSuffix
			);
			stream.write(str + "\n");
			writeStatusMessage();
		};
	};

	const writeGroupMessage = writeColored(
		"<-> ",
		"\u001b[1m\u001b[36m",
		"\u001b[39m\u001b[22m"
	);

	const writeGroupCollapsedMessage = writeColored(
		"<+> ",
		"\u001b[1m\u001b[36m",
		"\u001b[39m\u001b[22m"
	);

	return {
		log: writeColored("    ", "\u001b[1m", "\u001b[22m"),
		debug: writeColored("    ", "", ""),
		trace: writeColored("    ", "", ""),
		info: writeColored("<i> ", "\u001b[1m\u001b[32m", "\u001b[39m\u001b[22m"),
		warn: writeColored("<w> ", "\u001b[1m\u001b[33m", "\u001b[39m\u001b[22m"),
		error: writeColored("<e> ", "\u001b[1m\u001b[31m", "\u001b[39m\u001b[22m"),
		logTime: writeColored(
			"<t> ",
			"\u001b[1m\u001b[35m",
			"\u001b[39m\u001b[22m"
		),
		group: (...args) => {
			writeGroupMessage(...args);
			if (currentCollapsed > 0) {
				currentCollapsed++;
			} else {
				currentIndent += "  ";
			}
		},
		groupCollapsed: (...args) => {
			writeGroupCollapsedMessage(...args);
			currentCollapsed++;
		},
		groupEnd: () => {
			if (currentCollapsed > 0) currentCollapsed--;
			else if (currentIndent.length >= 2)
				currentIndent = currentIndent.slice(0, currentIndent.length - 2);
		},
		// eslint-disable-next-line node/no-unsupported-features/node-builtins
		profile: console.profile && (name => console.profile(name)),
		// eslint-disable-next-line node/no-unsupported-features/node-builtins
		profileEnd: console.profileEnd && (name => console.profileEnd(name)),
		clear:
			!appendOnly &&
			// eslint-disable-next-line node/no-unsupported-features/node-builtins
			console.clear &&
			(() => {
				clearStatusMessage();
				// eslint-disable-next-line node/no-unsupported-features/node-builtins
				console.clear();
				writeStatusMessage();
			}),
		status: appendOnly
			? writeColored("<s> ", "", "")
			: (name, ...args) => {
					args = args.filter(Boolean);
					if (name === undefined && args.length === 0) {
						clearStatusMessage();
						currentStatusMessage = undefined;
					} else if (
						typeof name === "string" &&
						name.startsWith("[webpack.Progress] ")
					) {
						currentStatusMessage = [name.slice(19), ...args];
						writeStatusMessage();
					} else if (name === "[webpack.Progress]") {
						currentStatusMessage = [...args];
						writeStatusMessage();
					} else {
						currentStatusMessage = [name, ...args];
						writeStatusMessage();
					}
			  }
	};
};

然后回到createConsoleLogger,这里我也大概说一下,其实就是配合刚才的nodeConsole,核心还是处理日志,我也省略一下内容,有兴趣的可以了解一下,因为这篇内容挺长的,我就讲一下核心,代码我也依旧扔到底下

module.exports = ({ level = "info", debug = false, console }) => {
	const debugFilters =
		typeof debug === "boolean"
			? [() => debug]
			: /** @type {FilterItemTypes[]} */ ([])
					.concat(debug)
					.map(filterToFunction);
	/** @type {number} */
	const loglevel = LogLevel[`${level}`] || 0;

	/**
	 * @param {string} name name of the logger
	 * @param {LogTypeEnum} type type of the log entry
	 * @param {any[]} args arguments of the log entry
	 * @returns {void}
	 */
	const logger = (name, type, args) => {
		const labeledArgs = () => {
			if (Array.isArray(args)) {
				if (args.length > 0 && typeof args[0] === "string") {
					return [`[${name}] ${args[0]}`, ...args.slice(1)];
				} else {
					return [`[${name}]`, ...args];
				}
			} else {
				return [];
			}
		};
		const debug = debugFilters.some(f => f(name));
		switch (type) {
			case LogType.debug:
				if (!debug) return;
				// eslint-disable-next-line node/no-unsupported-features/node-builtins
				if (typeof console.debug === "function") {
					// eslint-disable-next-line node/no-unsupported-features/node-builtins
					console.debug(...labeledArgs());
				} else {
					console.log(...labeledArgs());
				}
				break;
			case LogType.log:
				if (!debug && loglevel > LogLevel.log) return;
				console.log(...labeledArgs());
				break;
			case LogType.info:
				if (!debug && loglevel > LogLevel.info) return;
				console.info(...labeledArgs());
				break;
			case LogType.warn:
				if (!debug && loglevel > LogLevel.warn) return;
				console.warn(...labeledArgs());
				break;
			case LogType.error:
				if (!debug && loglevel > LogLevel.error) return;
				console.error(...labeledArgs());
				break;
			case LogType.trace:
				if (!debug) return;
				console.trace();
				break;
			case LogType.groupCollapsed:
				if (!debug && loglevel > LogLevel.log) return;
				if (!debug && loglevel > LogLevel.verbose) {
					// eslint-disable-next-line node/no-unsupported-features/node-builtins
					if (typeof console.groupCollapsed === "function") {
						// eslint-disable-next-line node/no-unsupported-features/node-builtins
						console.groupCollapsed(...labeledArgs());
					} else {
						console.log(...labeledArgs());
					}
					break;
				}
			// falls through
			case LogType.group:
				if (!debug && loglevel > LogLevel.log) return;
				// eslint-disable-next-line node/no-unsupported-features/node-builtins
				if (typeof console.group === "function") {
					// eslint-disable-next-line node/no-unsupported-features/node-builtins
					console.group(...labeledArgs());
				} else {
					console.log(...labeledArgs());
				}
				break;
			case LogType.groupEnd:
				if (!debug && loglevel > LogLevel.log) return;
				// eslint-disable-next-line node/no-unsupported-features/node-builtins
				if (typeof console.groupEnd === "function") {
					// eslint-disable-next-line node/no-unsupported-features/node-builtins
					console.groupEnd();
				}
				break;
			case LogType.time: {
				if (!debug && loglevel > LogLevel.log) return;
				const ms = args[1] * 1000 + args[2] / 1000000;
				const msg = `[${name}] ${args[0]}: ${ms} ms`;
				if (typeof console.logTime === "function") {
					console.logTime(msg);
				} else {
					console.log(msg);
				}
				break;
			}
			case LogType.profile:
				// eslint-disable-next-line node/no-unsupported-features/node-builtins
				if (typeof console.profile === "function") {
					// eslint-disable-next-line node/no-unsupported-features/node-builtins
					console.profile(...labeledArgs());
				}
				break;
			case LogType.profileEnd:
				// eslint-disable-next-line node/no-unsupported-features/node-builtins
				if (typeof console.profileEnd === "function") {
					// eslint-disable-next-line node/no-unsupported-features/node-builtins
					console.profileEnd(...labeledArgs());
				}
				break;
			case LogType.clear:
				if (!debug && loglevel > LogLevel.log) return;
				// eslint-disable-next-line node/no-unsupported-features/node-builtins
				if (typeof console.clear === "function") {
					// eslint-disable-next-line node/no-unsupported-features/node-builtins
					console.clear();
				}
				break;
			case LogType.status:
				if (!debug && loglevel > LogLevel.info) return;
				if (typeof console.status === "function") {
					if (args.length === 0) {
						console.status();
					} else {
						console.status(...labeledArgs());
					}
				} else {
					if (args.length !== 0) {
						console.info(...labeledArgs());
					}
				}
				break;
			default:
				throw new Error(`Unexpected LogType ${type}`);
		}
	};
	return logger;
};

compiler.infrastructureLogger 处理日志也就处理完毕了

接下来compiler.inputFileSystem = new CachedInputFileSystem(fs, 60000);

这个我觉得有必要多说一下,这里是我介绍的核心知识点

CachedInputFileSystem

这个类它不再webpack这个包里,在webpack官方的其他包里面,在enhanced-resolve/lib/CachedInputFileSystem

本篇内容也会在这个类讲解完毕结束

先留下一个点,为什么输入流不直接用fs呢,而选择用cachedinputfilesystem呢。它有什么魔力呢?

我先把代码扔上去,不着急看代码,先来听我分析

class CachedInputFileSystem {
	constructor(fileSystem, duration) {
		this.fileSystem = fileSystem;
	  
		this._lstatBackend = createBackend(
			duration,
			this.fileSystem.lstat,
			this.fileSystem.lstatSync,
			this.fileSystem
		);
		const lstat = this._lstatBackend.provide;
		this.lstat = /** @type {FileSystem["lstat"]} */ (lstat);
		const lstatSync = this._lstatBackend.provideSync;
		this.lstatSync = /** @type {SyncFileSystem["lstatSync"]} */ (lstatSync);

		this._statBackend = createBackend(
			duration,
			this.fileSystem.stat,
			this.fileSystem.statSync,
			this.fileSystem
		);
		const stat = this._statBackend.provide;
		this.stat = /** @type {FileSystem["stat"]} */ (stat);
		const statSync = this._statBackend.provideSync;
		this.statSync = /** @type {SyncFileSystem["statSync"]} */ (statSync);

		this._readdirBackend = createBackend(
			duration,
			this.fileSystem.readdir,
			this.fileSystem.readdirSync,
			this.fileSystem
		);
		const readdir = this._readdirBackend.provide;
		this.readdir = /** @type {FileSystem["readdir"]} */ (readdir);
		const readdirSync = this._readdirBackend.provideSync;
		this.readdirSync = /** @type {SyncFileSystem["readdirSync"]} */ (
			readdirSync
		);

		this._readFileBackend = createBackend(
			duration,
			this.fileSystem.readFile,
			this.fileSystem.readFileSync,
			this.fileSystem
		);
		const readFile = this._readFileBackend.provide;
		this.readFile = /** @type {FileSystem["readFile"]} */ (readFile);
		const readFileSync = this._readFileBackend.provideSync;
		this.readFileSync = /** @type {SyncFileSystem["readFileSync"]} */ (
			readFileSync
		);

		this._readJsonBackend = createBackend(
			duration,
			// prettier-ignore
			this.fileSystem.readJson ||
				(this.readFile &&
					(
						/**
						 * @param {string} path path
						 * @param {FileSystemCallback<any>} callback
						 */
						(path, callback) => {
							this.readFile(path, (err, buffer) => {
								if (err) return callback(err);
								if (!buffer || buffer.length === 0)
									return callback(new Error("No file content"));
								let data;
								try {
									data = JSON.parse(buffer.toString("utf-8"));
								} catch (e) {
									return callback(/** @type {Error} */ (e));
								}
								callback(null, data);
							});
						})
				),
			// prettier-ignore
			this.fileSystem.readJsonSync ||
				(this.readFileSync &&
					(
						/**
						 * @param {string} path path
						 * @returns {any} result
						 */
						(path) => {
							const buffer = this.readFileSync(path);
							const data = JSON.parse(buffer.toString("utf-8"));
							return data;
						}
				 )),
			this.fileSystem
		);
		const readJson = this._readJsonBackend.provide;
		this.readJson = /** @type {FileSystem["readJson"]} */ (readJson);
		const readJsonSync = this._readJsonBackend.provideSync;
		this.readJsonSync = /** @type {SyncFileSystem["readJsonSync"]} */ (
			readJsonSync
		);

		this._readlinkBackend = createBackend(
			duration,
			this.fileSystem.readlink,
			this.fileSystem.readlinkSync,
			this.fileSystem
		);
		const readlink = this._readlinkBackend.provide;
		this.readlink = /** @type {FileSystem["readlink"]} */ (readlink);
		const readlinkSync = this._readlinkBackend.provideSync;
		this.readlinkSync = /** @type {SyncFileSystem["readlinkSync"]} */ (
			readlinkSync
		);
	}

	/**
	 * @param {string|string[]|Set<string>} [what] what to purge
	 */
	purge(what) {
		this._statBackend.purge(what);
		this._lstatBackend.purge(what);
		this._readdirBackend.purgeParent(what);
		this._readFileBackend.purge(what);
		this._readlinkBackend.purge(what);
		this._readJsonBackend.purge(what);
	}
};

老规矩,先来分析构造函数,我们传递了fs ,60000

this.fileSystem = fs;

后续你会发现代码不都是重复的吗,createBackend,然后重写read,readsync,stat等方法

既然是重复的其实我们看一个就好

那我们就来分析readFile

		this._readFileBackend = createBackend(
			duration,
			this.fileSystem.readFile,
			this.fileSystem.readFileSync,
			this.fileSystem
		);
		const readFile = this._readFileBackend.provide;
		this.readFile = /** @type {FileSystem["readFile"]} */ (readFile);

先看 createBackend(60000,fs.readFile,fs.readFileSync,fs)

const createBackend = (duration, provider, syncProvider, providerContext) => {
	if (duration > 0) {
		return new CacheBackend(duration, provider, syncProvider, providerContext);
	}
	return new OperationMergerBackend(provider, syncProvider, providerContext);
}

肯定大于0呀,走到new CacheBackend(60000,fs.readFile,fs.readFileSync,fs);

class CacheBackend {
	/**
	 * @param {number} duration max cache duration of items
	 * @param {function} provider async method
	 * @param {function} syncProvider sync method
	 * @param {BaseFileSystem} providerContext call context for the provider methods
	 */
	constructor(duration, provider, syncProvider, providerContext) {
		this._duration = duration;
		this._provider = provider;
		this._syncProvider = syncProvider;
		this._providerContext = providerContext;
		/** @type {Map<string, FileSystemCallback<any>[]>} */
		this._activeAsyncOperations = new Map();
		/** @type {Map<string, { err?: Error, result?: any, level: Set<string> }>} */
		this._data = new Map();
		/** @type {Set<string>[]} */
		this._levels = [];
		for (let i = 0; i < 10; i++) this._levels.push(new Set());
		for (let i = 5000; i < duration; i += 500) this._levels.push(new Set());
		this._currentLevel = 0;
		this._tickInterval = Math.floor(duration / this._levels.length);
		/** @type {STORAGE_MODE_IDLE | STORAGE_MODE_SYNC | STORAGE_MODE_ASYNC} */
		this._mode = STORAGE_MODE_IDLE;

		/** @type {NodeJS.Timeout | undefined} */
		this._timeout = undefined;
		/** @type {number | undefined} */
		this._nextDecay = undefined;

		// @ts-ignore
		this.provide = provider ? this.provide.bind(this) : null;
		// @ts-ignore
		this.provideSync = syncProvider ? this.provideSync.bind(this) : null;
	}

	/**
	 * @param {string} path path
	 * @param {any} options options
	 * @param {FileSystemCallback<any>} callback callback
	 * @returns {void}
	 */
	provide(path, options, callback) {
		if (typeof options === "function") {
			callback = options;
			options = undefined;
		}
		if (typeof path !== "string") {
			callback(new TypeError("path must be a string"));
			return;
		}
		if (options) {
			return this._provider.call(
				this._providerContext,
				path,
				options,
				callback
			);
		}

		// When in sync mode we can move to async mode 当处于同步的时候,我们可以切换到异步
		if (this._mode === STORAGE_MODE_SYNC) {
			this._enterAsyncMode();
		}

		// Check in cache 有缓存 走缓存
		let cacheEntry = this._data.get(path);
		if (cacheEntry !== undefined) {
			if (cacheEntry.err) return nextTick(callback, cacheEntry.err);
			return nextTick(callback, null, cacheEntry.result);
		}

		// Check if there is already the same operation running 检查是否存在相同操作
		let callbacks = this._activeAsyncOperations.get(path);
		if (callbacks !== undefined) {
			callbacks.push(callback);
			return;
		}
		this._activeAsyncOperations.set(path, (callbacks = [callback]));

		// Run the operation
		this._provider.call(
			this._providerContext,
			path,
			/**
			 * @param {Error} [err] error
			 * @param {any} [result] result
			 */
			(err, result) => {
				 // 删除异步的事件
				this._activeAsyncOperations.delete(path);
				this._storeResult(path, err, result);

				// Enter async mode if not yet done
				this._enterAsyncMode();

				runCallbacks(
					/** @type {FileSystemCallback<any>[]} */ (callbacks),
					err,
					result
				);
			}
		);
	}

	/**
	 * @param {string} path path
	 * @param {any} options options
	 * @returns {any} result
	 */
	provideSync(path, options) {
		if (typeof path !== "string") {
			throw new TypeError("path must be a string");
		}
		if (options) {
			return this._syncProvider.call(this._providerContext, path, options);
		}

		// In sync mode we may have to decay some cache items
		if (this._mode === STORAGE_MODE_SYNC) {
			this._runDecays();
		}

		// Check in cache
		let cacheEntry = this._data.get(path);
		if (cacheEntry !== undefined) {
			if (cacheEntry.err) throw cacheEntry.err;
			return cacheEntry.result;
		}

		// Get all active async operations
		// This sync operation will also complete them
		const callbacks = this._activeAsyncOperations.get(path);
		this._activeAsyncOperations.delete(path);

		// Run the operation
		// When in idle mode, we will enter sync mode
		let result;
		try {
			result = this._syncProvider.call(this._providerContext, path);
		} catch (err) {
			this._storeResult(path, /** @type {Error} */ (err), undefined);
			this._enterSyncModeWhenIdle();
			if (callbacks) {
				runCallbacks(callbacks, /** @type {Error} */ (err), undefined);
			}
			throw err;
		}
		this._storeResult(path, undefined, result);
		this._enterSyncModeWhenIdle();
		if (callbacks) {
			runCallbacks(callbacks, undefined, result);
		}
		return result;
	}

	/**
	 * @param {string|string[]|Set<string>} [what] what to purge
	 */
	purge(what) {
		if (!what) {
			if (this._mode !== STORAGE_MODE_IDLE) {
				this._data.clear();
				for (const level of this._levels) {
					level.clear();
				}
				this._enterIdleMode();
			}
		} else if (typeof what === "string") {
			for (let [key, data] of this._data) {
				if (key.startsWith(what)) {
					this._data.delete(key);
					data.level.delete(key);
				}
			}
			if (this._data.size === 0) {
				this._enterIdleMode();
			}
		} else {
			for (let [key, data] of this._data) {
				for (const item of what) {
					if (key.startsWith(item)) {
						this._data.delete(key);
						data.level.delete(key);
						break;
					}
				}
			}
			if (this._data.size === 0) {
				this._enterIdleMode();
			}
		}
	}

	/**
	 * @param {string|string[]|Set<string>} [what] what to purge
	 */
	purgeParent(what) {
		if (!what) {
			this.purge();
		} else if (typeof what === "string") {
			this.purge(dirname(what));
		} else {
			const set = new Set();
			for (const item of what) {
				set.add(dirname(item));
			}
			this.purge(set);
		}
	}

	/**
	 * @param {string} path path
	 * @param {undefined | Error} err error
	 * @param {any} result result
	 */
	_storeResult(path, err, result) {
		// 如果data存在 直接 return
		if (this._data.has(path)) return;
		const level = this._levels[this._currentLevel];
		this._data.set(path, { err, result, level });
		level.add(path);
	}

	_decayLevel() {
		const nextLevel = (this._currentLevel + 1) % this._levels.length;
		// 找到下一个level
		const decay = this._levels[nextLevel];
		this._currentLevel = nextLevel;
		for (let item of decay) {
			// 删除 level里面的data
			this._data.delete(item);
		}
		decay.clear();
		if (this._data.size === 0) {
			this._enterIdleMode();
		} else {
			/** @type {number} */
			(this._nextDecay) += this._tickInterval;
		}
	}

	_runDecays() {
		while (
			/** @type {number} */ (this._nextDecay) <= Date.now() &&
			this._mode !== STORAGE_MODE_IDLE
		) {
			this._decayLevel();
		}
	}
	// _enterAsyncMode
	_enterAsyncMode() {
		let timeout = 0;
		switch (this._mode) {
			case STORAGE_MODE_ASYNC:
				return;
			case STORAGE_MODE_IDLE:
				// 下一个 decay
				this._nextDecay = Date.now() + this._tickInterval;
				timeout = this._tickInterval;
				break;
			case STORAGE_MODE_SYNC:
				this._runDecays();
				// _runDecays may change the mode
				if (
					/** @type {STORAGE_MODE_IDLE | STORAGE_MODE_SYNC | STORAGE_MODE_ASYNC}*/
					(this._mode) === STORAGE_MODE_IDLE
				)
					return;
				timeout = Math.max(
					0,
					/** @type {number} */ (this._nextDecay) - Date.now()
				);
				break;
		}
		// 模式切换成异步
		this._mode = STORAGE_MODE_ASYNC;
		const ref = setTimeout(() => {
			this._mode = STORAGE_MODE_SYNC;
			this._runDecays();
		}, timeout);
		if (ref.unref) ref.unref();
		this._timeout = ref;
	}

	_enterSyncModeWhenIdle() {
		if (this._mode === STORAGE_MODE_IDLE) {
			this._mode = STORAGE_MODE_SYNC;
			this._nextDecay = Date.now() + this._tickInterval;
		}
	}

	_enterIdleMode() {
		this._mode = STORAGE_MODE_IDLE;
		this._nextDecay = undefined;
		if (this._timeout) clearTimeout(this._timeout);
	}
}

CacheBackend代码我扔上去了,但是我不打算细讲这里,我不会精细到一行一行的讲,我们直接来讲整体实现,

先看构造函数,我们可以发现,每500,一个level,且有三个模式,idle async sync。

provide方法对应的就是外边调用的readfile,我们可以看到先走缓存data,有data直接nextTick回调,这就是一步cacheBackend对fs进行的优化,没有缓存,把回调方法加入ativeAsyncOperations中,然后在执行fs.readFile方法,然后看回调函数

			(err, result) => {
				 // 删除异步的事件
				this._activeAsyncOperations.delete(path);
				this._storeResult(path, err, result);

				// Enter async mode if not yet done
				this._enterAsyncMode();

				runCallbacks(
					/** @type {FileSystemCallback<any>[]} */ (callbacks),
					err,
					result
				);
			}

先清除activeAsyncOperations,然后_storeResult,给level和data赋值(其实就是加缓存),然后执行_enterAsyncMode,把状态切换成async,最后在执行callback方法。

这里说的非常的粗糙,我在细致讲一下,storeResult

	_storeResult(path, err, result) {
		// 如果data存在 直接 return
		if (this._data.has(path)) return;
		const level = this._levels[this._currentLevel];
		this._data.set(path, { err, result, level });
		level.add(path);
	}

为什么level要传递path,我先说一下,原因是this.level 就可以拿到path,从而可以this._data(this.level获取的path)拿到对应的内容,其实就是我可以通过level去删除_data的数据。

最后再说一下,_enterAsyncMode ,看代码前面什么都没做,其实就是拿到下一个tickInterval,最后改了一下mode,然后搞了一个set time out,这个计时器里的方法是关键,首先把异步变成同步,因为他会删除下一个nextLevel中的方法,可能现在还是有点懵,我就举一个demo

我现在查询了, './src/index.js' 那首先会在activeAsyncOperation加一个callback,然后执行回调,把他的返回值加到data中,下次再读取直接读就好,但是问题来了,什么时候清除data呢,不清除data是不行的,一直不删除那么内存会占用很多,所以enterAsyncMode这个函数就有用了,你会发现隔500ms,就会拿到下一个levels,从而可以拿到里面对应的path,从而清除data中的缓存。

所以缓存都做了什么处理呢?

  1. 定期删除缓存(500ms 防止占用内存,以及同步问题)。
  2. 多次连续调用操作文件,会合成一个callback,这样只用读取一次了,从而提高性能。

那本章就到这里结束

回复一下讲了什么

  1. compiler的hooks
  2. nodeEnviormentPlugins做了什么
  3. cachebackend怎么做的缓存,fs.readfile是怎么优化的?

#前端##前端面试##web前端#
前端技术专栏分享 文章被收录于专栏

1. 包含常规前端面试题解析,和源码分析 2. 从0-1深入浅出理解前端相关技术栈

全部评论

相关推荐

2024-12-04 20:41
南华大学 C++
牛客774533464号:现在要求你有实习经验,才让你实习!
点赞 评论 收藏
分享
评论
点赞
收藏
分享
牛客网
牛客企业服务