import defer
有限可用
import defer 声明的行为与常规 import 声明类似,但它会生成一个延迟模块命名空间对象。模块及其依赖会预先获取并链接,但只有在访问命名空间的属性时才会同步执行。使用顶层 await 的模块会立即执行。
语法
import defer * as name from "module-name";
name-
用于引用延迟模块命名空间对象的名称。必须是有效的 JavaScript 标识符。
module-name-
要导入的模块。其处理方式与常规
import声明中的module-name相同。
导入属性同样受支持,可以在模块说明符后使用 with 子句指定。
defer 不是保留字。例如,import defer from "./module.js" 仍然是常规默认导入,其本地绑定名称为 defer。
描述
默认情况下,import 声明会同时执行多项任务:解析模块说明符、获取模块源代码、解析(期间可能会发现传递依赖)、链接并执行。这样的提前执行并非总是理想的:它可能会拖慢启动速度,执行所需的环境可能尚未准备就绪,或者该模块根本不需要执行。
导入阶段修饰符允许模块导入过程停在特定阶段。在 import 后添加 defer,会链接源代码但暂不执行,前提是该模块可以同步执行(即不使用顶层 await)。通过延迟命名空间访问某个导出时,会同步执行该模块以及在它之前必须执行的依赖。执行完成后,此次访问会返回该导出的值。此操作会执行模块的顶层代码,而不只是初始化所请求导出所需的代码。使用各自的 import defer 声明导入的传递依赖可以继续保持延迟状态。
只要确保暂停的子图可以同步执行,就几乎无需修改使用该模块的代码,即可直接将 import defer 用作替代:
// 之前:
import * as ts from "typescript";
// 此处会执行整个 TypeScript 模块图
function compileFile(path) {
const program = ts.createProgram([path], {});
}
// 之后:
import defer * as ts from "typescript";
// 不会执行任何代码;启动速度可能更快
function compileFile(path) {
// 访问 `ts.createProgram` 时(也就是调用 `compileFile` 时),
// 才会执行 TypeScript 模块图。
// 如果从未调用 `compileFile`,就永远不会执行模块图。
const program = ts.createProgram([path], {});
}
警告:延迟导入会改变副作用发生的时机。不要延迟那些副作用必须在其余代码运行前发生的模块,例如安装 polyfill 的模块。
与 import source 不同,延迟模块仍会预先链接。预先链接可以让模块加载器解析依赖,从而在模块使用之前发现缺失的依赖或无效的导入。保持模块未链接则可以避免加载可能用不到的依赖,并允许你控制模块的实例化方式。
与 import() 不同,延迟模块仍会预先获取、解析和链接,从而避免不必要的异步传染性(整条函数调用链都被迫变为异步)。import defer 还具备静态声明的大部分优点,例如更好的静态分析能力。
请注意,它仅支持“命名空间导入”语法。你不能使用 import defer { property } from "./my-module.js" 等写法,因为模块的执行是由对命名空间对象的属性访问触发的。
缓存语义
该修饰符作用于导入,而不是模块本身。如果应用的其他部分在导入同一模块时没有使用 defer,该模块仍会照常执行。两种导入形式共享同一模块状态,模块代码最多执行一次。更改导入阶段不会在缓存中创建一个独立模块:
import defer * as ts from "typescript";
// 不会执行任何代码
import * as ts2 from "typescript";
// 此处会执行整个 TypeScript 模块图
function compileFile(path) {
// 由于模块图已经执行,访问 `ts.createProgram` 不会再次触发执行。
const program = ts.createProgram([path], {});
}
相比之下,导入属性可能会影响模块标识。例如,在支持文本模块的宿主环境中,以下两个声明请求的是不同的模块类型:
import * as mod from "./module.js";
import text from "./module.js" with { type: "text" };
这两个导入会被视为来自不同的模块,只是它们恰好使用相同的字符串说明符(在 Web 上,它们会使用不同的 HTTP 请求头发起请求)。支持哪些属性,以及它们对加载和模块标识的影响,均由宿主环境定义。
延迟模块命名空间对象
延迟模块命名空间对象的行为与常规模块命名空间对象非常相似:它的原型为 null,不可扩展且已密封,并以只读实时绑定的形式公开模块的导出。它的字符串键可枚举,并按字典序排序。默认导出可通过名为 default 的属性访问。
它与常规命名空间有以下三点不同:
- 检查导出的操作可能会触发执行并抛出执行错误,详见下文。
- 它的
[Symbol.toStringTag]属性值为"Deferred Module",而不是"Module"。即使执行完成后也是如此。 - 即使执行完成后,它也不会公开名为
then的导出。读取namespace.then总是返回undefined。这样可以避免 Promise 解析过程将该命名空间视为 thenable 并触发执行。若要访问这样的导出,请使用常规导入,或者引入一个中间模块,以其他名称重新导出then。
同一模块的延迟命名空间与常规命名空间是不同的对象,即使执行完成后也是如此。无论是静态导入还是动态导入,对同一模块进行多次延迟导入都会共享同一个延迟命名空间对象。
为了实现“访问键时触发模块执行”的行为,延迟模块命名空间本质上是一个 Proxy,它会拦截以下操作并触发模块执行:
-
对除
then以外的任意字符串键调用defineProperty():例如Object.defineProperty(namespace, "value", {})。备注:由于模块命名空间对象不可扩展且已密封,因此无法有效地添加或更改任何属性描述符,包括其值。不过,即使该操作失败,仍会触发执行。
代理不会拦截
set()。像namespace.value = 1;这样的属性赋值总是会失败。 -
对除
then以外的任意字符串键调用deleteProperty():例如delete namespace.value。备注:实际上无法删除命名空间已有的任何属性。不过,即使该操作失败,仍会触发执行。
-
对除
then以外的任意字符串键调用get()和getOwnPropertyDescriptor():例如namespace.value、namespace["missing"]、const { default: value } = namespace、Object.getOwnPropertyDescriptor(namespace, "value")。备注:因此,在顶层解构导出会使该模块无法保持延迟状态。
-
对除
then以外的任意字符串键调用has():例如"value" in namespace、Object.hasOwn(namespace, "missing")。 -
调用
ownKeys():例如Object.keys(namespace)、Object.getOwnPropertySymbols(namespace)、for (const key in namespace) {}。即使只枚举符号键,也会触发执行。
仅仅引用命名空间、将其赋值给另一个变量、比较其标识或将其传递给函数,都不会触发执行。读取 then 或符号键属性(例如 namespace[Symbol.toStringTag])也不会触发执行。调用 Object.getPrototypeOf() 或 Object.isExtensible() 同样不会触发执行。不过,Object.isSealed() 和 Object.isFrozen() 会枚举键,因此会触发执行。
顶层 await
读取命名空间属性是同步操作,因此无法等待异步模块执行。包含顶层 await 的模块会立即执行,同时也会执行它所依赖的模块。这也包括通过后续的延迟导入到达的模块。导入模块会等待此异步执行完成后,才运行自身的模块体。
如果直接导入的模块包含顶层 await,则不会延迟该模块的执行。如果只有部分依赖包含顶层 await,这些依赖会立即执行,但模块图中不必为执行这些依赖而执行的同步模块仍可保持延迟。参见延迟具有异步依赖的模块。
错误
加载、解析和链接错误不会延迟。例如,即使从未访问延迟命名空间,缺失的模块、依赖中的语法错误或无法解析的命名导入也会阻止导入模块运行。立即执行的异步依赖所产生的错误也会阻止导入模块运行。
延迟执行期间抛出的错误,会由触发执行的操作同步抛出。你可以在该操作外使用 try...catch 捕获错误。错误会被缓存:后续触发执行的操作会再次抛出同一错误,而不会重试模块代码。如果之前的另一次导入已经导致模块执行失败,也同样如此。
如果模块或其依赖尚未准备好进行同步执行,触发执行的操作会抛出 TypeError。这可能发生在循环导入中,即访问操作需要依赖一个仍在执行的模块时。import defer 声明并不会让所有循环依赖都能在初始化期间安全访问。就绪状态错误本身不会将请求的模块标记为执行失败:依赖准备就绪后,后续访问仍可能成功。
示例
>首次使用时执行模块
以下模块会在运行顶层代码时初始化一个查找表:
// -- squares.js --
console.log("Initializing squares");
const squares = Array.from({ length: 10000 }, (_, index) => index ** 2);
export function getSquare(index) {
return squares[index];
}
导入模块可以公开一个同步函数,而查找表会一直等到需要时才初始化:
// -- main.js --
import defer * as squares from "./squares.js";
console.log("Ready");
export function showSquare(index) {
console.log(squares.getSquare(index));
}
showSquare(3); // 输出 "Initializing squares",然后输出 9
showSquare(4); // 输出 16;不会重复初始化
"Ready" 会先于 "Initializing squares" 输出。如果从未调用 showSquare(),且没有其他导入触发 squares.js 执行,那么查找表就永远不会初始化。
延迟具有异步依赖的模块
在此示例中,report.js 本身是同步模块,它同时依赖一个异步配置模块和一个同步格式化模块。
// -- config.js --
export const locale = await Promise.resolve("en-US");
console.log("Configuration ready");
// -- format.js --
console.log("Formatting module evaluated");
export function format(value, locale) {
return new Intl.NumberFormat(locale).format(value);
}
// -- report.js --
import { locale } from "./config.js";
import { format } from "./format.js";
console.log("Report module evaluated");
export function createReport(value) {
return format(value, locale);
}
// -- main.js --
import defer * as report from "./report.js";
console.log("Main module evaluated");
console.log(report.createReport(1000));
输出如下:
Configuration ready Main module evaluated Formatting module evaluated Report module evaluated 1,000
由于 config.js 包含顶层 await,它会在 main.js 之前执行。执行 config.js 不需要运行 format.js 或 report.js,因此这两个模块会延迟到访问 report.createReport 时才执行。
捕获执行错误
// -- broken.js --
export const value = 1;
throw new Error("Initialization failed");
// -- main.js --
import defer * as broken from "./broken.js";
for (let attempt = 0; attempt < 2; attempt++) {
try {
console.log(broken.value);
} catch (error) {
console.log(error.message); // 两次都会输出 "Initialization failed"
}
}
尽管 value 在抛出异常之前已经初始化,通过延迟命名空间访问它仍会抛出缓存的执行错误。
导出延迟命名空间
没有 export defer 语法(更多信息请参见 export)。你可以导入延迟命名空间,然后导出其绑定,而不会触发执行:
// -- features.js --
import defer * as report from "./report.js";
export { report };
// -- main.js --
import { report } from "./features.js";
// 通过 report 访问导出会触发延迟执行。
console.log(report.createReport(1000));
规范
| 规范 |
|---|
| Deferred Imports Evaluation> # sec-imports> |