由于实习公司使用的是vue3.0,所以最近要开始学习vue3.0。 vue3.0有很多的前置知识,TS esm等。并且vue3.0的源码也非常值得一读。希望在完成日常的开发工作之后,也能逐步开始学习vue3的源码。
参考链接:commonjs模块和ES6模块
在学习nodejs刚刚入门的时候,想到之前碰到过得面试题:require和import的区别。所以借鉴上面这篇博客,总结一点自己的所学。
面试题
require方式的引入
// base.js
let count = 0;
setTimeout(() => {
console.log("base.count", ++count); // 1
}, 500)
module.exports.count = count;
// commonjs.js
const { count } = require('./base');
setTimeout(() => {
console.log("count is" + count + 'in commonjs'); // 0
}, 1000)
// base.js
const name = 'wjb'
const age = 12
function sayHello(name){
console.log('hello '+name)
}
exports.name = name
exports.age = age
exports.sayHello = sayHello
setTimeout(() => {
exports.name = 'fy'
}, 1000);
setTimeout(() => {
console.log(exports.name)
}, 3300);
// main.js
const obj = require('./base');
const name = obj.name
const age = obj.age
const sayHello = obj.sayHello
console.log(name)
console.log(age)
sayHello('kobe')
setTimeout(() => {
obj.name = 'gaile'
}, 2000);
import方式的引入
// base1.mjs
let count = 0;
setTimeout(() => {
console.log("base.count", ++count); // 1
}, 500)
export {count};
// es6.mjs
import { count } from './base1';
setTimeout(() => {
console.log("count is" + count + 'in es6'); // 1
}, 1000)
commonJS 模块
Module 模块编译
在Node中,每个文件模块都是一个对象,在源码中Module的定义如下:
function Module(id = '', parent) {
this.id = id;
this.path = path.dirname(id);
setOwnProperty(this, 'exports', {});
moduleParentCache.set(this, parent);
updateChildren(parent, this, false);
this.filename = null;
this.loaded = false;
this.children = [];
}
我们可以看出,nodejs中的源码对于Module采用函数的方法定义类,并且在new Module的时候,会初始化上述几个属性。
- module.id 模块的识别符,通常是带有绝对路径的模块文件名。
- module.filename 模块的文件名,带有绝对路径。
- module.loaded 返回一个布尔值,表示模块是否已经完成加载。
- module.parent 返回一个对象,表示调用该模块的模块。
- module.children 返回一个数组,表示该模块要用到的其他模块。
- module.exports 表示模块对外输出的值。
module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量。
再来看一看通过 console.log(module)输出的内容
我们重点关注一下
loaded和children,其中loaded是表示模块是否加载过了,在其他require当前这个模块的时候,如果loaded是true的话,会直接读取缓存。
children是表示当前模块引入的其他模块,我在这个文件中引入了node_test的模块,所以当前模块的子模块有node_test,并且模块的id是模块所在的路径。因为引入模块的时候会先执行一次模块的内容,所以loaded是true。
再在这里关注一下path。讲到path是先要讲讲require的引入顺序的。这里给出一个nodejs的官方文档,看看官方文档是怎么描述require的执行顺序的require的引入优先级
require(X) from module at path Y
1. If X is a core module,
a. return the core module
b. STOP
2. If X begins with '/'
a. set Y to be the filesystem root
3. If X begins with './' or '/' or '../'
a. LOAD_AS_FILE(Y + X)
b. LOAD_AS_DIRECTORY(Y + X)
c. THROW "not found"
4. If X begins with '#'
a. LOAD_PACKAGE_IMPORTS(X, dirname(Y))
5. LOAD_PACKAGE_SELF(X, dirname(Y))
6. LOAD_NODE_MODULES(X, dirname(Y))
7. THROW "not found"
.......(截取一部分)
简单描述一下require查找模块的过程
- x是一个核心模块(比如 http、path、fs),直接返回core module
- X是以 ./ 或 ../ 或 /(根目录)开头的。如果文件有后缀名,那么就按后缀名返回文件
如果没有后缀名,就按照如下顺序进行读取:
1.直接查找文件名 2. 依次查找
.js.jsonX.node - 如果安装文件名没有查找到,那么就按照路径名进行查找,并且依次查找路径名下的
index.jsindex.jsonindex.node - 直接是一个X(没有路径),并且X不是一个核心模块。那么就按照
module.path进行查找。可以见上面的贴图。
CommonJS 模块的加载原理
CommonJs 规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的 exports属性(即module.exports)是对外的接口,加载某个模块,其实是加载该模块的module.exports属性。
commonjs模块的特点
- 所有代码运行在模块作用域,不会污染全局作用域。
- 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
- 模块加载的顺序,按照其在代码中出现的顺序。模块加载的顺序是
DFS
模块的缓存
第一次加载某个模块时,Node会缓存该模块。以后再加载该模块,就直接从缓存取出该模块的module.exports属性。
require('./example.js');
require('./example.js').message = "hello";
require('./example.js').message
// "hello"
console.log(require(./example.js).message) // 能正常输出hello,说明模块仅仅加载一次,之后require都是直接访问之前加载的缓存
上面代码中,连续三次使用require命令,加载同一个模块。第二次加载的时候,为输出的对象添加了一个message属性。但是第三次加载的时候,这个message属性依然存在,这就证明require命令并没有重新加载模块文件,而是输出了缓存。
// 删除指定模块的缓存
delete require.cache[moduleName];
// 删除所有模块的缓存
Object.keys(require.cache).forEach(function(key) {
delete require.cache[key];
})
注意,缓存是根据绝对路径识别模块的,如果同样的模块名,但是保存在不同的路径,require命令还是会重新加载该模块
commonjs的循环依赖
// a.js
exports.done = false;
let b = require('./b');
console.log('a.js: b.done = %j', b.done); // true
exports.done = true;
console.log('a.js执行完毕');
// b.js
exports.done = false;
let a = require('./a'); // a.js停止
console.log('b.js: a.done = %j', a.done); // false
exports.done = true;
console.log('b.js执行完毕');
// main.js
/* 执行顺序
先要明确一个原则:CommonJS代码一个很重要的原则是,
在代码加载时(require时)执行
1. 引入 a.js
2. 执行 a.js到第二行
3. a.js 引入 b.js,所以执行b.js 代码,
此时 a.js代码等待b.js执行完毕再执行,所以a.js先暂停执行
4. 上面代码之中,b.js执行到第二行,就会去加载a.js,这时,就发生了“循环加载”。
系统会去a.js模块对应对象的exports属性取值,可是因为a.js还没有执行完,
从exports属性只能取回已经执行的部分,而不是最后的值。所以b.js 中输出false
5.继续执行a.js,执行完毕。所以在main.js中 输出 true
*/
// 上面的代码证明了两件事。
// 一是,在b.js之中,a.js没有执行完毕,只执行了第一行。
// 二是,main.js执行到第二行require('b.js')时,不会再次执行b.js,
// 而是输出缓存的b.js的执行结果,即它的第四行。
let a = require('./a');
let b = require('./b');
console.log('main.js: a.done = %j, b.done = %j', a.done, b.done); // true true
/*
输出结果
b.js: a.done = false
b.js执行完毕
a.js: b.done = true
a.js执行完毕
main.js: a.done = true, b.done = true
*/
ESM ES6模块
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。
CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。
commojs的运行时加载
// CommonJS模块
let { stat, exists, readFile } = require('fs');
// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
require本质就是一个函数,可以放在模块内的任何地方进行执行。
上面代码的实质是整体加载fs模块(即加载fs的所有方法),生成一个对象_fs,然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。
ES6模块的加载
ES6模块不是对象,而是通过export关键字显式指定输出的代码,再通过import关键字输入。
export的三种方法
1. export let name = 'www'
2. export {name}
3. export {name as nm}
import的三种方法
1. import {name} from '模块名'
2. import {name as nm} from '模块名'
3. import * as obj from '模块名'
defalut的用法 前面介绍的三种import和export方法都需要知道导出导入的具体包名称。default的默认导出就不需要知道我们导出的变量的名称,并且在imoport的时候不需要使用 {},并且可以自己指定名称
export default ...
import ... from ''
// ES6模块
import { stat, exists, readFile } from 'fs';
上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载称为“**编译时加载**”或者静态加载,即 ES6 可以在编译时(v8引擎 parse阶段 )就完成模块加载,效率要比 CommonJS 模块的加载方式高。
ES6模块的循环引用
// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar);
export let foo = 'foo';
// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo);
export let bar = 'bar';
上面代码中,a.mjs加载b.mjs,b.mjs又加载a.mjs,构成循环加载。执行a.mjs,结果如下。
$ node --experimental-modules a.mjs
b.mjs
ReferenceError: foo is not defined
至于 foo not found,且请看官细细往下看,将在 import/export的提升这个小节中讲到
nodejs环境下如何处理ES6模块
esm的模块在.js文件下是不被支持的。在明白了之前两种模块的差异之后。思考一下为什么在 .js文件中 es6的模块不被支持。原因是:require引入的模块是引入的时候就被立即执行的(在之前没有被引入过得情况下,也就是内存中没有缓存的情况下。)是一种同步加载。但是esm是异步加载的,也就是说esm的模块有一个独立的静态解析的阶段,依赖关系就是在这个静态解析的阶段完成的。(之后写一篇文章专门学习一下esm的独立静态解析的阶段)
node环境下执行es6模块的两个方法
- 将
.js文件的后缀名改为.mjs,那么node就会支持esm了。 - 在该项目的package.json中,将
type指定为 module
{
"type": "module"
}
node 下定义commonjs的模块的两个方法
.cjs,只能当成 commonjs模块执行.js并且不知道 type
commonjs 模块加载 esm的模块
commonjs为什么不能加载esm的模块,就是因为esm的模块在import的时候是异步加载的。所以我们只需要在commonjs中用 await等待,就可以实现commonjs的同步加载
// 在main.js 中引入 a.mjs
(async () => {
await import('./a.mjs');
})();
在 esm 中引入 commonjs
esm中是可以引入 commonjs的模块的,但是需要注意引入的模块只能整体加载,不能局部加载(引入时解构)
// 正确
import packageMain from 'commonjs-package';
// 报错
import { method } from 'commonjs-package';
这是因为 ES6 模块需要支持静态代码分析,而 CommonJS 模块的输出接口是module.exports,是一个对象,无法被静态分析,所以只能整体加载。
import packageMain from 'commonjs-package';
const { method } = packageMain;
注意:注意,ES6 模块与 CommonJS 模块尽量不要混用。require命令不能加载.mjs文件,会报错,只有import命令才可以加载.mjs文件。反过来,.mjs文件里面也不能使用require命令,必须使用import。
ES6模块和CommonJs模块主要有以下区别
1.CommonJs模块输出的是一个值的拷贝,ES6模块输出的是值的引用。
-
CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。请看下面这个模块文件lib.js的例子。
// lib.js const counter = 3; function incCounter() { counter++; } module.exports = { counter: counter, incCounter: incCounter, };// main.js const mod = require('./lib'); console.log(mod.counter); // 3 mod.incCounter(); console.log(mod.counter); // 3上面代码说明,lib.js 模块加载以后,它的内部变化就影响不到输出的 mod.counter了。这是因为 mod.counter是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值
// lib.js const counter = 3; function incCounter() { counter++; } module.exports = { get counter() { return counter }, incCounter: incCounter, }; //////因为 counter是一个取值器函数 get,访问counter的时候就运行get counter函数,所以会返回4(下面的main函数运行的结果)// main.js const mod = require('./lib'); console.log(mod.counter); // 3 mod.incCounter(); console.log(mod.counter); // 4 -
ES6模块输出的是值的引用
ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import有点像 Unix 系统的“符号连接”,原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
// mod.js function C() { this.sum = 0; this.add = function () { this.sum += 1; }; this.show = function () { console.log(this.sum); }; } export let c = new C();// x.js import {c} from './mod'; c.add(); // y.js import {c} from './mod'; c.show(); // main.js import './x'; import './y'; // 输出 1,说明x.js 和 y.js import的是同一个实例c
CommonJs模块是运行时加载(只要没有缓存,那么require一次就执行一次),ES6模块是编译时输出接口,所以可以用来确定依赖关系。
esm存在export 和 import的提升
esm对import/export的提升,类似于var 和 function foo(){}方式定义的函数一样。会存在变量提升。多嘴一句,通过
const foo = ()=>{};
这种方式定义的函数,不会存在变量提升。 言归正传,如何去验证export 和 import是存在 提升的呢?
// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar);
export let foo = 'foo';
// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo);
export let bar = 'bar';
以上述这个例子为例。 先执行 a.mjs。关于 import的引入时机不是很清楚。我去查阅一些资料再来更新。因为我看很多博客也没有讲清楚这个import的引入时机,编译时引入。
一些前置知识:
题目
// index.js
console.log('running index.js');
import { sum } from './sum.js';
console.log(sum(1, 2));
// sum.js
console.log('running sum.js');
export const sum = (a, b) => a + b;
答案: running sum.js, running index.js, 3。
import命令是编译阶段执行的,在代码运行之前。因此这意味着被导入的模块会先运行,而导入模块的文件会后执行。这是CommonJS中require()和import之间的区别。使用require(),您可以在运行代码时根据需要加载依赖项。 如果我们使用require而不是import,running index.js,running sum.js,3会被依次打印。
// module.js
export default () => "Hello world"
export const name = "Lydia"
// index.js
import * as data from "./module"
console.log(data)
答案:{ default: function default(), name: "Lydia" }
使用import * as name语法,我们将module.js文件中所有export导入到index.js文件中,并且创建了一个名为data的新对象。 在module.js文件中,有两个导出:默认导出和命名导出。 默认导出是一个返回字符串“Hello World”的函数,命名导出是一个名为name的变量,其值为字符串“Lydia”。data对象具有默认导出的default属性,其他属性具有指定exports的名称及其对应的值。