Webpack 如何实现 live binding

1,627 阅读3分钟

我们知道, 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 }