由fs.watchFile造成的性能问题说起

目前采用node作为前后端分离的中间件越来越多,而本地开发的时候经常涉及到hot-replace,也就是热替换,以达到不重启服务就可以改变效果的目的。一般就思路就是想办法找到修改的文件,然后清除require缓存。如何找到修改的文件呢,一般采用的是fs.watchFile。而fs.watchFile是有性能瓶颈的,接下来就详细说下这个问题。

fs.watchFile用来监听文件的变化,文件一旦发生变化就触发callback。简单的用法就是

    fs.watchFile('message.text', (curr, prev) => {
      console.log(`the current mtime is: ${curr.mtime}`);
      console.log(`the previous mtime was: ${prev.mtime}`);
    });

ok,api其实很简单,我们接下来看看源码,来分析下他是如何实现的。

##一、node源码

我们找到了lib/fs.js文件,然后找到了fs.watchFile方法。看看他的核心实现:


      if (stat === undefined) {
        stat = new StatWatcher();
        stat.start(filename, options.persistent, options.interval);
        statWatchers.set(filename, stat);
      }

      stat.addListener('change', listener);

这里是创造了一个StatWatcher对象,调用了它的start方法。而StatWatcher类其实是一个事件类,这时候我们应该猜到,是有一个change事件的处理,我们看来看看他是如何处理的

    function StatWatcher() {
      EventEmitter.call(this);

      var self = this;
      this._handle = new binding.StatWatcher();
      this._handle.onchange = function(current, previous, newStatus) {
        ...
        self.emit('change', current, previous);
      };
    }
    util.inherits(StatWatcher, EventEmitter);

ok,这下有点眉目了,StatWatcher其实是从binding这个对象里的,binding我们知道,是v8与jsapi的一个桥梁,这时候,我们去看看c++的源码。我们找到了src/node_stat_watcher.cc

    void StatWatcher::Start(const FunctionCallbackInfo<Value>& args) &#123;
      ...
      uv_fs_poll_start(wrap->watcher_, Callback, *path, interval);
      ...
    &#125;

最核心的就是这个uv_fs_poll_start函数,接下来,我们又找到了deps/uv/src/fs-poll.c

    int uv_fs_poll_start(uv_fs_poll_t* handle,
                     uv_fs_poll_cb cb,
                     const char* path,
                     unsigned int interval) &#123;
        ...
        err = uv_fs_stat(loop, &ctx->fs_req, ctx->path, poll_cb);
        ...
    &#125;

重点关注下poll_cb,我们再看看它是在哪儿定义的

    static void poll_cb(uv_fs_t* req) &#123;
        ...
        if (uv_timer_start(&ctx->timer_handle, timer_cb, interval, 0))
            abort();
    &#125;

再看看timer_cb

    static void timer_cb(uv_timer_t* timer) &#123;
        ...
          if (uv_fs_stat(ctx->loop, &ctx->fs_req, ctx->path, poll_cb))
            abort();
    &#125;

我们发现,它再一次调用了uv_fs_stat这个函数。

##二、watchFile的结论

  • watchFile需要用poll的方式去拿文件的stat,然后比对前后的stat是否一致。

因此当watchFile的文件比较多的时候,磁盘的io、内存的占用必定会上涨。而我们正常项目里面,文件只可能会越来越多,这时候我们可能需要做些简单的处理了。

##三、解决watch文件太多的问题

第一种思路很明显,就是尽量减少watch的文件。比如有些node_modules、.git等等根本用不到,我们直接在项目init的时候把他们t出去。这时候,我们可能要用到minimatch来判断文件的路径,比如我们在项目里用到配置就是这样:

"ext": [
    "**/order/**/*.+(js|html|json)",
    "!**/node_modules/**/*",
    "!**/.svn/**/*",
    "!**/.idea/**/*"
]

真正清除cache的方法其实很简单

delete require.cache[file];

注意:这里的file是绝对路径。

###四、用setInterval去清除cache

还有一种思路就是用定时器去清除cache。这种方法比较暴力,但是性能比较好,不用一直去检查文件的状态。
比如:

setInterval(()=>&#123;
    files.forEach((file)=>&#123;
        delete require.cache[file]
    &#125;);
&#125;,interval);