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})
,这是webpack的compiler的钩子,里面有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中的缓存。
所以缓存都做了什么处理呢?
- 定期删除缓存(500ms 防止占用内存,以及同步问题)。
- 多次连续调用操作文件,会合成一个callback,这样只用读取一次了,从而提高性能。
那本章就到这里结束
回复一下讲了什么
- compiler的hooks
- nodeEnviormentPlugins做了什么
- cachebackend怎么做的缓存,fs.readfile是怎么优化的?
1. 包含常规前端面试题解析,和源码分析 2. 从0-1深入浅出理解前端相关技术栈