[JavaScript]关于JS模块的循环引用问题
我也是从别处学习的,这里主要是做一个个人的整理、总结和记录
关于CommonJS 和 ESM 的区别:
1、CommonJS 是值得拷贝,发生在运行时,外部可以改动,且内部改动不会影响外部
2、ESM 是值的引用,发生在编译时,是readonly,外部改变会影响内部
在谈到CommonJS和ESM处理模块循环引用的区别时,经常会看到这样一个结论:
CommonJS模块是加载时执行。一旦出现某个模块被“循环加载”,就只输出已经执行的部分,没有执行的部分不会输出。 ESM模块对导出模块,变量,对象是动态引用,遇到模块加载命令import时不会去执行模块,只是生成一个指向被加载模块的引用。 ES6根本不会关心是否发生了"循环加载",只是生成一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。
接下来我们开始测试
a.js
javascript
console.log('a starting');
exports.done = false;
const b = require('./b.js');
console.log('in a, b.done = %j', b.done);
exports.done = true;
console.log('a done');
b.js
javascript
console.log('b starting');
exports.done = false;
const a = require('./a.js');
console.log('in b, a.done = %j', a.done);
exports.done = true;
console.log('b done');
main.js
css
console.log('main starting');
const a = require('./a.js');
const b = require('./b.js');
console.log('in main, a.done = %j, b.done = %j', a.done, b.done);
输出结果
css
main starting
a starting
b starting
in b, a.done = false
b done
in a, b.done = true
a done
in main, a.done = true, b.done = true
Webpack编译结果分析
javascript
(() => {
var __webpack_modules__ = {
"./src/commonJS/a.js": (
__unused_webpack_module,
exports,
__webpack_require__
) => {
console.log("a starting");
exports.done = false;
const b = __webpack_require__("./src/commonJS/b.js");
console.log("in a, b.done = %j", b.done);
exports.done = true;
console.log("a done");
},
"./src/commonJS/b.js": (
__unused_webpack_module,
exports,
__webpack_require__
) => {
console.log("b starting");
exports.done = false;
const a = __webpack_require__("./src/commonJS/a.js");
console.log("in b, a.done = %j", a.done);
exports.done = true;
console.log("b done");
},
};
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) {
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
var module = (__webpack_module_cache__[moduleId] = {
exports: {},
});
__webpack_modules__[moduleId](
module,
module.exports,
__webpack_require__
);
return module.exports;
}
(() => {
console.log("main starting");
const a = __webpack_require__("./src/commonJS/a.js");
const b = __webpack_require__("./src/commonJS/b.js");
console.log("in main, a.done = %j, b.done = %j", a.done, b.done);
})();
})();
Webpack中CommonJS模块化的实现,就是将a.js和b.js的内容外面包装一层函数,放入__webpack_modules__ 这个对象中。定义一个__webpack_require__方法,这样require一个模块就相当于给这包装后的函数传入一个exports对象,执行这个包装后的函数,将结果挂到传入的exports对象上。
处理循环引用,就是在执行这个require函数时,判断这个模块是否执行过,如果执行过就直接返回缓存结果。分析整个流程刚好和上面的输出结果对应的上
接下来再看看ESM
输出结果
css
b starting
in b a is undefined
b done
a starting
in a b is true
a done
main starting
in main, a = true, b = true
这里去发现变了,也刚好对应的上 CommonJS 是运行时的,而ESM是编辑时
Webpack编译结果分析
javascript
(() => {
"use strict";
var __webpack_modules__ = ({
"./src/esm/a.js":
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
"a": () => (a)
});
var _b_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/esm/b.js");
console.log("a starting");
console.log("in a b is ", _b_js__WEBPACK_IMPORTED_MODULE_0__.b);
var a = true;
console.log("a done");
}),
"./src/esm/b.js":
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
"b": () => (b)
});
var _a_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/esm/a.js");
console.log("b starting");
console.log("in b a is ", _a_js__WEBPACK_IMPORTED_MODULE_0__.a);
var b = true;
console.log("b done");
})
});
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) {
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
var module = __webpack_module_cache__[moduleId] = {
exports: {}
};
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}
(() => {
__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] });
}
}
};
})();
(() => {
__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
})();
(() => {
__webpack_require__.r = (exports) => {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
})();
var __webpack_exports__ = {};
(() => {
__webpack_require__.r(__webpack_exports__);
var _a_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/esm/a.js");
var _b_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__("./src/esm/b.js");
console.log("main starting");
console.log("in main, a = %j, b = %j", _a_js__WEBPACK_IMPORTED_MODULE_0__.a, _b_js__WEBPACK_IMPORTED_MODULE_1__.b);
})();
})();
我们发现输出结果是一样的,也是通过缓存的方式来处理循环引用的。但是处理的时机不一样,进一步证实CommonJS 是运行时的,而ESM是编辑时
我们进一步分析差别
javascript
var __webpack_modules__ = {
"./src/esm/a.js":
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
"a": () => (a)
});
var _b_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/esm/b.js");
console.log("a starting");
console.log("in a b is ", _b_js__WEBPACK_IMPORTED_MODULE_0__.b);
var a = true;
console.log("a done");
}),
}
我们发现,ESM编译后的代码,不是直接将导出变量挂在到__webpack_exports__对象上,而是通过定义一个函数的方式取这个变量。
我们再来看__webpack_require__.d这个函数的实现
javascript
__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],
});
}
}
};
通过给key设置get方法的方式,去调用定义的函数,去取export的值,通过这样的方式实现所谓的“引用”。
发表评论