JavaScript 模块导入缓存(CommonJS & ESM)

1,314 阅读2分钟

JavaScript 模块一旦被导入到运行环境中,就会被缓存,对于 Node.js 运行环境和浏览器页面均如此。

当再次尝试导入这个模块时,就会读取缓存中的内容,而不会重新加载一遍这个模块的代码。

首先看看在 Node.js 运行环境的表现:

  • CommonJS
// common.js

let count = 0

console.log('commonjs')

module.exports = {
  count
}

// test.js

const obj1 = require('./common.js')

console.log(obj1.count)
obj1.count++
console.log(obj1.count)

const obj2 = require('./common.js')

console.log(obj1 === obj2)
console.log(obj2.count)
obj2.count++
console.log(obj2.count)
$ node test.js
commonjs
0
1
true
1
2

可以看出 require('./common.js') 多次导入的都是同一个对象

  • ESM
// esm.mjs

let count = 0

console.log('esm')

export default {
  count
}

// test.mjs

import obj1 from './esm.mjs'

console.log(obj1.count)
obj1.count++
console.log(obj1.count)

import obj2 from './esm.mjs'

console.log(obj1 === obj2)
console.log(obj2.count)
obj2.count++
console.log(obj2.count)
$ node test.mjs
esm
0
1
true
1
2

和 CommonJS 一样,import xxx from './esm.mjs' 多次导入的都是同一个对象

再看看浏览器原生加载 ES6 模块的表现:

// esm.js

let count = 0

console.log('esm')

export default {
  count
}
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script type="module">
    import obj1 from './esm.js'

    console.log(obj1.count)
    obj1.count++
    console.log(obj1.count)

    import obj2 from './esm.js'

    console.log(obj1 === obj2)
    console.log(obj2.count)
    obj2.count++
    console.log(obj2.count)
  </script>
</body>
</html>
esm
0
1
true
1
2

也是一样的表现。

总结:

模块在第一次加载后被缓存,每次调用 require('foo')(或 import xxx from 'foo') 都会返回完全相同的对象(如果解析为相同的文件)。

在 Node.js 运行环境中,如果 require.cache 没有被修改,则多次调用 require('foo') 不会导致模块代码被多次执行。

需要注意的是,require.cache 没有被 import 使用,因为 ES 模块加载器有自己独立的缓存。

可以在第一次引入文件以后,使用 require.cache 来看一下都缓存了什么。缓存中实际上是一个对象,这个对象中包含了引入模块的属性。我们可以从 require.cache 中把相应的属性删掉,以使缓存失效,这样 Node 就会重新加载模块并且将其重新缓存起来。

// test.js
const path = require('node:path')
const obj1 = require('./common.js')

console.log(obj1.count)
obj1.count++
console.log(obj1.count)

console.log(require.cache)
delete require.cache[path.join(__dirname, './common.js')]

const obj2 = require('./common.js')

console.log(obj1 === obj2)
console.log(obj2.count)
obj2.count++
console.log(obj2.count)
$ node test.js
commonjs
0
1
[Object: null prototype] {
  'xxx/test.js': {
    id: '.',
    path: 'xxx',
    exports: {},
    filename: 'xxx/test.js',
    loaded: false,
    children: [ [Object] ],
    paths: [...]
  },
  'xxx/common.js': {
    id: 'xxx/common.js',
    path: 'xxx',
    exports: { count: 1 },
    filename: 'xxx/common.js',
    loaded: true,
    children: [],
    paths: [...]
  }
}
commonjs
false
0
1

要让模块多次执行代码,则导出函数,然后调用该函数。

参考:

nodejs.cn/api/modules…

nodejs.cn/api/esm.htm…

segmentfault.com/a/119000000…