目前采用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) {
...
uv_fs_poll_start(wrap->watcher_, Callback, *path, interval);
...
}
最核心的就是这个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) {
...
err = uv_fs_stat(loop, &ctx->fs_req, ctx->path, poll_cb);
...
}
重点关注下poll_cb,我们再看看它是在哪儿定义的
static void poll_cb(uv_fs_t* req) {
...
if (uv_timer_start(&ctx->timer_handle, timer_cb, interval, 0))
abort();
}
再看看timer_cb
static void timer_cb(uv_timer_t* timer) {
...
if (uv_fs_stat(ctx->loop, &ctx->fs_req, ctx->path, poll_cb))
abort();
}
我们发现,它再一次调用了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(()=>{
files.forEach((file)=>{
delete require.cache[file]
});
},interval);