源碼解讀系列之 chokidar

目的

許多工具(vs code,webpack,gulp)都帶有監控文件變化而後執行自動處理的功能。有時候會想到,這些工具都是如何優雅地實現文件變化的呢?爲何個人開發環境在某些工具的 watch 模式下,cpu 會瘋狂飆高,而換一個操做系統卻又不會出現這些問題?本着好奇心,藉此瞭解 NodeJs 監控文件變化的細節以及現有的一些問題,chokidar 又是如何解決這些問題的javascript

chokidar 介紹

chokidar 是什麼?java

chokidar 是封裝 Node.js 監控文件系統文件變化功能的庫node

Node.js 原生的監控功能很差用嗎?爲何要進行這樣的封裝?webpack

Node.js 原生的監控功能還真有問題,根據 chokidar 的介紹,有以下問題:git

Node.js fs.watchgithub

  • 在 MacOS 上不報告文件名變化
  • 在 MacOS 上使用 Sublime 等編輯器時,不報告任何事件
  • 常常報告兩次事件
  • 把多數事件通知爲 rename
  • 沒有便捷的方式遞歸監控文件樹

Node.js fs.watchFileweb

  • 事件處理有大量問題
  • 不提供遞歸監控文件樹功能
  • 致使 CPU 佔用高

chokidar 解決了上面的這些問題,而且在大量開源項目,生產環境上獲得了檢驗json

版本

3.1.0gulp

項目結構

解釋數組

  • index:程序入口,包含了程序主邏輯,默認使用 node 提供的 fs.watchfs.watchFile對文件資源進行監控,若是是 OS X 系統,則會經過自定義的 fsevents-handler對文件資源進行監控
  • nodefs-handler:基於 nodejs 的 fs.watchfs.watchFile 接口擴展的文件資源監控器
  • fsevents-handler:自制的文件資源監控器,一樣使用了 fs 模塊,可是沒有使用 watch 和 watchFile 接口

關鍵流程

index:

  1. 入口邏輯:
/** * 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 實例進行監控

  1. 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 事件的能力

  1. 關鍵方法:add
/** * 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 進行文件狀態的監聽

nodedef-handler

從 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.watchFilefs.watch兩個方法生成監聽器並管理起來,實現文件以及目錄的有效監控

fsevent-handler

主要入口是 _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 模塊自帶監控的問題

總結

  1. 應該說, chokidar 的代碼仍然有很大的工程提高空間,應該能夠寫得更加簡潔,模塊耦合度更低 以及 擁有更好的方法、變量命名等;
  2. 經過本次分析,可以瞭解 chokidar 模塊的大體結構,知道了不一樣環境下,監控事件的來源;尚有許多細節:事件過濾、事件合併、監聽器的變化,相關內容會繼續更新;
相關文章
相關標籤/搜索