先记住这个答案
会。import './module' 会加载并执行该模块的顶层代码,包括所有可执行的副作用语句,但不会把任何导出绑定到当前模块作用域。它用于只需模块运行效果(如全局注册、环境初始化)而无需引用其导出的情形,且模块只执行一次。
- 纯副作用导入会执行模块代码。
- 不产生任何导入绑定。
- 模块只执行一次且静态提升。
静态解析下的模块执行顺序
在 ESM 中,import 声明总是被提升到模块顶部,这种静态结构使得依赖图在代码执行前就完成链接。对 import './module' 而言,链接阶段不会建立任何导出绑定,但求值阶段仍会完整运行该模块的顶层代码。这意味着副作用在所有后续语句之前发生,无论它在文件中哪个位置写法。
模块的执行遵循深度优先顺序,每个模块只求值一次,缓存结果。如果多个导入语句引用同一个模块,第一个执行会触发求值,后续导入直接复用已完成的模块记录。因此纯副作用导入不会导致重复执行,但它会阻塞依赖它的模块——只有副作用模块代码运行完,当前模块才继续执行。
注册全局 hook 的初始化模块
假设有一个库 error-tracker.js,它需在应用启动时向 window 注入错误监听器并设置全局错误处理器,但其 API 不导出任何内容。在入口模块顶部写 import './error-tracker.js',就能保证该初始化代码先于业务代码执行,无需手动调用。
这种写法比动态 import() 更可靠,因为静态导入能被构建工具提前分析和优化,且执行顺序明确——在模块图遍历时就会触发。如果改用 commonjs 风格的 require 则无法在各环境中统一行为,而纯副作用导入是 ESM 标准的一部分。
依赖顺序与执行失败的边界
如果副作用模块依赖另一个模块的导出,但导入顺序不当,可能触发暂时性死区。比如在副作用模块中读取一个尚未初始化的 let 绑定,会抛 ReferenceError。这种问题在循环依赖中更常见,纯副作用导入可能让错误更隐蔽,因为看不到实际引用的值。
还有一点:静态的纯副作用导入无法条件执行。若只想在开发环境启用某个模块,必须使用动态 import() 配合标志判断,或者依赖打包器的替代机制。此外,若副作用模块顶层抛异常,整个模块图求值立即失败,后续模块不会执行。
容易答错的地方
- 认为纯副作用导入不执行代码
- 错误。
import './mod'与普通导入一样会触发模块求值,只是不引入绑定。许多初学者误以为只有发生了引用的变量才会加载模块,实际上任何 import 声明都会让模块运行。 - 将纯副作用导入与动态 import 混淆
- 动态
import()返回 Promise 且可异步、条件执行,而纯副作用导入是静态、无条件且阻塞后续。二者设计目标不同,前者需处理回调,后者在模块链接时统一处理。
面试官还会怎么问?
同一个模块既被纯副作用导入又被命名导入会执行两次吗?
不会。ESM 模块记录一旦求值完成,后续导入都会复用同一实例。纯副作用导入与命名导入在第一次出现时触发执行,之后导入只读取缓存,因此只执行一次。
纯副作用导入能导出绑定吗?它和 `import * as ns` 有何不同?
import './mod' 不创建任何绑定,而 import * as ns 会生成命名空间对象。后者用于访问导出值,前者纯粹为了副作用。若模块有导出但你想忽略它们,使用纯副作用导入是更明确的信号。
为什么构建工具能对纯副作用导入进行 tree-shaking?
有些打包器会假定纯副作用导入可被移除,但标准 ESM 并不保证。正确做法是在库的 package.json 中标注 sideEffects: false,否则直接删除会导致运行时错误,因为模块可能确实有依赖的环境操作。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。