面试官:聊聊ESModule的工作流程和实现原理

概述

对于 ESModule的工作流程主要包含以下三个步骤:

  • 构造(Construction) --- 找到、下载并解析所有文件为模块记录。
  • 实例化(Instantiation) --- 在内存中找到位置用于存放所有的导出值,但是不用实际值来填充它们。然后让导出和导入都指向内存中的这些位置。这被称为链接(linking)。
  • 评估(Evaluation) --- 运行代码以真实值填充这些位置。

下面我们结合源码和构建工具处理的逻辑,来理解这三个步骤

构造

这个过程涉及找到,并下载必要的模块文件。一旦找到这些文件,JavaScript 会解析这些文件并将它们转换为模块记录。模块记录是包含有关模块所有导出、导入以及源代码的信息的数据结构。

javaScript 复制代码
// 导入模块
import { a } from './test.js';

实例化

在这个阶段,JavaScript 引擎会为所有导出的变量找到内存位置,并且原始的导入和导出声明都会被替换为这些内存位置的引用。这个过程称为链接,是实现模块间的交互的关键步骤。

下面我们通过源码来理解:CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用

CommonJS

对于 CommonJS,具体可以看我之前的文章深入了解 CommonJs 如何在浏览器运行

CommonJS源码:

javaScript 复制代码
// index.js
const { a } = require('./test')
console.log(a)
setTimeout(() => {
  console.log(a)
}, 500)

// test.js
let a = 0
setTimeout(() => {
  a = 1
}, 500)
module.exports = {
  a: a
}

CommonJS 构建后简化代码

javaScript 复制代码
let a = 0
const module = {
  exports: {
    a
  },
};

function test(module, exports) {
  console.log(exports.a)
  setTimeout(() => {
    console.log(exports.a)
  }, 500)
}
setTimeout(() => {
  a = 1
}, 500)
test(module, module.exports)
// 打印结果: 0 0

ESModule

ESmodule规范实现跟上面一样逻辑的代码

javaScript 复制代码
// index.js
import a from './test'
console.log(a)
setTimeout(() => {
  console.log(a)
}, 500)

// test.js
export let a = 0
setTimeout(() => {
  a = 1
}, 500)

ESModule 构建后简化代码

javaScript 复制代码
__webpack_require__ = {}
// 判断是否已经有该私有属性
__webpack_require__.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop)
// 输出值的引用的关键实现
__webpack_require__.d = (exports, definition) => {
  for (var key in definition) {
    if (
      __webpack_require__.o(definition, key) &&
      !__webpack_require__.o(exports, key)
    ) {
      Object.defineProperty(exports, key, {
        enumerable: true,
        get: definition[key], // 通过get方法巧妙返回值的引用
      });
    }
  }
}
let a = 0
const module = {
  exports: {},
};
__webpack_require__.d(module.exports, {a: () => (a)})

function test(module, exports) {
  console.log(exports.a)
  setTimeout(() => {
    console.log(exports.a)
  }, 500)
}
setTimeout(() => {
  a = 1
}, 500)

test(module, module.exports)
// 打印结果: 0 1

defineProperty

可以对比打印结果,我们可以感受到值的拷贝和值的引用的区别,而对于值的引用的关键实现就是:

javaScript 复制代码
Object.defineProperty(exports, key, {
  enumerable: true,
  get: definition[key], // 通过get方法巧妙返回值的引用
});

使用Object.defineProperty能提供更高层次的控制,可以控制属性是否可枚举、可写、可配置,并且可以提供getter和setter方法,使得在访问和设置属性时可以执行自定义的行为。但是,如果不需要这些额外的控制,那么使用普通的赋值方式(exports[key]=definition[key])会更简单、清晰。

这里我们get: definition[key]来实现值的引用

评估

在这个过程中,JavaScript 引擎会实际运行模块的代码。所有的实际值(如函数和变量)都会被保存在之前实例化阶段建立的内存位置中。

tree-shaking

ESModule 尽量静态化处理,特点:

  • import 只能作为模块顶层的语句出现
  • import 的模块名是静态固定的
  • import 绑定不变

所以模块依赖关系是确认的,和运行时的状态无关,所以在运行前可以进行静态分析,删除无用代码,使得最终的打包文件更小

不过要注意,tree-shaking 不会影响到那些有副作用的模块。即使模块没有被明确导入,如果它在顶层范围有副作用(例如调用一个函数,修改一个全局变量),那么它就不会被剔除。因为这些副作用可能会影响到程序的运行。

  • 正常情况下:
javaScript 复制代码
// index.js
import { a } from "./test.js";
console.log(a);

// test.js
export let a = 1;
export let b = 3;

// 打包产物
(() => {
  "use strict";
  console.log(1);
})();
  • 增加有副作用的模块
javaScript 复制代码
// index.js
import { a } from "./test.js";
console.log(a);

// test.js
export let a = 1;
export let b = 3;
setTimeout(() => {
  b = 2;
}, 500);

// 打包产物
(() => {
  "use strict";
  let e = 3;
  setTimeout(() => {
    e = 2;
  }, 500),
    console.log(1);
})();

动态引入 import()

前面讲到 import 模块名是固定的,绑定也是不变的。所以下面的代码会报错:

JavaScript 复制代码
// 报错
if (a === 1) {
  import module from './module';
}

上面代码中,引擎处理 import 语句是在编译时,这时不会去分析或执行 if 语句,所以 import 语句放在 if 代码块之中毫无意义,因此会报句法错误,而不是执行时错误.

ES2020 提案 引入 import()函数,支持动态加载模块

JavaScript 复制代码
if (a === 1) {
  import('./module').then((module) => {
    // todo module
  })
}

而对于动态 import()的引入,如不会影响其他模块静态解析,而对于这部分动态模块的引入,打包工具主要做了以下事项:

  1. 将异步模块单独打包到一个文件
  2. 当运行到对应代码时,在 Promise 内创建一个 script 标签加载异步文件
  3. 监听 script 的 onload、onerror,进行加载成功或者失败时的处理

参考文献

hacks.mozilla.org/2018/03/es-... es6.ruanyifeng.com/#docs/modul...

相关推荐
拉不动的猪2 分钟前
前端常见数组分析
前端·javascript·面试
小吕学编程19 分钟前
ES练习册
java·前端·elasticsearch
Asthenia041226 分钟前
Netty编解码器详解与实战
前端
袁煦丞31 分钟前
每天省2小时!这个网盘神器让我告别云存储混乱(附内网穿透神操作)
前端·程序员·远程工作
一个专注写代码的程序媛2 小时前
vue组件间通信
前端·javascript·vue.js
一笑code2 小时前
美团社招一面
前端·javascript·vue.js
懒懒是个程序员2 小时前
layui时间范围
前端·javascript·layui
NoneCoder2 小时前
HTML响应式网页设计与跨平台适配
前端·html
凯哥19702 小时前
在 Uni-app 做的后台中使用 Howler.js 实现强大的音频播放功能
前端
烛阴2 小时前
面试必考!一招教你区分JavaScript静态函数和普通函数,快收藏!
前端·javascript