许多工具(vs code,webpack,gulp)都带有监控文件变化而后执行自动处理的功能。有时候会想到,这些工具都是如何优雅地实现文件变化的呢?为何个人开发环境在某些工具的 watch 模式下,cpu 会疯狂飙高,而换一个操做系统却又不会出现这些问题?本着好奇心,借此了解 NodeJs 监控文件变化的细节以及现有的一些问题,chokidar 又是如何解决这些问题的javascript
chokidar 是什么?java
chokidar 是封装 Node.js 监控文件系统文件变化功能的库node
Node.js 原生的监控功能很差用吗?为何要进行这样的封装?webpack
Node.js 原生的监控功能还真有问题,根据 chokidar 的介绍,有以下问题:git
Node.js fs.watch
:github
rename
Node.js fs.watchFile
:web
chokidar 解决了上面的这些问题,而且在大量开源项目,生产环境上获得了检验json
3.1.0gulp
解释:数组
fs.watch
和fs.watchFile
对文件资源进行监控,若是是 OS X 系统,则会经过自定义的 fsevents-handler
对文件资源进行监控fs.watch
和 fs.watchFile
接口扩展的文件资源监控器/** * Instantiates watcher with paths to be tracked. * @param {String|Array<String>} paths file/directory paths and/or globs * @param {Object=} options chokidar opts * @returns an instance of FSWatcher for chaining. */
const watch = (paths, options) => {
const watcher = new FSWatcher(options);
watcher.add(watcher._normalizePaths(paths));
return watcher;
};
exports.watch = watch;
复制代码
const chokidar = require('chokidar');
// One-liner for current directory
chokidar.watch('.').on('all', (event, path) => {
console.log(event, path);
});
复制代码
向外暴露 watch 方法,watch 方法会建立一个 FSWatcher 实例,将输入的监控路径 paths 进行格式化(转换成数组)后,传入给 FSWatcher 实例进行监控
/** * Watches files & directories for changes. Emitted events: * `add`, `addDir`, `change`, `unlink`, `unlinkDir`, `all`, `error` * * new FSWatcher() * .add(directories) * .on('add', path => log('File', path, 'was added')) */
class FSWatcher extends EventEmitter 复制代码
this._emitRaw = (...args) => this.emit('raw', ...args);
this._readyEmitted = false;
this.options = opts;
// Initialize with proper watcher.
if (opts.useFsEvents) {
this._fsEventsHandler = new FsEventsHandler(this);
} else {
this._nodeFsHandler = new NodeFsHandler(this);
}
复制代码
在处理完配置参数后,关键点在于根据最终状况决定使用 FsEventsHandler 仍是 NodeFsHandler
因为 FSWatcher 扩展自 EventEmitter,因此 FSWatcher 的实例有 on 和 emit 方法实现事件发射与监听,同时将 _emitRaw 方法传入到两个 handler 的实例中,使得 handler 得到向外 emit 事件的能力
/** * Adds paths to be watched on an existing FSWatcher instance * @param {Path|Array<Path>} paths_ * @param {String=} _origAdd private; for handling non-existent paths to be watched * @param {Boolean=} _internal private; indicates a non-user add * @returns {FSWatcher} for chaining */
add(paths_, _origAdd, _internal) {
const {cwd, disableGlobbing} = this.options;
this.closed = false;
复制代码
if (this.options.useFsEvents && this._fsEventsHandler) {
if (!this._readyCount) this._readyCount = paths.length;
if (this.options.persistent) this._readyCount *= 2;
paths.forEach((path) => this._fsEventsHandler._addToFsEvents(path));
} else {
if (!this._readyCount) this._readyCount = 0;
this._readyCount += paths.length;
Promise.all(
paths.map(async path => {
const res = await this._nodeFsHandler._addToNodeFs(path, !_internal, 0, 0, _origAdd);
if (res) this._emitReady();
return res;
})
).then(results => {
if (this.closed) return;
results.filter(item => item).forEach(item => {
this.add(sysPath.dirname(item), sysPath.basename(_origAdd || item));
});
});
}
复制代码
将 paths 进行遍历,根据条件分别经过 fsEventsHandler 或者 nodeFsHandler 进行文件状态的监听
从 index 的逻辑能够知道,该模块的关键入口方法为 _addToNodeFs
/** * Handle added file, directory, or glob pattern. * Delegates call to _handleFile / _handleDir after checks. * @param {String} path to file or ir * @param {Boolean} initialAdd was the file added at watch instantiation? * @param {Object} priorWh depth relative to user-supplied path * @param {Number} depth Child path actually targetted for watch * @param {String=} target Child path actually targeted for watch * @returns {Promise} */
async _addToNodeFs(path, initialAdd, priorWh, depth, target) {
const ready = this.fsw._emitReady;
if (this.fsw._isIgnored(path) || this.fsw.closed) {
ready();
return false;
}
let wh = this.fsw._getWatchHelpers(path, depth);
if (!wh.hasGlob && priorWh) {
wh.hasGlob = priorWh.hasGlob;
wh.globFilter = priorWh.globFilter;
wh.filterPath = entry => priorWh.filterPath(entry);
wh.filterDir = entry => priorWh.filterDir(entry);
}
复制代码
该方法的关键逻辑以下:
if (stats.isDirectory()) {
const targetPath = follow ? await fsrealpath(path) : path;
closer = await this._handleDir(wh.watchPath, stats, initialAdd, depth, target, wh, targetPath);
// preserve this symlink's target path
if (path !== targetPath && targetPath !== undefined) {
this.fsw._symlinkPaths.set(targetPath, true);
}
} else if (stats.isSymbolicLink()) {
const targetPath = follow ? await fsrealpath(path) : path;
const parent = sysPath.dirname(wh.watchPath);
this.fsw._getWatchedDir(parent).add(wh.watchPath);
this.fsw._emit('add', wh.watchPath, stats);
closer = await this._handleDir(parent, stats, initialAdd, depth, path, wh, targetPath);
// preserve this symlink's target path
if (targetPath !== undefined) {
this.fsw._symlinkPaths.set(sysPath.resolve(path), targetPath);
}
} else {
closer = this._handleFile(wh.watchPath, stats, initialAdd);
}
复制代码
能够看出,这里涉及两个重要方法:_handleDir 和 _handleFile
_handleFile 处理具体文件路径
_handleDir 处理文件夹路径
经过阅读它们的源码,最终都会导向一个方法:_watchWithNodeFs
/** * Watch file for changes with fs_watchFile or fs_watch. * @param {String} path to file or dir * @param {Function} listener on fs change * @returns {Function} closer for the watcher instance */
_watchWithNodeFs(path, listener) {
// createFsWatchInstance
// setFsWatchFileListener
复制代码
抽象流程以下:
经过递归遍历目录,调用fs.watchFile
和 fs.watch
两个方法生成监听器并管理起来,实现文件以及目录的有效监控
主要入口是 _addToFsEvents
抽象结构以下:
能够看见,关键点在于 'fsevents.watch' 的调用
fsevents 模块来源于第三方依赖:
"engines": {
"node": ">= 8"
},
"dependencies": {
"anymatch": "^3.1.0",
"braces": "^3.0.2",
"glob-parent": "^5.0.0",
"is-binary-path": "^2.1.0",
"is-glob": "^4.0.1",
"normalize-path": "^3.0.0",
"readdirp": "^3.1.1"
},
"optionalDependencies": {
"fsevents": "^2.0.6"
},
复制代码
fsevents 在 github 上的 readme 介绍为:
可知,fs-events 模块是 nodejs 的扩展模块,调用了 MacOS 的底层 API 以及相关文件监控事件,从而避免 nodejs fs 模块自带监控的问题