[FE] webpack群俠傳(九):watch

本系列文章記錄了我在webpack源碼學習過程中遇到的事情,
正如前幾篇文章介紹的那樣,
一路上我遇到了很多“江湖人物”。

例如,Compiler,Compilation,loader-runner,babel-loader,
tapable,uglifyjs-webpack-plugin,worker-farm,cacahe,extract-text-webpack-plugin,等等。

所以我們可以說,webpack江湖是由這些“人物”組成的,而不是由文本組成的,
這正是面向對象編程,和模塊化編程的精髓所在。

就好比金庸先生的武俠小說,
引人入勝的故事情節,離不開鮮活的人物形象

在代碼的世界中,
我們看到的各種“人物”,也是真實存在的,
它們反映了作者對信息組織方式的理解和認知。

故事由哪些人物組成,主線劇情是什么,
哪些情節要詳細介紹,哪些應該略過不表,
這些都是把故事講清楚而不得不考慮的事情。

本文我們繼續學習webpack源碼,
了解webpack是怎樣watch文件變更的。

1. 修改npm scripts

1.1 加入watch命令

我們修改debug-webpack項目的package.json,增加一個新的npm scripts,

{
  ...
  "scripts": {
    ...
    "watch": "webpack --watch"
  },
  ...
}

這樣我們就可以使用npm run watch來調用 node_modules/.bin/webpack --watch了。

1.2 執行watch

我們在項目根目錄中,執行 npm run watch

$ npm run watch

> debug-webpack@1.0.0 watch ~/Test/debug-webpack
> webpack --watch


webpack is watching the files…

Hash: 2e91628041d9a877f709
Version: webpack 4.20.2
Time: 347ms
Built at: 2018-10-25 10:50:27
   Asset       Size  Chunks             Chunk Names
index.js  937 bytes       0  [emitted]  index
Entrypoint index = index.js
[0] ./src/index.js 8 bytes {0} [built]

命令執行完之后,并沒有退出,
它會監控源碼文件,然后只對改變的文件進行重編譯。

1.3 修改源代碼

我們修改一下src/index.js文件,把內容改成,

alert(1);

然后保存。
我們發現命令行中,在以上輸出內容的尾部,又增加了如下信息,

Hash: 3d9c84dc401a1a18ea6b
Version: webpack 4.20.2
Time: 238ms
Built at: 2018-10-25 10:53:51
   Asset       Size  Chunks             Chunk Names
index.js  938 bytes       0  [emitted]  index
Entrypoint index = index.js
[0] ./src/index.js 9 bytes {0} [built]

其中Hash值發生了變化。

2. webpack watch流程

2.1 回顧compiler.run

第三篇文章中,我們知道,
npm run build,調用了node_modules/.bin/webpack,它是一個軟鏈接,
原身在 node_modules/_webpack@4.20.2@webpack/bin/webpack.js。

然后 webpack/bin/webpack.js require了 webpack-cli/bin/cli.js,
webpack-cli中引用了webpack模塊,然后調用了compiler.run

2.2 webpack-cli調用compiler.watch

npm run build不同是,npm run watch會帶參數 --watch 調用 node_modules/.bin/webpack,

$ node_modules/.bin/webpack --watch

這樣會影響 webpack-cli的代碼邏輯,
重新分析 webpack-cli/bin/cli.js ,我們發現在 第518行,判斷了是否處于watch模式

if (firstOptions.watch || options.watch) {
    ...
    compiler.watch(watchOptions, compilerCallback);
    ...
} else compiler.run(compilerCallback);

如果處于watch模式,就調用compiler.watch
通過寫log我們得到watchOptions的值為true

2.3 如何debug

(1)新建debug.js

const webpack = require('webpack');
const options = require('./webpack.config');

const compiler = webpack(options);
compiler.watch(true, (...args) => { });

(2)作為node腳本執行

$ node debug.js

結果命令行什么也沒輸出,也沒有返回,卡在了那里。

(3)修改源代碼
現在我們修改一下 src/index.js,然后保存,

alert(2);

(4)檢查編譯結果
打開 dist/index.js ,文件內容如下,

!function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=0)}([function(e,t){alert(2)}]);

我們看到它已經更新了。

