webpack 4 源码主流程分析(十三):watch
前面分析了 webpack
的普通主流程构建,另外,通过设置 watch
模式,webpack
可以监听文件变化,当它们修改后会重新编译。文档
webpack-dev-server
和webpack-dev-middleware
里Watch
模式默认开启。
接下来设置 cli
命令加上 --watch
之后 对 watch
模式下的主流程进行分析(mode = development
)。
初次构建
资源构建
代码执行后,跟主流程类似,执行到之前文章介绍到的 编译前的准备 -> 回到 cli.js
里,读取到 options.watchOptions
等 watch
配置后,走 compiler.watch
:
1 | //... |
该方法初始化一些属性后,new
一个 Watching
实例并返回。
在 Watching
实例化的过程中(文件 webpack/lib/Watching.js
),先对 watchOptions
进行了处理后,在 compiler.readRecords
的回调里执行 _go
:
1 | //Watching.js |
_go
与 Compiler
里的 run
很类似。 在 _go
里,触发 compiler.hooks
:watchRun
,执行插件 CachePlugin
设置 this.watching = true
。与 webpack
普通构建一致,在钩子 watchRun
回调里执行 compiler.compile
开始构建,在资源构建结束后执行 onCompiled
。
1 | // Watching.js |
onCompiled
方法与 compiler.run
里的 onCompiled
大致一致,不同点是所有回调由 finalCallback
改为 _done
,并且将 stats
统计信息相关处理也放到了 _done
里:
1 | //... Watching.js |
在该方法里对 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
,该方法里执行:
1 | //watchpack.js |
这里循环对每一个 file
进行执行 this._fileWatcher
。
一般情况的监听只会涉及
this._fileWatchers
,目录类的this._dirWatchers
会在require.context
的情况下被监听。
其中 watcherManager.watchFile
里执行:
1 | //watcherManager.js |
其中 getDirectoryWatcher
根据文件对应目录路径 directory
,实例化不同的 DirectoryWatcher
并执行 watch
方法。
DirectoryWatcher
与 Watchpack
一样也继承了 events
模块的 EventEmitter
,在实例化的过程中执行:
1 | //DirectoryWatcher.js |
webpack
采用 npm
包 chokidar 来进行文件夹的监听,然后根据不同操作(增加,删除,修改等)绑定事件后,执行 this.doInitialScan
读取该 path
(文件对应的文件夹路径 directory
)下的所有文件及文件夹,如果是文件则执行 this.setFileTime
根据是否是首次 watch
来收集该文件的修改时间;如果是文件夹则执行 this.setDirectory
记录所有子路径。
因为 fs.readdir
为异步,先回到 this.getDirectoryWatcher(directory, options).watch(p, startTime)
中执行 watch
,方法里执行:
1 | //...DirectoryWatcher.js |
类 Watcher
依旧继承了 events
模块的 EventEmitter
。 这里实例化了一个 Watcher
,然后订阅了他的 close
方法后,将该 watcher
push
到 this.watchers
,然后返回一个 watcher
实例。
然后回到:
1 | //watchpack.js |
执行 this._fileWatcher
给对应的 watcher
订阅了 change
和 remove
事件。最终 this.fileWatchers
得到一个 watcher
数组。
然后回到 _done
里,这一轮代码执行结束。
然后转而执行之前在 doInitialScan
里的 fs.readdir
的异步回调,收集文件修改时间,到此 webpack watch
的初次构建结束,文件正在被监听。
修改文件触发监听
修改文件后,触发 chokidar
的 change
事件,即 this.onChange
,在方法里对 path
进行验证后,执行 this.setFileTime
。在方法里更新 this.files[filePath]
里对应的最新修改时间后,执行:
1 | //DirectoryWatcher.js |
判断该文件是否在 this.watchers
即在被监听之列后,对该文件的每一个 watcher
触发其 change
,即执行在 _fileWatcher
里注册的事件:
1 | //watchpack.js |
方法里执行:
1 | //watchpack.js |
函数防抖(debounce),通过设置配置项 options.aggregateTimeout
可以设置间隔时间,间隔时间越长,性能越好。
执行 this._onTimeout
里触发 aggregated
事件 (NodeWatchFileSystem
里注册),执行:
1 | //NodeWatchFileSystem.js |
得到 times
:
1 | { |
得到每个文件的最新修改时间后,执行回调 callback
,即 Watching.js
的 this.compiler.watchFileSystem.watch
方法的倒数第二个参数方法,在方法里将 fileTimestamps
即 times
赋给 this.compiler.fileTimestamps
后,执行 this._invalidate
即执行 this._go
开启新一轮的构建。
watch 优化
在构建过程中,依旧从入口开始构建,但在 moduleFactory.create
的回调里(包括依赖构建 addModuleDependencies
里的 factory.create
),执行:
1 | const addModuleResult = this.addModule(module); |
该方法除了判断 module
已加载之外,还判断了如果在 compilation
的 this.cache
存在该模块的话,则执行:
1 | let rebuild = true; |
在方法 needRebuild
里判断模块修改时间 fileTimestamps.get(file)
与 模块构建时间 this.buildTimestamp
(在 module.build
时取得)的先后来决定是否需要重新构建模块,若修改时间大于构建时间,则需要 rebuild
,否则跳过 build
这步直接执行 afterBuild
即递归解析构建依赖。这样在监听时只 rebuild
修改过的 module
可大大提升编译过程。