webpack 4 源碼主流程分析(十三):watch

原文首發於 blog.flqin.com。若有錯誤,請聯繫筆者。分析碼字不易,轉載請代表出處,謝謝!html

前面分析了 webpack 的普通主流程構建,另外,經過設置 watch 模式,webpack 能夠監聽文件變化,當它們修改後會從新編譯。文檔webpack

webpack-dev-serverwebpack-dev-middlewareWatch 模式默認開啓。git

接下來設置 cli 命令加上 --watch 以後 對 watch 模式下的主流程進行分析(mode = development)。github

初次構建

資源構建

代碼執行後,跟主流程相似,而後執行到以前文章介紹到的 編譯前的準備 -> 回到 cli.js 裏,讀取到 options.watchOptionswatch 配置後, 走 compiler.watchweb

//...
compiler.watch(watchOptions, compilerCallback);
複製代碼

complier 裏的 watch 方法裏,new 一個 Watching 實例:npm

//...
return new Watching(this, watchOptions, handler); //handler即compilerCallback
複製代碼

來到文件 Watching.js,在 Watching 實例化的過程當中,先對 watchOptions 進行了處理後,在 compiler.readRecords 的回調裏執行 _gojson

//...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 裏,執行 _doneasync

//... 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.sealthis.summarizeDependencies 方法裏生成的 this.fileDependencies, this.contextDependencies, this.missingDependencies 這些須要監聽的文件和目錄。

Watching 的實例 watch 方法裏僅僅執行 this.compiler.watchFileSystem.watchwatchFileSystem 便是在前文 NodeEnvironmentPlugin 裏所設置的 NodeWatchFileSystem 的實例。

NodeWatchFileSystem 的實例 watch 方法裏,先對參數進行了格式判斷後,而後執行:

//NodeWatchFileSystem.js
const oldWatcher = this.watcher;
this.watcher = new Watchpack(options);
複製代碼

this.watcherNodeWatchFileSystem 實例化的時候已經建立了一個 Watchpack 的實例,這裏至關於從新建立了一個實例。

Watchpack 繼承了 events 模塊的 EventEmitter,因此接下來分別在 this.watcherWatchpack 實例) 上註冊了 changeaggregated 事件,而後執行:

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 爲目錄路徑,valueDirectoryWatcher 實例的對象。

可見 this.getDirectoryWatcher 返回了一個參數爲目錄路徑和配置的 DirectoryWatcher 實例。

DirectoryWatcherWatchpack 同樣,也 繼承了 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 採用 npmchokidar 來進行文件的監聽,而後根據不一樣操做(增長,刪除,修改等)綁定一些事件:

//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 pushthis.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 訂閱了 changeremove 事件。最終 this.fileWatchers 獲得一個 watcher 數組。

而後回到 NodeWatchFileSystem 實例的 watch 方法執行 oldWatcher.close() 刪除舊的 Watchpack 實例。

而後回到 _done 裏,這一輪代碼執行結束。

而後轉而執行以前在 doInitialScan 裏的 fs.readdir 的異步回調,收集文件修改時間(前文已解釋),到此 webpack watch 的初次構建結束,文件正在被監聽。

修改文件觸發監聽

修改文件後,觸發 chokidarchange 事件,即對應路徑在 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.jsthis.compiler.watchFileSystem.watch 方法的倒數第二個參數方法,在方法裏將 fileTimestampstimes 賦給 this.compiler.fileTimestamps 後,執行:

this._invalidate();
複製代碼

方法裏執行:

this._go();
複製代碼

開啓新一輪的構建。

watch 優化

在構建過程當中,依舊從入口開始構建,但在 moduleFactory.create 的回調裏(包括 addModuleDependencies 裏的 factory.create),執行:

const addModuleResult = this.addModule(module);
複製代碼

該方法除了判斷 module 已加載以外,還判斷了若是在 compilationthis.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 可大大提高編譯過程。

相關文章
相關標籤/搜索