[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的值,通过这样的方式实现所谓的“引用”。

标签

发表评论