(5)調試
這說明debug.js是有效的,我們復現了watch過程,
接下來我們就可以在compiler.watch位置打斷點,
跟蹤watch代碼邏輯了。

進行單步調試,流程跳轉到了 Compiler.js 第189行watch 方法中。

2.4 watch循環

(1)Watching類

查看Compiler.js 第189行watchCompiler類的一個實例方法,

watch(watchOptions, handler) {
    ...
    return new Watching(this, watchOptions, handler);
}

其中Watching 是在 webpack/bin/Watching.js 中實現的。

(2)compiler.readRecords

Watching構造函數調用了this.compiler.readRecords

class Watching {
    constructor(compiler, watchOptions, handler) {
        ...
        this.compiler.readRecords(err => {
            ...
            this._go();
        });
    }
}

readRecords位于Compiler.js 第393行

readRecords(callback) {
    if (!this.recordsInputPath) {
        ...
        return callback();
    }
    ...
}

它判斷了,compiler.recordsInputPath這個屬性,
在我們的例子中,它為undefined,于是直接調用callback返回了。

this.compiler.readRecords返回后,會調用this._go();

(3)watching._go
this._goWatching類的實例方法,位于Watching.js 第36行

_go() {
    ...
    this.compiler.hooks.watchRun.callAsync(this.compiler, err => {
        ...
        this.compiler.compile(onCompiled);
    });
}

它會先調用compiler.hooks.watchRun,然后再調用compiler.compile方法。
compiler.compile方法我們已經很熟悉了,它會先make然后在seal。

(4)onCompiled
onCompiledcompiler.compile做完之后的回調,它會處理把文件內容實際寫到文件中的邏輯。

const onCompiled = (err, compilation) => {
    ...
    this.compiler.emitAssets(compilation, err => {
        ...
        this.compiler.emitRecords(err => {
            ...
            return this._done(null, compilation);
        });
    });
};

最終調用了this._done,它是Watching類的實例方法,位于Watching.js 第88行

_done(err, compilation) {
    ...
    this.compiler.hooks.done.callAsync(stats, () => {
        ...
        if (!this.closed) {
            this.watch(
                ...
            );
        }
        ...
    });
}

this._done里面會觸發compiler.hooks.done,表示編譯完成了,
然后調用this.watch開始監控文件的變更。

(5)循環

this.watchWatching類的一個方法,位于Watching.js 第113行

watch(files, dirs, missing) {
    ...
    this.watcher = this.compiler.watchFileSystem.watch(
        ...
        (
            ...
        ) => {
            ...
            this._invalidate();
        },
        (fileName, changeTime) => {
            this.compiler.hooks.invalid.call(fileName, changeTime);
        }
    );
}

在文件發生變化時,會調用它的最后一個回調,從而觸發compiler.hooks.invalid這個hooks。
我們可以拿到發生變更的文件名fileName,和變更時間changeTime

我們在這里打個斷點,然后修改一下src/index.js再保存,會發現程序會跳轉到這里,
fileName的值為,

~/Test/debug-webpack/src/index.js

changeTime的值是一個時間戳,

1540440595000

這個hooks執行完之后,程序會跳轉到this.compiler.watchFileSystem.watch的第一個回調中,
調用this._invalidate(); ,然后在_invalidate中又調用了this._go(); 對源碼進行重編譯再寫入到文件中,
最后回到this._done,調用this.watch重新監控。

_invalidate方法,位于 Watching.js 第155行

_invalidate() {
    ...
    if (...) {
        ...
    } else {
        this._go();
    }
}

3. watch原理

3.1 NodeEnvironmentPlugin

那么webpack到底是怎樣監控文件變更的呢?

Watching.js 第115行Watching類的watch方法中調用了,this.compiler.watchFileSystem.watch

watch(files, dirs, missing) {
    ...
    this.watcher = this.compiler.watchFileSystem.watch(
        ...
        (
           ...
        ) => {
            ...
            this._invalidate();
        },
        (fileName, changeTime) => {
            this.compiler.hooks.invalid.call(fileName, changeTime);
        }
    );
}

然而我們在Compiler.js中卻找不到watchFileSystem的定義。
通過全文搜索,我們發現watchFileSystem屬性,是由lib/node/NodeEnvironmentPlugin.js 添加上去的。

