[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阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容