我们知道, ES Module 与 CommonJs 有一个比较大的差别在于,ES Module 中导出的变量只是一个占位符,并不在 import 的时候进行赋值操作,而是当真正用到的时候才会去 import 的模块中取值,而且导入的值只能在声明值的模块内部被修改。
所有的 import 的值都是动态绑定的,可以理解为它们指向同一块内存区,这在规范中称为 live binding。 那么,webpack 的运行时是怎么实现这一套机制的呢?
先上 demo 代码
`index.js`
import { a } from './async-data.mjs';
console.log('instance ', a);
setTimeout(() => {
// 根据 ESM 规范,a 此时应该为 2
console.log('a ', a);
}, 1000 );
`a.js`
let a = 1;
setTimeout(() => {
a = 2;
}, 0);
export { a } ;
复制代码
事实证明, webpack 编译出来的结果符合规范。
那么,webpack 是如何做到的呢?
我们来看看 webpack 打包出来的文件。
(functions(modules)){
///....
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ }
/******/ };
//....
})({
/***/ "./src/a.js":
/*!******************!*\
!*** ./src/a.js ***!
\******************/
/*! exports provided: a */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __wbpack_require__.d(__webpack_exports__, "a", function() { return a; });
let a = 1;
setTimeout(() => {
a = 2;
}, 0);
/***/ }),
/***/ "./src/index.mjs":
/*!***********************!*\
!*** ./src/index.mjs ***!
\***********************/
/*! no exports provided */
/***/ (function(__webpack_module__, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _a_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./a.js */ "./src/a.js");
console.log('instance ', _a_js__WEBPACK_IMPORTED_MODULE_0__["a"]);
setTimeout(() => {
console.log('a ', _a_js__WEBPACK_IMPORTED_MODULE_0__["a"]);
}, 1000 );
/***/ })
})
复制代码
我们可以看到,webpack 将值拷贝变成了函数调用,把 a 变成一个 getter,每次获取相当于一次函数调用,调用时返回模块内的值,相当于利用闭包实现了 live binding。
还有一处值得留意的细节是,webpack 把 export 放在了所有 import 的上方,这么做也是符合 ESM 语义的,因为在模块执行前,模块的代码就应该被 parse 一遍,模块的 import 和export 在当时就已经确定了。
那为什么 export 会在 import 之上呢? 因为 export 是一个没有副作用的语句,所做的仅仅是把 expert 出去的变量包裹在 get 方法里,这样能够有效解决循环引用的问题。而 import 是一个有副作用的函数,会跳到另一个模块中执行语句,当下一个模块依赖于当前模块(即造成了循环引用),提前 export 声明可以确定被引用模块的 export。
附加题: 如何利用 commonJs 实现 ESM 的 live binding 属性
本质上说,我们希望我们导出的变量,能够在之后通过对于模块内值的改动,影响到外部。
而 CommonJS 对于模块化的实现简单粗暴,就是通过立即执行函数实现了作用域的封装,而 module 是立即执行函数的参数之一,是 require 执行前事先准备好的一个对象,其他模块通过对 module 的赋值实现模块中值的导入。
那么,我们知道 js 中对象的赋值是引用传递,利用这个特性,我们就可以实现近似于 live binding 的效果。
只要我们 export 一个对象,那么对这个对象的修改,就会影响到所有 require 的值。
根据这个思路,我们上面的 demo 就可以用 commonJs 等价实现为下面的例子:
`index.js`
const a = require('./a.js');
console.log('instance ', a);
setTimeout(() => {
console.log('a ', a);
}, 1000 );
`a.js`
let b = { a: 1 }
setTimeout(() => {
b.a = 2;
}, 0);
module.exports = b;
复制代码
执行结果:
//-> node index.js
instance { a: 1 }
a { a: 2 }
复制代码