class NodeEnvironmentPlugin {
    apply(compiler) {
        ...
        compiler.watchFileSystem = new NodeWatchFileSystem(
            compiler.inputFileSystem
        );
        ...
    }
}

NodeWatchFileSystem 則是由 lib/node/NodeWatchFileSystem.js實現的,它的watch方法如下,

watch(files, dirs, missing, startTime, options, callback, callbackUndelayed) {
    ...
    this.watcher = new Watchpack(options);

    if (callbackUndelayed) {
        this.watcher.once("change", callbackUndelayed);
    }

    this.watcher.once("aggregated", (changes, removals) => {
        ...
        callback(
            ...
        );
    });

    this.watcher.watch(files.concat(missing), dirs.concat(missing), startTime);
    ...
}

它實例化了一個WatchPack對象,然后為watcher注冊了兩個事件監聽器,
change事件發生時,會觸發最后一個回調callbackUndelayed
aggregated事件發生時會觸發第一個回調callback

3.2 WatchPack

其中WatchPack來自一個獨立的代碼庫,它是由模塊watchpack(v1.6.0)導出的,
它可以用來監控文件和目錄的變更。

(1)watchPack.watch
我們來看一下WatchPackwatch方法,

Watchpack.prototype.watch = function watch(files, directories, startTime) {
    ...
    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);
    ...
};

它調用了_fileWatcher_dirWatcher方法,第一個參數是filedir
第二個參數是一個watcher對象,根據_fileWatcher_dirWatcher方法的形參我們可以確定這一點,

Watchpack.prototype._fileWatcher = function _fileWatcher(file, watcher) {
    watcher.on("change", function(mtime, type) {
        ...
    }.bind(this));
    watcher.on("remove", function(type) {
        ...
    }.bind(this));
    return watcher;
};

Watchpack.prototype._dirWatcher = function _dirWatcher(item, watcher) {
    watcher.on("change", function(file, mtime, type) {
        ...
    }.bind(this));
    return watcher;
};

它們只是調用了第二個參數watcher,為之注冊了changeremove事件而已。
因此,我們要重點考慮下watcher是怎么來的,
查看watch方法,我們知道,watcher是由watcherManager.watchFilewatchDirectory創建的,

watcherManager.watchFile(file, this.watcherOptions, startTime)
watcherManager.watchDirectory(dir, this.watcherOptions, startTime)

(2)watcherManager.watchDirectory
watcherManager.watchFilewatcherManager.watchDirectory
定義在watchpack/lib/watchManager.js中,

WatcherManager.prototype.watchFile = function watchFile(p, options, startTime) {
    ...
    return this.getDirectoryWatcher(directory, options).watch(p, startTime);
};

WatcherManager.prototype.watchDirectory = function watchDirectory(directory, options, startTime) {
    return this.getDirectoryWatcher(directory, options).watch(directory, startTime);
};

它們都調用了getDirectoryWatcher
getDirectoryWatcher中則創建了一個DirectoryWatcher對象執行watch操作。
位于 watchpack/lib/watchManager.js 第18行

WatcherManager.prototype.getDirectoryWatcher = function(directory, options) {
    ... 
    if(...) {
        this.directoryWatchers[key] = new DirectoryWatcher(directory, options);
        ...
    }
    ...
};

(3)DirectoryWatcher
DirectoryWatcher也是watchpack創建的對象,定義在 watchpack/lib/DirectoryWatcher.js中,

function DirectoryWatcher(directoryPath, options) {
    ...
    this.watcher = chokidar.watch(directoryPath, {
        ...
    });
    ...
}

它調用了chokidar(v2.0.4)模塊得到了一個watcher
chokidar,封裝了Node.js內置的fs.watch方法,位于chokidar/lib/nodefs-handler.js 第37行

return fs.watch(path, options, handleEvent);

fs.watch的文檔可以參考這里,Class: fs.FSWatcher
總之,watchpack調用了chokidar,chokidar調用了fs.watch完成了watch操作。


參考

webpack-cli v3.1.2 lib/cli.js
webpack v4.20.2 bin/Watching.js
watchpack v1.6.0
chokidar v2.0.4

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,310評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,951評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,566評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,055評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,303評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,799評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,683評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,899評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,135評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,528評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,844評論 2 372

推薦閱讀更多精彩內容