最近在看webpack5的源码,此文作为笔记用于记录。内容仅限于watch过程中的事件流转及代码的调用过程,具体实现以及如何编译代码,暂且略过。目的是让需要看源码的同学可以快速厘清整个Watch源码的脉络,后续会补上watch的编译流程
watch部分源码大概有2500行左右,鄙人花了2天时间才看完,看得我头痛。这还只是冰山一角
在了解webpack的Watch系统之前,先抛出几个问题让大家思考:
- 如果让你来设计
watch机制(文件更新 -> 触发回调),你会怎么做?(要求高可用,稳定强,兼容性强) - 你和
webpack作者的设计差别在哪?有哪些不足、优点?
对于问题1,我想你可以先把页面关了,花一点时间思考下。这对你的提升将是巨大的,因为这样可以让你深刻体会到自己和大神的差距,是一个很好的锻炼过程。
把心中预想的设计,先简单写下来:
先预想10分钟~~
自行设计watch系统
假设,我这样设计,简单明了,这不是很好吗,浪费那么多那细胞写那几千行代码干啥?
class Watcher{
constructor(options={},callback){
this.options = options;
this.callback = callback;
}
watchfile(path){
this.getFiles(path).forEach(file=>{
let watcher = fs.watch(file);
watcher.on('change',this.callback);
})
}
getFiles(path){
retrun ['./file1.js','../../xxx.peg','./xxx.js','...'];
}
}
Compiler.watch = (compilation,options)=>{
let callback = ()=>{
/*...*/
compilation.rebuild()
};
return new Watcher(options,callback).watchfile(path);
}
Compiler.watch()
接下来看,我们的设计有没有覆盖以下的场景:
- 监听文件的依据是什么,比如工程目录下有各种各样的文件,如何保证只监听需要的文件(不仅仅是js文件)。这里可能想到用到配置,如果用户没配置呢?用默认配置,那默认配置如何设计呢?
- 是否考虑平台的兼容性问题,Wath?
node.js还有兼容问题?答案是:的确有。fs.watch有的系统不支持 - 超大型的目录,需要
watch的文件可能有千上万个,是否考虑了其性能问题? - 如何对接其他模块,提供怎样的接口,
watch触发回调之后的处理,callback传入什么样的参数。
很明显,以上的设计还有许多需要完善的地方。
解读webpack的watch设计
我们来欣赏下webpack的设计吧~
核心思想是通过继承EventEmitter类,通过订阅发布模型来实现。
主要有类有 Watching,Watchpack,DirectoryWatcher
还有几个辅助类 NodeWatchFileSystem,DirectWatcher,WatcherManager,Watcher ... 主要是起到桥梁的作用。
为了简单起见,我们将主要的类组织起来,大概就是这样:
Watching用于处理文件更新后的各种hooks回调
class Watching {
constructor(){
this.startTime = Date.now()
}
watch(files){
let watcher = new Watchpack();
let callback = (changes, removals)=>{
// 执行各种hooks bala bala
this.compiler.hooks.watchRun.callAsync(compiler,()=>{
let onCompiled = ()=>{ // 执行各种hooks bala bala
process.nextTick(() => {
if (!this.closed) {
this.watch(
compilation.fileDependencies,
compilation.contextDependencies,
compilation.missingDependencies
);
}
});
}
this.compiler.compile(onCompiled);
})
}
// 源码这里使用 NodeWatchFileSystem 类关联
watcher.once("aggregated",callback);
watcher.watch(files, directories, this.startTime);
return watcher;
}
}
Watchpack 用于汇总文件信息、管理不同watcher,
class Watchpack extends EventEmitter{
constructor(){
this.fileWatchers = new Map();
this.directoryWatchers = new Map();
}
watch(files, directories, startTime){
// 为每个文件添加一个或多个 DirectoryWatcher
for(let file of files){
let watcher = new DirectoryWatcher();
this.fileWatchers.set(file, watcher);
watcher.on("change", this._onTimeout);
watcher.watch(file, startTime);
}
// for of directories
// bala bala
// 源码这里的方法调用,没太明白是个啥意思?
// 后面还有一个watchEventSource.watch()
watchEventSource.batch()
}
_onTimeout() {
const changes = this.aggregatedChanges;
const removals = this.aggregatedRemovals;
this.emit("aggregated", changes, removals);
}
}
DirectoryWatcher 主要处理单个文件、目录的信息
class DirectoryWatcher extends EventEmitter {
constructor(){
this.watchers = new Map();
this.files = new Map();
this.watcher = watchEventSource.watch(file);
this.watcher.on("change", this.onWatchEvent);
}
watch(file, startTime){
let watchers = new Set();
let watcher = new Watcher(startTime);
watchers.add(watcher);
this.watchers.set(file, watchers);
}
onWatchEvent(eventType, filename){
fs.lstat(filePath, (err, stats)=>{
this.setFileTime(filePath, stats.mtime);
// this.setDirectory(filePath, eventType);
})
}
setFileTime(filePath, mtime){
// 记录文件信息
let safeTime = Date.now();
this.files.set(filePath, {
safeTime,
timestamp: mtime
})
let watchers = this.watchers.get(filePath)
for (const w of watchers) {
if (w.checkStartTime(safeTime)) {
// 文件更新后触发的事件源
w.emit("change", mtime);
}
}
}
}
class Watcher extends EventEmitter {
constructor(startTime){
this.startTime = startTime
}
checkStartTime(mtime) {
// startTime 编译器创建之后的时间
// mtime 文件变化时的时间
const startTime = this.startTime;
return startTime <= mtime;
}
}
最终的使用逻辑,大概是这样
const compiler = new Compiler(options.context, options);
compiler.watch = () => {
return new Watching(this, watchOptions, handler)
}
compiler.watch()
上面就是整个Watch经过简化后的主要逻辑,其设计架构简化后大概是这样:
事件回调处理 <--> 文件汇总处理 <--> 单文件信息处理 <--> 文件修改事件
从左往右是订阅的过程,从右往左是发布的过程。
看源码的时候有个问题一直卡住我,我们知道发布订阅模式,有两个必要的条件那就是:
- 先订阅
- 再发布
首先,订阅是在Watchpack.watch中,这个没问题,问题是没有地方调用??
经过多次调试之后发现,原来是在new的时候自动调用内部方法实现的,流程大概是这样this._invalidate -> this._done() --> this.watch() --> watcher.once("aggregated",callback)
这里面还有个小小的问题,正常订阅不应该是用on吗?那once是如何实现循环监听的呢?答案其实隐藏callback中,callback就是重复上面的调用过程this._invalidate --> ... ,回调之后会重新注册。
留个小小的疑问:作者为什么不用
on来订阅呢?
其次,事件发布在哪里触发的呢?
理论上应该是通过fs.watch来触发的,但也没看到其调用的地方,也就没有一个事件源来触发emit('change'),那上层的订阅是如何触发的呢?
还是经过断点调试才发现,由于fs.watch在某些平台上没有效果,为了兼容这种情况,作者采用了时间轮训的方式来实现,即setTimeout(this.setFileTime,timeout)(简化的代码,源码并不长这样)。最终通过对比缓存的文件信息checkStartTime,来判断是否需要触发事件,将事件源集中在下面这段代码里:
即DirectoryWatcher中的w.emit("change", mtime);
那对于可以正常使用fs.watch的平台呢,其实现大概是这样
class DirectWatcher{
constructor(filePath){
this.watchers = new Set()
const watcher = fs.watch(filePath);
watcher.on("change", (type, filename) => {
for (const w of this.watchers) {
// DirectWatcher本身不是EventEmitter对象,没有emit
// watchEventSource.watch调用后,this.watchers才存放EventEmitter
w.emit("change", type, filename);
}
});
}
add(watcher){
this.watchers.add(watcher);
}
}
可以看到上面调用了fs.watch,这也正是我们要找的事件源。
DirectWatcher中的this.watchers在实例化的时候是没有值的,要通过下面这两个方法watchEventSource.batch、watchEventSource.watch添加继承自EventEmitter类的实例,
这两个方法中的执行过程大家可以自行断点调试下,单看代码还真有点难以理解。
虽然说fs.watch有兼容性问题,但作者其实并没有写任何的兼容性代码,那是如何实现的呢?
具体的做法就是把球踢给用户。
用户:你这
watch没效果啊?
作者:你加个配置项试试~~
就这一小小的设计,看完源码只能说两个字:佩服!因为我相信绝大部分人都会通过兼容检测来实现。
最后,订阅和发布位于不同的实例对象中,那么事件是如何传递顶层的呢?
通常订阅/发布都是在同一个实例类上完成的,比如:
const eventEmitter = new EventEmitter();
eventEmitter.on(event,callback);
eventEmitter.emit(event,params);
假设有多个实例对象,例如:
const watcher1 = new EventEmitter();
watcher1.on('change',callback);
const watcher2 = new EventEmitter();
watcher2.emit('change',file);
上面这样watcher1显然无法接收到watcher2发出的事件。
从webpack的源码可以看出,其事件的订阅和发布也是分散在不同的实例中,尤其最后部署fs.watch时,每个文件都有一个或多个watcher。
要实现不同事件对象之前的通信,那么必须在中间架设一座桥梁,这也是源码中比较绕的地方。
作者的做法主要是通过实例传参、实例映射2种方式来实现的,比如:
方式1
const watcher1 = new EventEmitter();
watcher1.on('change',callback);
const watcher2 = new EventEmitter(watcher1);
watcher2.watcher = watcher1;
watcher2.on('change',(file)=>{
watcher2.watcher.emit('change',file);
});
方式2
const parentWatcher = new EventEmitter();
parentWatcher.on('change',callback);
// parentWatcher 通过方式1传递到当前上下文
const eventMap = new Map();
for(let file of files){
let watcher = new EventEmitter();
eventMap.set(file, watcher);
watcher.on('change',(file)=>{
parentWatcher.emit('change', file);
});
}
// 事件源change
const watcher = fs.watch('file');
watcher.on('change',(file)=>{
const watcher = eventMap.get(file);
watcher.emit('change', file);
})
以上就是整个watch系统的基本原理介绍,想了解更多信息还请看webpack官方源码
watch源码完整流程参考
以下是各个类的方法调用过程,以及类之间的依赖关系。看源码的时候可以参考下面的顺序由浅入深。
ps:不对的地方还请指出来~~
options.watch = true;
webpack(options);
1. Compiler --> new Compiler() --> Compiler.watchFileSystem = new NodeWatchFileSystem()
--> Compiler.watch()
--> this.watching = new Watching()
2. Watching --> new Watching() --> this._invalidate() --> this._go() --> this._done()
--> this.watch()
--> Compiler.watchFileSystem.watch(callback)
3. NodeWatchFileSystem --> this.watcher = new Watchpack(watcherOptions)[EventEmitter]
--> this.watcher.once('aggregated',callback)
--> this.watcher.watch({files, directories})
4. Watchpack[EventEmitter] --> this.watcherManager = new WatcherManager()
--> watcher = new DirectoryWatcher()[EventEmitter].watch()
--> this.watcherManager.watchFile() --> new WatchpackFileWatcher(watcher)
--> this.watcherManager.watchDirectory() --> new WatchpackDirectoryWatcher(watcher)
--> watcher.on('change',this._onTimeout())
--> watcher.emit('aggregated')
5. DirectoryWatcher[EventEmitter] --> this.watch(path)
--> this.watchers = new Map();this.watchers[path] = new Set([new Watcher(), ...])
--> watchEventSource.watch() --> new [DirectWatcher | RecursiveWatcher](filePath)
--> this.on('change',this.onWatchEvent)
--> this.doScan() --> this.setFileTime() | this.setDirectory()
--> Watcher.checkStartTime() === true --> this.emit("change)
6. DirectWatcher --> new DirectWatcher(filePath) --> watcher = fs.watch(filePath)
--> watcher.on('change',Watcher.emit('change'))
7. RecursiveWatcher --> new RecursiveWatcher(filePath) --> watcher = fs.watch(filePath)
--> watcher.on('change',Watcher.emit('change'))
最后,以上是暂时的一些心得,后面如果有新的收获再补上~
参考链接: