原文首发于 blog.flqin.com。若有错误,请联系笔者。分析码字不易,转载请代表出处,谢谢!html
前面分析了 webpack
的普通主流程构建,另外,经过设置 watch
模式,webpack
能够监听文件变化,当它们修改后会从新编译。文档webpack
webpack-dev-server
和webpack-dev-middleware
里Watch
模式默认开启。git
接下来设置 cli
命令加上 --watch
以后 对 watch
模式下的主流程进行分析(mode = development
)。github
代码执行后,跟主流程相似,而后执行到以前文章介绍到的 编译前的准备 -> 回到 cli.js
里,读取到 options.watchOptions
等 watch
配置后, 走 compiler.watch
:web
//...
compiler.watch(watchOptions, compilerCallback);
复制代码
在 complier
里的 watch
方法里,new
一个 Watching
实例:npm
//...
return new Watching(this, watchOptions, handler); //handler即compilerCallback
复制代码
来到文件 Watching.js
,在 Watching
实例化的过程当中,先对 watchOptions
进行了处理后,在 compiler.readRecords
的回调里执行 _go
:json
//...Watching.js
this._go();
复制代码
_go
方法与 Compiler
里的 run
很相似。 在 _go
里,触发 compiler.hooks
:watchRun
,执行插件 CachePlugin
,即 CachePlugin
里的 this.watching = true
,在钩子 watchRun
回调里执行:数组
// Watching.js
const onCompiled = (err, compilation) => {
//...
};
this.compiler.compile(onCompiled);
复制代码
与普通 webpack
构建一致,即执行 compiler.compile
开始构建,在资源构建结束后执行 onCompiled
。异步
onCompiled
方法与 compiler.run
里的 onCompiled
大体一致,不一样点是全部回调由 finalCallback
改成 _done
,而且将 stats
统计信息相关处理也放到了 _done
里,执行 _done
:async
//... Watching.js
this.compiler.hooks.done.callAsync(stats, () => {
this.handler(null, stats); // compilerCallback
if (!this.closed) {
this.watch(Array.from(compilation.fileDependencies), Array.from(compilation.contextDependencies), Array.from(compilation.missingDependencies));
}
for (const cb of this.callbacks) cb();
this.callbacks.length = 0;
});
复制代码
在该方法里对 stats
设置后,先执行 handler
(实际与 finalCallback
执行一致) 即 compilerCallback
,在 cli
里打印出构建相关的信息。到此,初始化构建完毕。
而后执行 watch
方法并传入在以前 compilation.seal
里 this.summarizeDependencies
方法里生成的 this.fileDependencies, this.contextDependencies, this.missingDependencies
这些须要监听的文件和目录。
在 Watching
的实例 watch
方法里仅仅执行 this.compiler.watchFileSystem.watch
,watchFileSystem
便是在前文 NodeEnvironmentPlugin
里所设置的 NodeWatchFileSystem
的实例。
在 NodeWatchFileSystem
的实例 watch
方法里,先对参数进行了格式判断后,而后执行:
//NodeWatchFileSystem.js
const oldWatcher = this.watcher;
this.watcher = new Watchpack(options);
复制代码
this.watcher
在 NodeWatchFileSystem
实例化的时候已经建立了一个 Watchpack
的实例,这里至关于从新建立了一个实例。
该 Watchpack
继承了 events
模块的 EventEmitter
,因此接下来分别在 this.watcher
(Watchpack
实例) 上注册了 change
,aggregated
事件,而后执行:
this.watcher.watch(cachedFiles.concat(missing), cachedDirs.concat(missing), startTime);
复制代码
即执行 watchpack
的实例方法 watch
,在方法里执行:
//...watchpack.js
this.fileWatchers = files.map(function(file) {
return this._fileWatcher(file, watcherManager.watchFile(file, this.watcherOptions, startTime));
}, this);
this.dirWatchers = directories.map(function(dir) {
return this._dirWatcher(dir, watcherManager.watchDirectory(dir, this.watcherOptions, startTime));
}, this);
复制代码
这里循环对每个 file
进行执行 this._fileWatcher
方法。
通常状况的监听只会涉及 this._fileWatchers,目录类的 this._dirWatchers 会在 require.context 的状况下被监听。
这里先执行 watcherManager.watchFile
,在类 WatcherManager
的实例方法 watchFile
中执行:
//watcherManager.js
var directory = path.dirname(p);
return this.getDirectoryWatcher(directory, options).watch(p, startTime);
复制代码
获取到文件对应路径 directory
后(文件路径 -> 目录路径),this.getDirectoryWatcher
里执行:
//...watcherManager.js
var key = directory + ' ' + JSON.stringify(options);
if (!this.directoryWatchers[key]) {
this.directoryWatchers[key] = new DirectoryWatcher(directory, options);
this.directoryWatchers[key].on(
'closed',
function() {
delete this.directoryWatchers[key];
}.bind(this)
);
}
return this.directoryWatchers[key];
复制代码
this.directoryWatchers
是一个 key
为目录路径,value
为 DirectoryWatcher
实例的对象。
可见 this.getDirectoryWatcher
返回了一个参数为目录路径和配置的 DirectoryWatcher
实例。
DirectoryWatcher
与 Watchpack
同样,也 继承了 events
模块的 EventEmitter
,在实例化的过程当中执行:
//DirectoryWatcher.js
this.watcher = chokidar.watch(directoryPath, {
ignoreInitial: true,
persistent: true,
followSymlinks: false,
depth: 0,
atomic: false,
alwaysStat: true,
ignorePermissionErrors: true,
ignored: options.ignored,
usePolling: options.poll ? true : undefined,
interval: interval, // 即 options.poll 文件系统轮询的时间间隔,越大性能越好
binaryInterval: interval,
disableGlobbing: true
});
复制代码
webpack
采用 npm
包 chokidar 来进行文件的监听,而后根据不一样操做(增长,删除,修改等)绑定一些事件:
//DirectoryWatcher.js
this.watcher.on('add', this.onFileAdded.bind(this));
this.watcher.on('addDir', this.onDirectoryAdded.bind(this));
this.watcher.on('change', this.onChange.bind(this));
this.watcher.on('unlink', this.onFileUnlinked.bind(this));
this.watcher.on('unlinkDir', this.onDirectoryUnlinked.bind(this));
this.watcher.on('error', this.onWatcherError.bind(this));
复制代码
这些事件是挂载在 DirectoryWatcher
类的原型方法上。而后执行:
//DirectoryWatcher.js
this.doInitialScan();
复制代码
即执行:
//DirectoryWatcher.js
fs.readdir(
this.path,
function(err, items) {
//...
async.forEach(
items,
function(item, callback) {
var itemPath = path.join(this.path, item);
fs.stat(
itemPath,
function(err2, stat) {
//...
if (stat.isFile()) {
if (!this.files[itemPath]) this.setFileTime(itemPath, +stat.mtime || +stat.ctime || 1, true);
} else if (stat.isDirectory()) {
if (!this.directories[itemPath]) this.setDirectory(itemPath, true, true);
}
callback();
}.bind(this)
);
}.bind(this),
function() {
this.initialScan = false;
this.initialScanRemoved = null;
}.bind(this)
);
}.bind(this)
);
复制代码
即读取该 path
(上文对应的文件对应文件夹路径 directory
)下的全部文件及文件夹,若是是文件则执行 this.setFileTime
,在该方法里根据是不是首次 watch
来收集该文件的修改时间:
//DirectoryWatcher.js
this.files[filePath] = [initial ? Math.min(now, mtime) : now, mtime];
复制代码
若是是文件夹则执行 this.setDirectory
记录全部子路径。
由于 fs.readdir
为异步,因此 fs.readdir
的回调里先不执行,转而先执行 this.getDirectoryWatcher(directory, options).watch(p, startTime)
的 watch
方法,方法里执行:
//...DirectoryWatcher.js
var watcher = new Watcher(this, filePath, startTime);
复制代码
类 Watcher
依旧继承了 events
模块的 EventEmitter
。这里实例化了一个 watcher
,而后订阅了他的 close
方法后,将该 watcher
push
到 this.watchers
,而后返回一个 watcher
,即执行 watcherManager.watchFile(file, this.watcherOptions, startTime)
返回了一个 watcher
。而后回到:
//...DirectoryWatcher.js
return this._fileWatcher(file, watcherManager.watchFile(file, this.watcherOptions, startTime));
复制代码
执行 this._fileWatcher
方法:
watcher.on(
'change',
function(mtime, type) {
this._onChange(file, mtime, file, type);
}.bind(this)
);
watcher.on(
'remove',
function(type) {
this._onRemove(file, file, type);
}.bind(this)
);
return watcher;
复制代码
即给对应的 watcher
订阅了 change
和 remove
事件。最终 this.fileWatchers
获得一个 watcher
数组。
而后回到 NodeWatchFileSystem
实例的 watch
方法执行 oldWatcher.close()
删除旧的 Watchpack
实例。
而后回到 _done
里,这一轮代码执行结束。
而后转而执行以前在 doInitialScan
里的 fs.readdir
的异步回调,收集文件修改时间(前文已解释),到此 webpack watch
的初次构建结束,文件正在被监听。
修改文件后,触发 chokidar
的 change
事件,即对应路径在 DirectoryWatcher
实例化里设置的 onChange
事件,在方法里对 path
进行验证后,执行:
this.setFileTime(filePath, mtime, false, 'change');
复制代码
再次调用了 setFileTime
方法。在方法里更新 this.files[filePath]
里对应的最新修改时间后,执行:
//DirectoryWatcher.js
if (this.watchers[withoutCase(filePath)]) {
this.watchers[withoutCase(filePath)].forEach(function(w) {
w.emit('change', mtime, type);
});
}
复制代码
判断该文件是否在 this.watchers
即在被监听之列后,对该文件的每个 watcher
触发其 change
事件,即执行:
//watchpack.js
this._onChange(file, mtime, file, type);
复制代码
方法里执行:
//watchpack.js
this.emit('change', file, mtime);
if (this.aggregateTimeout) clearTimeout(this.aggregateTimeout);
if (this.aggregatedChanges.indexOf(item) < 0) this.aggregatedChanges.push(item);
this.aggregateTimeout = setTimeout(this._onTimeout, this.options.aggregateTimeout);
复制代码
this.emit('change', file, mtime)
用于触发 this.compiler.watchFileSystem.watch
里的回调:
//Watching.js
this.compiler.hooks.invalid.call(fileName, changeTime);
复制代码
而后剩下的部分是一个标准的函数防抖(debounce),经过设置配置项 options.aggregateTimeout
能够设置间隔时间,间隔时间越长,性能越好。
执行 this._onTimeout
:
//watchpack.js
this.emit('aggregated', changes, removals);
复制代码
主要做用触发 aggregated
事件即在 NodeWatchFileSystem
里注册,执行:
//NodeWatchFileSystem.js
const times = objectToMap(this.watcher.getTimes());
复制代码
获得 times
:
{
//...map结构
0: {
"key": "/Users/github/webpack-demo/src/a.js",
"value": "1578382937093"
},
1: {
"key": "/Users/github/webpack-demo/src/a.js",
"value": "1578382937093"
},
2: {
"key": "/Users/github/webpack-demo/src/a.js",
"value": "1578382937093"
},
3: {
"key": "/Users/github/webpack-demo/src/a.js",
"value": "1578382937093"
},
4: {
"key": "/Users/github/webpack-demo/src/a.js",
"value": "1578382937093"
},
5: {
"key": "/Users/github/webpack-demo/src/a.js",
"value": "1578382937093"
}
}
复制代码
获得每一个文件的最新修改时间后,执行回调 callback
,即 Watching.js
的 this.compiler.watchFileSystem.watch
方法的倒数第二个参数方法,在方法里将 fileTimestamps
即 times
赋给 this.compiler.fileTimestamps
后,执行:
this._invalidate();
复制代码
方法里执行:
this._go();
复制代码
开启新一轮的构建。
在构建过程当中,依旧从入口开始构建,但在 moduleFactory.create
的回调里(包括 addModuleDependencies
里的 factory.create
),执行:
const addModuleResult = this.addModule(module);
复制代码
该方法除了判断 module
已加载以外,还判断了若是在 compilation
的 this.cache
存在该模块的话,则执行:
let rebuild = true;
if (this.fileTimestamps && this.contextTimestamps) {
rebuild = cacheModule.needRebuild(this.fileTimestamps, this.contextTimestamps);
}
复制代码
在方法 needRebuild
里判断模块修改时间 fileTimestamps.get(file)
与 模块构建时间 this.buildTimestamp
(在 module.build
时取得)的前后来决定是否须要从新构建模块,若修改时间大于构建时间,则须要 rebuild
,不然跳过 build
这步直接执行 afterBuild
即递归解析构建依赖。这样在监听时只 rebuild
修改过的 module
可大大提高编译过程。