Webpack 如何实现 live binding

我们知道, 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 }
复制代码
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Webpack 实现 Code Splitting 的方式主要有以下几种: 1. 基于入口点(Entry Points)的 Code Splitting:通过将应用程序拆分为多个入口点,每个入口点都可以生成一个独立的代码块,从而实现 Code Splitting。可以使用 Webpack 的 `entry` 配置选项来指定多个入口点,例如: ```js module.exports = { entry: { app: './src/app.js', vendor: './src/vendor.js' }, // ... } ``` 在上面的例子中,`app` 和 `vendor` 分别是两个入口点,它们可以生成独立的代码块。 2. 基于模块(Module)的 Code Splitting:通过将应用程序中的模块拆分为多个代码块,从而实现 Code Splitting。可以使用 Webpack 的 `import()` 函数来实现基于模块的 Code Splitting,例如: ```js function loadComponent() { // 动态加载异步模块 return import('./components/MyComponent.vue') } // 使用异步组件 Vue.component('my-component', () => ({ // 异步加载组件 component: loadComponent(), // 加载组件时显示的占位符 loading: LoadingComponent, // 加载组件失败时显示的占位符 error: ErrorComponent, // 组件加载成功后的延迟时间 delay: 200, // 最长等待时间 timeout: 10000 })) ``` 在上面的例子中,`loadComponent()` 函数返回一个异步加载的模块,可以通过 `import()` 函数来加载模块。然后,在组件中使用异步组件来加载动态组件。 3. 基于运行时(Runtime)的 Code Splitting:通过将应用程序的代码分成多个块,并在运行时动态加载这些代码块,从而实现 Code Splitting。可以使用 Webpack 的 `require.ensure()` 函数来实现基于运行时的 Code Splitting,例如: ```js function loadComponent() { // 动态加载异步模块 require.ensure(['./components/MyComponent.vue'], function() { // 加载成功后的回调函数 const MyComponent = require('./components/MyComponent.vue') // 渲染组件 new Vue({ el: '#app', render: h => h(MyComponent) }) }, function() { // 加载失败后的回调函数 }, 'my-component') } ``` 在上面的例子中,`require.ensure()` 函数用于异步加载模块,并在加载成功后执行回调函数。第三个参数 `'my-component'` 表示这个代码块的名称,用于指定 Webpack 生成的代码块文件名。 需要注意的是,虽然这些方式都可以实现 Code Splitting,但它们的实现方式和使用场景有所不同。因此,在实际开发中,应该根据具体的需求来选择合适的方式来实现 Code Splitting。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值