先记住这个答案
一个模块被 import 时要经过几个阶段:解析地址、加载并编译、链接导入导出、执行顶层代码。普通 import 会一口气走完,给你的是执行后的导出。source phase import 写成 import source mod from "./a.wasm",只走到加载编译完成就停,给你的是这个模块的「源码对象」,还没有链接和执行。目前它主要用于 WebAssembly,拿到的是一个未实例化的 WebAssembly.Module,由你决定用什么导入对象去实例化、实例化几次。
import source获取源码对象- 目前的主要用途是导入 WebAssembly 模块
import.source()异步取得源码对象import defer延迟模块求值
导入一个 WASM 模块
以前要用 WASM,得先 fetch 文件,再调 WebAssembly.compileStreaming 编译,地址还要自己拼。有了 import source,这些交给模块系统。
// 静态写法:拿到的是 WebAssembly.Module,不是实例
import source calcModule from './calc.wasm'
// 可以用不同的导入对象实例化多次
const a = await WebAssembly.instantiate(calcModule, { env: { log: console.log } })
const b = await WebAssembly.instantiate(calcModule, { env: { log: () => {} } })
// 动态写法
const mod = await import.source('./calc.wasm')
console.log(mod instanceof WebAssembly.Module) // true
// 也可以把 Module 发给 Worker,在那边再实例化
worker.postMessage(mod)Node.js 官方文档把 WASM 的 import source 列在 source phase imports 下,并展示了用同一个 WebAssembly.Module 分别实例化的写法。MDN 仍将这项语法标为实验性,浏览器和运行时支持情况需要按项目目标环境核对;不要把它当成所有环境都能直接运行的写法。
这里的关键区别在于:如果直接 import 一个 WASM 模块(WebAssembly 的 ESM 集成),宿主会替你实例化好,你拿到的是它的导出函数,导入对象也由宿主按模块依赖去找,你插不上手。import source 把实例化的控制权还给了你。
另一个调整求值时机的语法:import defer
import source 是「提前停下」,import defer 是「晚点执行」,两者经常被放在一起讨论。
- 加载照常,执行推迟到第一次使用
- 写成
import defer * as heavy from "./heavy.js"时,heavy.js和它的依赖照常加载、链接,但顶层代码先不执行,等你第一次读取heavy上的某个属性时才同步执行。它只能用命名空间形式,因为要靠「访问属性」来触发执行。 它想解决的是启动性能:有些模块很大,但只在少数分支里用到。动态import()也能推迟,但会把调用链全部变成异步;import defer保持同步写法,只推迟求值成本。这项语法仍属实验性能力,并非所有主流浏览器都支持。实际采用前应核对目标运行环境,不能把它当作已经普遍可用的import写法。
容易答错的地方
- 以为
import source能用在任何模块上 - 它要求宿主为这种模块定义了源码对象。WASM 定义了,就是
WebAssembly.Module;普通 JS 模块目前没有,对.js文件写import source,Node.js 会报「这个模块没有定义源码阶段对象」的错误。 - 把
import defer当成懒加载 import defer不推迟下载,文件在页面启动时照样被请求和解析,只是不执行。想连下载一起推迟,还是得用动态import()。另外如果被推迟的模块里有顶层await,这部分异步工作没法等到访问时同步完成,会被提前执行。
面试官还会怎么问?
为什么要把 WASM 的实例化交给开发者?
WASM 模块实例化时需要一个导入对象,里面是它要调用的 JS 函数和内存。同一个模块可能要配不同的导入对象,比如一个实例用于主线程、一个用于测试替身;也可能要把编译好的模块发给多个 Worker 各自实例化,省去重复编译。这些场景都需要拿到「编译好但没实例化」的那个对象。
现在项目里能用这些语法吗?
生产项目里直接写还太早,支持情况在各运行时之间差异很大。Vite 这类工具已经有加载 WASM 的插件和 ?init 这类约定,功能接近。了解这两个提案的价值,主要在于理解模块导入其实分好几个阶段,以及为什么语言要开放这些阶段。
import() 和 import.source() 返回的有什么区别?
import() 返回的 Promise 解决为模块命名空间对象,里面是执行后的导出。import.source() 返回的 Promise 解决为源码对象,没有导出,也没有执行过任何代码。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。