解決掉了最頭疼的DirectoryWatcher內部實現,這一節可以結束NodeWatchFileSystem模塊。 關於watch的應用場景,仔細思考了下,這不就是熱重載的核心嘛。 首先是監視文件,觸發文件change事件後收集變動文件信息,重新進行打包,更新JS後觸發頁面重新渲染,perfect ...
解決掉了最頭疼的DirectoryWatcher內部實現,這一節可以結束NodeWatchFileSystem模塊。
關於watch的應用場景,仔細思考了下,這不就是熱重載的核心嘛。
首先是監視文件,觸發文件change事件後收集變動文件信息,重新進行打包,更新JS後觸發頁面重新渲染,perfect!
首先重新回憶一下NodeWatchFileSystem模塊:
"use strict"; const Watchpack = require("watchpack"); class NodeWatchFileSystem { constructor(inputFileSystem) { this.inputFileSystem = inputFileSystem; this.watcherOptions = { aggregateTimeout: 0 }; this.watcher = new Watchpack(this.watcherOptions); } watch(files, dirs, missing, startTime, options, callback, callbackUndelayed) { // ... const oldWatcher = this.watcher; this.watcher = new Watchpack(options); // 當新監視器生成時立即調用的函數 if (callbackUndelayed) this.watcher.once("change", callbackUndelayed); // callback在這裡調用 this.watcher.once("aggregated", (changes, removals) => { /**/ }); // 分別傳入 文件目錄數組 文件夾目錄數組 時間標記 this.watcher.watch(files.concat(missing), dirs.concat(missing), startTime); // 關閉舊監視器 if (oldWatcher) { oldWatcher.close(); } return { close: () => { if (this.watcher) { this.watcher.close(); this.watcher = null; } }, pause: () => { if (this.watcher) { this.watcher.pause(); } } }; } } module.exports = NodeWatchFileSystem;
在構造函數中會初始化一個Watchpack實例賦給watcher,每一次調用watch方法會重新生成一個的watcher並同時關閉舊的watcher。
模塊只有一個原型方法watch,參數解析如下:
1.files、dirs、missing為文件路徑相關
2.options為初始化watchpack實例的參數
3.callback與callbackUndelayed為回調函數
這裡有兩個回調函數,一個在新監視器生成時立即調用,一個在監視器觸發aggregated事件時調用。
返回的對象有兩個方法,一個用來關閉監視器,一個用來暫停監視器。
下麵看Watchpack模塊源碼,先從構造函數開始講解:
class Watchpack { constructor(options) { EventEmitter.call(this); // 參數處理 if (!options) options = {}; // 設置定時器參數預設值 if (!options.aggregateTimeout) options.aggregateTimeout = 200; this.options = options; this.watcherOptions = { ignored: options.ignored, poll: options.poll }; // 文件監視器容器 this.fileWatchers = []; // 文件夾監視器容器 this.dirWatchers = []; // 指定文件修改時間容器 this.mtimes = Object.create(null); // 暫停標記 this.paused = false; // 定時器ID收集容器 this.aggregatedChanges = []; this.aggregatedRemovals = []; // 本地定時器參數 this.aggregateTimeout = 0; this._onTimeout = this._onTimeout.bind(this); }; // prototype methods... }
大體上可分為容器、標記、參數三部分。
容器包括文件與文件夾的監視器容器以及幾個定時器ID相關的容器,標記只有一個暫停標記,參數為定時器的時間參數。
下麵是核心方法watch,源碼整理如下:
Watchpack.prototype.watch = function watch(files, directories, startTime) { // 暫停標記置false this.paused = false; // 取出舊的監視器 var oldFileWatchers = this.fileWatchers; var oldDirWatchers = this.dirWatchers; // 分別調用watchFile與watchDirectory對文件與文件夾進行監視 // 將監視器賦值給對應的容器 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); // 關閉所有舊的監視器 oldFileWatchers.forEach(function(w) { w.close(); }, this); oldDirWatchers.forEach(function(w) { w.close(); }, this); };
過了watchManager模塊後,這裡就變得十分簡單明瞭,分別取出數組的目錄元素,分別進行監視操作,將返回的監視器數組賦值給容器。
同樣,每一次調用watch會關閉所有舊的監視器。
接下里是關於file與dir的不同處理:
_fileWatcher
// 傳入文件路徑與watcher Watchpack.prototype._fileWatcher = function _fileWatcher(file, watcher) { // 綁定change、remove事件的觸發事件 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; };
如果看了上一節會發現,DirectoryWatcher模塊內部源碼只有emit觸發事件,並沒有任何on來處理事件。
這裡就是處理模塊內部事件觸發的地方,觸發change調用本地的_onchange方法,觸發remove調用本地的_onRemove方法,參數沒有什麼解釋的。
_dirWatcher
Watchpack.prototype._dirWatcher = function _dirWatcher(item, watcher) { // 只是觸發change事件 watcher.on("change", function(file, mtime, type) { this._onChange(item, mtime, file, type); }.bind(this)); return watcher; };
文件夾只有增加和刪除,一個change事件就足夠了。
_onChange
// item、file都是文件路徑 Watchpack.prototype._onChange = function _onChange(item, mtime, file) { file = file || item; // 新增或更新對應文件的修改時間 this.mtimes[file] = mtime; // 暫停時不觸發change事件 if (this.paused) return; 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); };
變動的文件信息會被加入到對應的容器,最後會調用一個定時器,定時器間隔為穿進來的參數。
_onRemove
Watchpack.prototype._onRemove = function _onRemove(item, file) { file = file || item; // 刪除容器中對應的文件信息 delete this.mtimes[item]; if (this.paused) return; // 觸發remove事件 this.emit("remove", item); if (this.aggregateTimeout) clearTimeout(this.aggregateTimeout); // 刪除文件的信息加入容器 if (this.aggregatedRemovals.indexOf(item) < 0) this.aggregatedRemovals.push(item); // 觸發aggregated事件 this.aggregateTimeout = setTimeout(this._onTimeout, this.options.aggregateTimeout); };
_onTimeout
Watchpack.prototype._onTimeout = function _onTimeout() { // 定時器ID置0 this.aggregateTimeout = 0; // 變動與刪除的文件信息數組 var changes = this.aggregatedChanges; var removals = this.aggregatedRemovals; // 清空 this.aggregatedChanges = []; this.aggregatedRemovals = []; // 觸發aggregated事件 this.emit("aggregated", changes, removals); };
簡單概括就是會在給定時間後調觸發aggregated事件,將變動與刪除的文件信息數組作為參數傳遞出去並清空數組。
總體來說,文件的增加與內容修改會觸發change事件,刪除會觸發remove事件。文件夾只有change事件。無論是觸發change還是remove,都會將對應的文件信息用aggregated事件傳遞出去。
調用pause方法時,所有的操作將不會觸發任何事件,但是文件修改信息仍然會被收集。
值得註意的是,源碼內部並沒有任何繼續監視的方法,雖然有一個resume函數,但是:
Watchpack.prototype.close = function resume() { /**/ }
看到沒,假的,雖然名字叫resume,但是實際上關掉了監視。繼續監視唯一的辦法是重新調用watch方法,但是會清空所有watcher容器並重新生成一批新的。也就是說,pause相當於stop。
源碼中還有個getTimes的原型方法,有興趣自己去看,暫時不講了。
最後來用小案例模擬這些模塊的使用,目錄如圖:
測試代碼如下:
// 模塊引入 // 我都複製過來了! const Watchpack = require('./lib/watchpack'); const fs = require('fs'); const path = require('path'); // 實例化一個Watchpack類 不傳參 const el = new Watchpack(); // 需要監視的文件夾 const rootPath = path.join(process.cwd(), 'test'); fs.readdir(rootPath, (err, items) => { // 文件夾中的文件全部做監視 items = items.map((v) => path.join(rootPath, v)); // 對所有文件做監視 el.watch(items, [], 1); }); // 監視change事件 el.on('change', (...args) => { console.log('Detect file change\nthe filename is:' + args[0] + '\nthe filename mtime is:' + args[1]); }); // 監視remove事件 el.on('remove', (...args) => { console.log('Detect file remove\nthe filename is:' + args[0]); });
這裡暫時先不對文件夾進行監視,遍歷test文件夾,將所有文件路徑包裝成數組傳入watch方法(第三個參數真不懂啥意思,傳1反正沒錯)。
在node指令執行的時候,就會列印出一連串的信息:
在初始化的時候,每一次生成一個監視器,就會先觸發一次change事件,並初始化文件的mtime,觸發的源碼如下:
DirectoryWatcher.prototype.setFileTime = function setFileTime(filePath, mtime, initial, type) { // ... if (!old) { if (mtime) { // watch方法會設置該屬性 if (this.watchers[withoutCase(filePath)]) { this.watchers[withoutCase(filePath)].forEach(function(w) { if (!initial || w.checkStartTime(mtime, initial)) { // 觸發事件 w.emit("change", mtime, initial ? "initial" : type); } }); } } } // ... }
這個地方的事件只會在初始化的時候被調用。
這裡有一個小問題,在每一次初始化的時候會進行doInitScan掃描,掃描的文件信息會被填充到files容器中,即
// files ['D:\\workspace\\doc\\test\\a.js', 'D:\\workspace\\doc\\test\\b.js', 'D:\\workspace\\doc\\test\\c.js']
然後在watch方法有這麼一段代碼:
DirectoryWatcher.prototype.watch = function watch(filePath, startTime) { //... var data; if (filePath === this.path) { /**/ } // 獲取文件 else { data = this.files[filePath]; } process.nextTick(function() { if (data) { var ts = data[0] === data[1] ? data[0] + FS_ACCURACY : data[0]; // 這裡的startTime為1 肯定能進去 if (ts >= startTime) watcher.emit("change", data[1]); } // ... }.bind(this)); return watcher; };
watch方法會在後面會嘗試獲取容器中的文件信息並處罰change事件,理論上這裡會觸發兩次change,然而實際上只有一次。
原因就在初始化掃描時候使用了async模塊的方法,即:
fs.readdir(this.path, function(err, items) { // 掃描文件並將信息填入容器中 async.forEach(items, function(item, callback) { // ... }) });
該模塊的方法全是非同步調用,所以在watch方法調用的第一時刻,此時初始化掃描還在進行中,files容器仍然為空,在watch與doInitScan方法中加log,可以發現:
在watch方法完成後,掃描才開始。
掃描開始後,進程掛起等待文件操作行為,這裡分別對文件進行各種操作:
修改文件內容
觸發了change事件。
刪除文件
觸發了remove事件。
修改文件名
這裡僅僅觸發了remove事件。
原因在於,這個操作被系統認為是刪除一個文件再增加一個文件,但是文件增加在監視文件時是不會觸發任何事件的,也不會生成該文件的watcher,只會將該文件信息收集進files容器中,這個在之前講過。
註意,順序是先刪後增,這裡可以簡單的log一下,因為在setFileTime中傳了對應的事件類型,雖然沒有用上,這裡測試可以用用:
改名後,列印:
過程為,先觸發了觸發unlink事件,將文件刪除,然後將新文件的信息加入到files容器中,然後觸發文件的change事件。
接下來是文件夾監視操作,測試代碼如下:
// 模塊引入 // 我都複製過來了! const Watchpack = require('./lib/watchpack'); const fs = require('fs'); const path = require('path'); // 實例化一個Watchpack類 不傳參 const el = new Watchpack(); // 需要監視的文件夾 const directory = path.join(process.cwd(), 'test'); el.watch([], [directory], 1); // 監視change事件 el.on('change', (...args) => { console.log('change: ' + args.join(',')); });
註意,文件夾無論怎樣都只會觸發change事件。這裡搞兩張圖片特別累,所以直接展示傳過來的參數。
新建文件
文件路徑與修改時間。
刪除文件
只有文件路徑,因為文件被刪了。
文件改名
這個地方事就多了,這三處觸發全部來源於setFileTime方法中:
DirectoryWatcher.prototype.setFileTime = function setFileTime(filePath, mtime, initial, type) { // ... if (this.watchers[withoutCase(this.path)]) { this.watchers[withoutCase(this.path)].forEach(function(w) { if (!initial || w.checkStartTime(mtime, initial)) { w.emit("change", filePath, mtime, initial ? "initial" : type); } }); } }; DirectoryWatcher.prototype.watch = function watch(filePath, startTime) { this.watchers[withoutCase(filePath)] = this.watchers[withoutCase(filePath)] || []; var watcher = new Watcher(this, filePath, startTime); this.watchers[withoutCase(filePath)].push(watcher); // ... }
因為watch的是一個文件夾,所以在watcher容器中會有對應的鍵,所以任何文件的變動都會觸發文件夾的change事件。
這裡改文件名會涉及:刪除文件觸發一次,增加文件觸發一次,change事件觸發一次。
至此,基本上該watch模塊的內容基本處理完畢,撒花!