esm模块的加载顺序

207 阅读2分钟

1.

有以下js代码:

// index.js
console.log(1)
import { sin } from './sin.js'
// sin.js
console.log(2)
export const sin = (x) => {
    return Math.sin(x)
}

执行 index.js 时,输出顺序是:

// 2
// 1

这是因为如果使用的esm规范,在执行 index.js 时,会先执行其导入的模块文件,然后再执行当前文件的代码,所以先输出 2,然后输出 1

2.

所以如果在index.js中通过globalThis在全局挂载了一个变量_B,在sin.js的全局中读取这个变量,就会报错_B is not defined。代码如下:

// index.js
globalThis._B = 1
import { sin } from './sin.js'
// sin.js
console.log(_B)
export const sin = (x) => {
    return Math.sin(x)
}

3.

要解决这个问题,一个解决方案是将对_B的读取放在运行时,而不是在模块加载执行的时候读取,以下代码:

// index.js
globalThis._B = 1
import { sin } from './sin.js'
sin(1)
// sin.js
export const sin = (x) => {
    console.log(_B)
    return Math.sin(x)
}

这样在index.js中执行sin的时候就可以打印出_B的值,而不会报错。

4.

另一个解决方案是使用import进行动态加载,如下

// index.js
globalThis._B = 1
const sin = await import('./sin.js')
// sin.js
console.log(_B)
export const sin = (x) => {
    return Math.sin(x)
}

这时只需要确保 globalThis._B = 1await import('./sin.js') 之前执行就可以。

5.

在esm中,模块是单例的,首次导入后,模块所有的导出都被缓存,且只会执行一次。如下代码:

// index.js
globalThis._B = true
const sinMod = await import('./sin.js')

sinMod.print() // 1

globalThis._B = false
const sinMod2 = await import('./sin.js')
sinMod2.print() // 1
// sin.js
console.log(22)
function print1() {
  console.log('1')
}
function print2() {
  console.log('2')
}

export const print = _B ? print1 : print2

上述代码的输出是:

// 22
// 1
// 1

即使执行了 globalThis._B = false,然后重新加载,加载的sinMod2也会只是用第一次加载的缓存,此时print函数还是print1,而不是print2,所以打印了两个1。

要使import()的模块不相同,可以在路径后面增加一个查询参数,如下:

// index.js
globalThis._B = true
const sinMod = await import('./sin.js')

sinMod.print() // 1

globalThis._B = false
const sinMod2 = await import('./sin.js?cacheBust=1')
sinMod2.print() // 1

这样sinMod和sinMod2在内存中就是两个不同的模块。代码的输出如下,模块会执行两次。

// 22
// 1
// 22
// 2

6.

如果使用的是commomjs规范,就不会遇到最前面说的问题,因为commonjs模块是同步加载的。下面的代码不会报错,会打印 1

// index.js
globalThis._B = 1
const sinMod = require('./sin.js')
// sin.js
console.log(_B)
module.exports.sin = (x) => {
    return Math.sin(x)
}