原文首發於 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
可大大提高編譯過程。