很多时候,特别是在开发过程中,我们希望能够不需要频繁重启服务就立即应用改动的代码。但在常驻内存的服务器应用中,要实现这种需求并不简单。
强引用和弱引用
因为常驻内存的自然特性,变量存在强引用的问题,特别是在像 CommonJS 这样的模块加载方案中,一旦 `require/import` 的模块被赋值到了新的变量中,它就自动形成了对模块的“拷贝”,即原模块发生改变之后,在导入的地方,这个改变将毫无作用。例如下面的例子:
// app.ts
const app = {
name: "app" /* ... */ };
export default app;
// index.ts
import app from "./app";
console.log(app.name); // => app
现在,修改 app.ts 中的代码为如下这样子:
const app = {
name: "my-app" /* ... */ };
export default app;
这并没有任何作用,因为如果不重启进程,新代码将不会被运行。但 NodeJS 提供了重载模块的方案。默认地,NodeJS 将模块缓存到 `require.cache` 中,因此,理论上,只要删除缓存,并重新导入即可。因此,开发者可以实现下面这样的逻辑,用来监听文件改变,并自动重载模块。
import {
watch } from "chokidar"; // chokidar 是一个兼容更多平台的文件监听模块
watch(__dirname).on("change", (filename) => {
delete require.cache[filename];
require(filename);
});
但实际上这毫无作用,因为在 index.ts 中,`import app from "./app";` 创建了一个对模块的
强引用(不完整的拷贝),当模块本身发生了改变并重载(重新赋值到缓存中),强引用就会立即变成完整的拷贝,就像下面这样:
var cache = {
name: "app" };
var copy = cache;
// 如果 cahce 被重载
cache = {
name: "my-app" };
// copy 将不会有任何改变
console.log(capy.name) // => app
要实现缓存重载后原来的引用也能够发生改变,则需要通过"弱引用"导入。即每一处使用导入对象的地方,都重新检查缓存,例如重新调用 `require`,类似下面这样:
console.log(require("./app").default.name);
如此一来,当缓存被重载,`require` 就会返回新的对象。