源码分析(十三):watch
前面分析了 webpack
的普通主流程构建,另外,通过设置 watch
模式,webpack
可以监听文件变化,当它们修改后会重新编译。文档
webpack-dev-server
和webpack-dev-middleware
里Watch
模式默认开启。
接下来设置 cli
命令加上 --watch
之后 对 watch
模式下的主流程进行分析(mode = development
)。
初次构建
资源构建
代码执行后,跟主流程类似,执行到之前文章介绍到的 编译前的准备 -> 回到 cli.js
里,读取到 options.watchOptions
等 watch
配置后,走 compiler.watch
:
//...
compiler.watch(watchOptions, compilerCallback);
该方法初始化一些属性后,new
一个 Watching
实例并返回。
在 Watching
实例化的过程中(文件 webpack/lib/Watching.js
),先对 watchOptions
进行了处理后,在 compiler.readRecords
的回调里执行 _go
:
//Watching.js
this.compiler.readRecords((err) => {
//...
this._go();
});
_go
与 Compiler
里的 run
很类似。 在 _go
里,触发 compiler.hooks
:watchRun
,执行插件 CachePlugin
设置 this.watching = true
。与 webpack
普通构建一致,在钩子 watchRun
回调里执行 compiler.compile
开始构建,在资源构建结束后执行 onCompiled
。
// Watching.js
_go(){
//...
this.compiler.hooks.watchRun.callAsync(this.compiler, err => {
//...
const onCompiled = (err, compilation) => {
//...
};
this.compiler.compile(onCompiled);
}
}
onCompiled
方法与 compiler.run
里的 onCompiled
大致一致,不同点是所有回调由 finalCallback
改为 _done
,并且将 stats
统计信息相关处理也放到了 _done
里:
//... Watching.js
_done(err, compilation){
//...
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
设置后,compiler.hooks
: done
的回调里执行 this.handler
(实际与 finalCallback
功能一致) 即 compilerCallback
,在 cli
里打印出构建相关的信息。到此,初始化构建完毕。
添加监听
接着执行 this.watch
并传入 fileDependencies, contextDependencies, missingDependencies
(compilation.seal
里 this.summarizeDependencies
生成) 这些需要监听的文件和目录。
this.watch
即执行 this.compiler.watchFileSystem.watch
即 NodeWatchFileSystem
的实例 watch
方法( 文件 webpack/lib/node/NodeWatchFileSystem.js
,NodeEnvironmentPlugin
里所设置),方法里先对参数进行了格式判断后,实例化了 Watchpack。Watchpack
继承了 events
模块的 EventEmitter
,然后在 this.watcher
(Watchpack
实例) 上注册了 change,aggregated
事件后,执行 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.js
var directory = path.dirname(p);
return this.getDirectoryWatcher(directory, options).watch(p, startTime);
其中 getDirectoryWatcher
根据文件对应目录路径 directory
,实例化不同的 DirectoryWatcher
并执行 watch
方法。
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 来进行文件夹的监听,然后根据不同操作(增加,删除,修改等)绑定事件后,执行 this.doInitialScan
读取该 path
(文件对应的文件夹路径 directory
)下的所有文件及文件夹,如果是文件则执行 this.setFileTime
根据是否是首次 watch
来收集该文件的修改时间;如果是文件夹则执行 this.setDirectory
记录所有子路径。
因为 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
实例。
然后回到:
//watchpack.js
//...
return this._fileWatcher(file, watcherManager.watchFile(file, this.watcherOptions, startTime)); // watcherManager.watchFile 返回一个 watcher 实例
执行 this._fileWatcher
给对应的 watcher
订阅了 change
和 remove
事件。最终 this.fileWatchers
得到一个 watcher
数组。
然后回到 _done
里,这一轮代码执行结束。
然后转而执行之前在 doInitialScan
里的 fs.readdir
的异步回调,收集文件修改时间,到此 webpack watch
的初次构建结束,文件正在被监听。
修改文件触发监听
修改文件后,触发 chokidar
的 change
事件,即 this.onChange
,在方法里对 path
进行验证后,执行 this.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
,即执行在 _fileWatcher
里注册的事件:
//watchpack.js
this._onChange(file, mtime, file, type);
方法里执行:
//watchpack.js
this.emit('change', file, mtime); // 触发 `this.compiler.watchFileSystem.watch` 里的回调:this.compiler.hooks.invalid.call(fileName, changeTime)
if (this.aggregateTimeout) clearTimeout(this.aggregateTimeout);
if (this.aggregatedChanges.indexOf(item) < 0) this.aggregatedChanges.push(item);
this.aggregateTimeout = setTimeout(this._onTimeout, this.options.aggregateTimeout);
函数防抖(debounce),通过设置配置项 options.aggregateTimeout
可以设置间隔时间,间隔时间越长,性能越好。
执行 this._onTimeout
里触发 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
开启新一轮的构建。
watch 优化
在构建过程中,依旧从入口开始构建,但在 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
可大大提升编译过程。