Node

node-watch

node-watch 是在 fs.watch 的基础上封装而来的库,能监听文件变化

实现

从提交历史来看,最初的提交的代码非常简单,几乎是只多了一个递归监听功能。在 normalizeCall 函数中使用计时函数,把回调往后延迟了 100ms,防止短暂时间内同一个文件多次修改(比方说某些编辑器保存文件时会先生成一个临时文件)。

//  https://github.com/yuanchuan/node-watch/commit/d2d8e2db4b771f918d67d261708752d8eb5c17b3
function watch(dir, cb) {
  if (is.File(dir)) {
    fs.watchFile(dir, function(err) {
      normalizeCall(cb, dir)
    })
    return
  }
  if (is.Directory(dir)) {
    fs.watch(dir, function(err, fname) {
      normalizeCall(cb,  
        path.join(dir, fname)
      )
    })
    fs.readdir(dir, function(err, files) {
      if (err) throw err
      files.forEach(function(n) { 
        var file = path.join(dir, n)
        if ( is.Directory(file) ) {
          watch(file, cb)
        }
      })
    })     
  }
}

由于 fs.watchFile 可能导致高 CPU 负载等问题,watch 文件时使用 watch 其父目录作替代,并通过匹配事件参数是否和此文件同名来判断是否要触发回调函数。

if (is.file(fpath)) {
  var parent = path.resolve(fpath, '..')
  fs.watch(parent, function(evt, fname) {
    if (path.basename(fpath) === fname) {
      normalizeCall(fpath, options, cb)
    }
  })
}

如果目录中带有符号链接,node-watch 会默认跟踪下去,同时使用一个默认的 maxSymbolLevel 数字配置项,设置最大跟踪层数,防止死循环或性能问题。

function watch(fpath, options, cb) {
  if (is.symbolic(fpath) 
    && !(options.followSymLinks 
      && options.maxSymLevel--)) {
    return
  }
}

NodeJS 文档提到,fs.watch 的 recursive 选项在 Linux 中是无效的,因为 fs.watch 的实现依赖具体操作系统底层 API。所以到底要不要用原生的 recursive 选项需要提前判断一下,node-watch 的实现挺粗暴,直接使用 IO 操作去检测:先到系统临时文件目录创建一些文件夹,然后监听它们并删除里面的内容,如果超时了还没有触发 watcher 的 change 事件,那就说明不是原生支持 recursive 选项。

var IS_SUPPORT

try {
  watcher = fs.watch(parent, options)
} catch (e) {
  if (e.code == 'ERR_FEATURE_UNAVAILABLE_ON_PLATFORM') {
    return fn(IS_SUPPORT = false)
  } else {
    throw e
  }
}

var timer = setTimeout(function() {
  watcher.close()
  stack.cleanup(function() {
    fn(IS_SUPPORT = false)
  })
}, 200)

watcher.on('change', function(evt, name) {
  if (path.basename(file) === path.basename(name)) {
    watcher.close()
    clearTimeout(timer)
    stack.cleanup(function() {
      fn(IS_SUPPORT = true)
    })
  }
})

Watcher 仅向外暴露指定方法。

Watcher.prototype.expose = function() {
  var expose = {}
  var self = this
  var methods = [
    'on', 'emit', 'once',
    'close', 'isClosed',
    'listeners', 'setMaxListeners', 'getMaxListeners',
    'getWatchedPaths'
  ]
  methods.forEach(function(name) {
    expose[name] = function() {
      return self[name].apply(self, arguments)
    }
  })
  return expose
}

监听路径支持传数组,我们当然希望就算是数组,也是由同一个 EventEmitter 来管理事件的,传入的是既然是数组,那么自然会生成多个 watcher 实例,只不过代码中使用了 composeWatcher 函数,把这些事件整合并向外发送。

function composeWatcher(watchers) {
  // 使用一个新的 watcher 整合事件列表
  var watcher = new Watcher()
  var filterDups = createDupsFilter()
  var counter = watchers.length

  watchers.forEach(function(w) {
    w.on('change', filterDups(function(evt, name) {
      watcher.emit('change', evt, name)
    }))
    w.on('error', function(err) {
      watcher.emit('error', err)
    })
    w.on('ready', function() {
      if (!(--counter)) {
        emitReady(watcher)
      }
    })
  })

  watcher.close = function() {
    watchers.forEach(function(w) {
      w.close()
    })
    process.nextTick(emitClose, watcher)
  }

  watcher.getWatchedPaths = function(fn) {
    if (is.func(fn)) {
      var promises = watchers.map(function(w) {
        return new Promise(function(resolve) {
          w.getWatchedPaths(resolve)
        })
      })
      Promise.all(promises).then(function(result) {
        var ret = unique(flat1(result))
        fn(ret)
      })
    }
  }

  return watcher.expose()
}

工具函数

通过索引快速去重。第一次见到有人使用 filter 方法的第三个参数。

function unique(arr) {
  return arr.filter(function(v, i, self) {
    return self.indexOf(v) === i
  })
}

获取 UUID,这个有意思,但不知道效率怎么样。

const getUUID = () => Math.random().toString(16).substr(2)

考虑的点

最后总结一下一些框架设计相关的小细节,如果需要自己设计一个 fs.watch,可能要做以下考虑:

  • 使用原生依赖吗?使用原生依赖会带来更高的性能,但是也会增加代码难度以及测试的复杂度
  • 支持本地符号路径吗?支持网络路径吗?
  • 如何控制包的大小?
  • 由于不同系统的底层依赖不同,那么 API 要怎么设计,屏蔽底层,才能便于使用?

也注意到一些有意思的问题:

题外话

《Recursive Node.js fs.watch on Linux》

这个回答中的 node-watch 为啥会被踩,我没看很懂。

其它资料

相关类库:

推荐阅读:


Copyright © 2024 Lionad - CC-BY-NC-CD-4.0