关于 import 模块导入执行顺序的思考

692 阅读5分钟

关于 import 模块导入执行顺序的思考

一、引言

首先抛出一个问题:以下代码的打印输出顺序会是怎样?

module.js

console.log('模块被加载和执行。');

export const foo = () => {
  console.log('foo函数被调用。');
};

main.js

console.log('我会先执行吗?');
import { foo } from './module.js';

console.log('main模块的代码执行。');
foo();

乍看之下,可能会认为输出如下:

我会先执行吗?
模块被加载和执行。
main模块的代码执行。
foo函数被调用。

然而,实际输出是:

模块被加载和执行。
我会先执行吗?
main模块的代码执行。
foo函数被调用。

为什么会这样呢?为什么不像我们预期的那样按代码顺序执行?


二、解释

通过查阅资料,可以总结出以下几点原因:

  1. 依赖解析: 在 JavaScript 中,当一个模块被导入时,JavaScript 引擎会立即加载并执行该模块的顶层代码,以解析任何导出的值或功能。
  2. 单例模式: 每个模块只会被加载和执行一次,无论被导入多少次。因此,模块可以提供全局一致的状态或接口。
  3. 初始化副作用: 模块可能包含初始化逻辑(例如设置全局变量、注册事件监听器等)。这部分逻辑在导入时需要立即执行。
  4. 同步加载: ES6 模块的导入是同步的,这意味着在模块代码执行完成之前,导入语句之后的代码不会继续执行。

简单来说,只要文件中包含 import,JavaScript 引擎会先解析并执行所有导入的模块代码,再继续执行当前文件的代码。就像“变量提升”的概念一样。


三、验证

那么,有同学可能会问了(谁问你啊?😒)

问:为什么是 mjs 啊?  这是啥啊?

答曰:问得好啊

.mjs文件是一个ECMAScript模块的JavaScript文件。这种文件格式用于明确地告诉Node.js或者浏览器,该文件应该作为一个模块来处理,使用ES6的importexport语句来导入和导出模块成员。这与传统的.js文件形成对比,后者可以作为脚本或模块来运行,这取决于它们是如何被引入的。.mjs文件扩展名强制文件被视为模块,这有助于在不同环境下一致地处理模块代码。

其实写这个格式,你就可以直接在 VScode 直接执行这个文件,当然是用 node 环境哈。

写完了,直接点击右上角那个播放图标一样的东西就行了,如图所示,是不是很方便呢?

1.png

2.png

验证 1:基本验证

main.mjs

console.log('我会先执行吗?');

import { foo } from './module.js';

console.log('main模块的代码执行。');
foo();

module.mjs

console.log('模块被加载和执行。');

export const foo = () => {
  console.log('foo函数被调用。');
};

执行结果:

模块被加载和执行。
我会先执行吗?
main模块的代码执行。
foo函数被调用。

验证了 模块先加载执行,然后再执行 main 文件的代码

验证 2:调整 import 位置

import 移动到 foo() 调用之后: 会不会报错?因为我们印象里,要使用一个方法,肯定要先声明对不对?

main.mjs

console.log('我会先执行吗?');

console.log('main模块的代码执行。');
foo();

import { foo } from './module.js';

执行结果:

模块被加载和执行。
我会先执行吗?
main模块的代码执行。
foo函数被调用。

很遗憾,跟上面结果是一致的,那咱们就能够证明,只要有 import,无论放在哪一行,甚至是执行方法之后,都能够正常执行这个方法

验证 3:多个模块导入

module1.mjs

console.log('模块1被加载和执行。');

export const foo1 = () => {
  console.log('foo1函数被调用。');
};

module2.mjs

console.log('模块2被加载和执行。');

export const foo2 = () => {
  console.log('foo2函数被调用。');
};

main.mjs

console.log('我会先执行吗?');

import { foo1 } from './module1.mjs';
import { foo2 } from './module2.mjs';

console.log('main模块的代码执行。');
foo1();
foo2();

执行结果:

模块1被加载和执行。
模块2被加载和执行。
我会先执行吗?
main模块的代码执行。
foo1函数被调用。
foo2函数被调用。

即使有多个模块,仍然遵循 先加载模块,再执行主文件 的顺序。

验证 4:调整导入顺序

交换导入顺序:

main.mjs

console.log('我会先执行吗?');

import { foo2 } from './module2.mjs';
import { foo1 } from './module1.mjs';

console.log('main模块的代码执行。');
foo1();
foo2();

执行结果:

模块2被加载和执行。
模块1被加载和执行。
我会先执行吗?
main模块的代码执行。
foo1函数被调用。
foo2函数被调用。

导入的执行顺序与代码中的 import 顺序一致,也就是说根据谁先 import 谁先执行


四、总结

通过以上验证,可以总结出 ES6 模块导入的执行顺序:

  1. 模块加载:遇到 import 语句时,JavaScript 引擎会先加载并解析所有导入的模块。
  2. 模块执行:每个模块的顶层代码会立即执行,按 import 的顺序依次进行。
  3. 导出处理:模块中的导出内容在加载后即可被主文件使用。
  4. 主文件执行:所有模块加载完成后,主文件的代码才会继续执行。

这种机制保证了模块依赖的解析顺序和代码执行的正确性。在模块化开发中,理解这种执行顺序有助于更好地管理依赖关系和优化代码结构。