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深入浅出理解前端相关技术栈

查看15道真题和解析