在研究 Vue3 源码时(3.2.45
版本)看到以下代码:
ts
// packages/runtime-core/src/apiAsyncComponent.ts
// interop module default
if (
comp &&
(comp.__esModule || comp[Symbol.toStringTag] === 'Module')
) {
comp = comp.default
}
__esModule
属性是 ES 模块的标识,如果 __esModule
属性为 true ,则说明该模块是 ES 模块。
如果模块的 Symbol.toStringTag
属性值为 Module
,则该模块也是 ES 模块。
在日常开发中,可以通过判断 __esModule
是否为 true ,Symbol.toStringTag
属性值是否为 Module
来判断一个模块是否为 ES 模块。
html
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script type="module" src="main.js"></script>
</body>
</html>
js
// b.js
export const name ='Tom'
export default function sayHello(){
console.log('hello, world')
}
js
// main.js
setTimeout(() => {
const result = import('./b.js')
result.then(res=>{
console.log(res)
})
}, 0);
在浏览器中运行 main.js 模块的代码
可以看到, ES 模块对象会有个值为 Module
的 Symbol.toStringTag
属性。我们可以将该属性作为 ES 模块的标识。
关于 Symbol.toStringTag 属性的认识
Symbol.toStringTag
属性可用于自定义对象的字符串描述。
所谓对象的字符串描述,就是对象调用 Object.prototype.toString()
方法时返回的字符串,例如:
js
const map = new Map()
Object.prototype.toString.call(map) // [object Map]
Object.prototype.toString()
返回 "[object Type]"
,这里的 Type
是对象的类型。如果对象有 Symbol.toStringTag
属性,其值是一个字符串,则它的值将被用作 Type
。
js
const map = new Map()
map[Symbol.toStringTag] // Map
不过一些早于 ES6 的对象没有 Symbol.toStringTag
属性,例如:
js
const arr = [5, 6, 7]
arr[Symbol.toStringTag] // undefined
但是没有 Symbol.toStringTag
属性的对象也是能通过 Object.prototype.toString()
方法获取该对象的字符串描述的
js
const arr = [5, 6, 7]
Object.prototype.toString.call(arr) // [object Array]
对于自定义的对象,开发者可通过 Symbol.toStringTag
属性自定义对象的字符串描述
js
class XiaoMing {
get [Symbol.toStringTag]() {
return '小明';
}
}
const xiaoMing = new XiaoMing()
console.log(Object.prototype.toString.call(xiaoMing));
// Expected output: "[object 小明]"
console.log(xiaoMing[Symbol.toStringTag]);
// Expected output: "小明"
关于 __esModule 属性的认识
__esModule
属性是 ES 模块的标识。
当使用打包工具,比如 Babel 、Webpack 或者使用 TypeScript 的编译器 tsc 将 ES 模块的代码转换成 CommonJS 模块的代码时,都在导出的模块对象(exports
)打上 __esModule
为 true 的标记。
个人推测,这是为了让打包工具或编译器知道生成的代码的源码是 ES 模块的。
让我们看看 Babel 的例子,该例子的整体结构为:
Babel 的配置如下:
js
// babel.config.js
const presets = [
[
"@babel/preset-env",
{
targets: {
edge: "17",
firefox: "60",
chrome: "67",
safari: "11.1",
},
useBuiltIns: "usage",
corejs: "3.6.4",
modules: "cjs",
},
],
];
module.exports = { presets };
整个 Babel 例子的依赖为:
在 src 文件夹下创建 add.js 、index.js 文件:
js
// src/add.js
function add(a, b) {
return a + b;
}
export default add
js
// src/index.js
import add from "./add.js";
const total = add(2, 3);
console.log(total);
add.js 、index.js 都是 ES 模块的,我们打算使用 Babel 将他们都转成 CommonJS 的。
在 package.json 的 scripts
创建 npm scripts 命令:
json
{
"scripts": {
"babel": "babel src --out-dir lib"
}
}
在 npm scripts 下编写的命令,会自动在
node_modules
下的.bin
目录下寻找。 上面配置中的babel src --out-dir lib
,相当于是./node_modules/.bin/babel src --out-dir lib
。
然后在终端 npm run babel
命令,得到代码转换后的结果:
js
// lib/add.js
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
function add(a, b) {
return a + b;
}
var _default = exports.default = add;
js
// lib/index.js
"use strict";
var _add = _interopRequireDefault(require("./add.js"));
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
const total = (0, _add.default)(2, 3);
console.log(total);
在 lib/add.js
中可以看到,导出的对象 exports
被打上了 __esModule
为 true 的标记。
我们再来看看 Webpack 的例子,整个 Webpack 例子的项目结构如下:
对应的 package.json 文件配置为:
json
{
"private": "true",
"scripts": {
"webpack": "webpack --config webpack.config.js"
},
"dependencies": {
"webpack": "^5.92.0",
"webpack-cli": "^5.1.4"
}
}
在 src 文件夹下创建 add.js
和 index.js
文件,代码分别为:
js
// src/add.js
function add(a, b) {
return a + b;
}
export default add
js
// src/index.js
import add from "./add.js";
const total = add(2, 3);
console.log(total);
在终端运行 npm run webpack
命令,可得代码打包后的结果为(删去了大部分注释):
js
// dist/main.js
(() => {
"use strict";
var __webpack_modules__ = {
"./src/add.js": (
__unused_webpack_module,
__webpack_exports__,
__webpack_require__
) => {
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
default: () => __WEBPACK_DEFAULT_EXPORT__,
});
function add(a, b) {
return a + b;
}
const __WEBPACK_DEFAULT_EXPORT__ = add;
},
};
// The module cache
var __webpack_module_cache__ = {};
// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// Create a new module (and put it into the cache)
var module = (__webpack_module_cache__[moduleId] = {
// no module.id needed
// no module.loaded needed
exports: {},
});
// Execute the module function
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
// Return the exports of the module
return module.exports;
}
(() => {
// define getter functions for harmony 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);
})();
(() => {
// define __esModule on exports
__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 _add_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
/*! ./add.js */ "./src/add.js"
);
const total = (0, _add_js__WEBPACK_IMPORTED_MODULE_0__["default"])(2, 3);
console.log(total);
})();
上面的转换结果中出现了与本文主题相关的两个属性 Symbol.toStringTag
、__esModule
其实这两个属性的目的都是为转换后的代码打上 ES 模块的标记,用于告诉 Webpack 等其他打包工具,该打包后的代码源码为 ES 模块。
最后再看看 TypeScript 的例子,在 Typescript 中,将 ES 模块的代码编译成 CommonJS 的,也会在输出的代码结果上打上 __esModule
为 true 的标记。
TypeScript 例子的整体目录结构如下:
相关的编译配置为(tsconfig.json
):
json
{
"compilerOptions": {
"module": "commonjs",
"noImplicitAny": true,
"removeComments": true,
"preserveConstEnums": true,
"sourceMap": true,
"outDir": "dist"
},
"include": ["src/**/*"],
}
package.json 配置如下:
json
{
"private": "true",
"scripts": {
"tsc": "tsc"
},
"devDependencies": {
"typescript": "^5.4.5"
}
}
然后在 src 下创建 add.ts
、index.ts
文件,代码分别为:
ts
// src/add.ts
function add(a:number, b:number) {
return a + b;
}
export default add
ts
// src/index.ts
import add from "./add.js";
const total = add(2, 3);
console.log(total);
在终端运行 npm run tsc
命令,可得代码转换的结果为:
js
// dist/add.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
function add(a, b) {
return a + b;
}
exports.default = add;
js
// dist/index.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var add_js_1 = require("./add.js");
var total = (0, add_js_1.default)(2, 3);
console.log(total);
可以看到 TypeScript 编译器也会把 ES 模块的代码编译成 CommonJS 后,在编译后的结果代码中,将导出的 exports
对象打上 __esModule
为 true 的标记。
读源码的一些感想
一开始笔者并不知道下面两行代码中 __esModule
、Symbol.toStringTag
属性是做什么用的,只是直觉告诉我可能跟 ES 模块有关系。
ts
// packages/runtime-core/src/apiAsyncComponent.ts
// interop module default
if (
comp &&
(comp.__esModule || comp[Symbol.toStringTag] === 'Module')
) {
comp = comp.default
}
然后为了弄懂这两行代码,查阅了许多的资料,便输出了以下文章:
因此,在阅读优秀框架源码的过程中,通过深挖一个点,可以学到很多相关知识,最后很多知识点都可以通过一条线连接起来,所以掘友们,现在就开始看源码吧!
知识点总结
Symbol.toStringTag
属性用于自定义对象的字符串描述。在浏览器中,可判断对像的 Symbol.toStringTag
属性是否为 Module
来判断该模块是否为 ES 模块。
__esModule
是 ES 模块的标识,在一些打包工具或编译器中,比如:Babel 、Webpack 、Typescript 的编译器 tsc ,会将 ES 模块转成 CommonJS 模块后,在导出的模块对象 exports
打上 __esModule
为 true 的标识。
js
Object.defineProperty(exports, "__esModule", { value: true });
综上所述,在日常的开发中,可借助判断 __esModule
是否为 true 或 Symbol.toStringTag
是否为 Module
判断当前模块是否为 ES 模块。