Open maicFir opened 2 years ago
项目中我们常常会接触到模块,最为典型代表的是esModule与commonjs,在es6之前还有AMD代表的seajs,requirejs,在项目模块加载的文件之间,我们如何选择,比如常常因为某个变量,我们需要动态加载某个文件,因此你想到了require('xxx'),我们也常常会用import方式导入路由组件或者文件,等等。因此我们有必要真正明白如何使用好它,并正确的用好它们。
esModule
commonjs
es6
AMD
seajs
requirejs
require('xxx')
import
以下是笔者对于模块理解,希望在实际项目中能给你带来一点思考和帮助。
模块
正文开始...
关于script加载的那几个标识,defer、async、module
script
defer
async
module
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>module</title> </head> <body> <div id="app"></div> <script src="./js/2.js" defer></script> <script src="./js/1.js" async></script> <script src="./js/3.js"> console.log('同步加载', 3) </script> </body> </html>
// js/2.js console.log('defer加载', 2); // js/1.js console.log('async异步加载不保证顺序', 1); // js/3.js console.log('同步加载', 3);
我们会发现执行顺序是3,1,2
3,1,2
defer与async是异步的,而同步加载的 3,在页面中优先执行了。在执行顺序中,我们可以知道标识的defer是等同步的3与async的1执行后才最后执行的。
同步的3
async的1
为了证明这点,我们在1.js中加入一段代码
1.js
// 1.js console.log('没有定时器的async', 1); setTimeout(() => { console.log('有定时器的async,异步加载不保证顺序', 1); }, 1000);
最后我们发现打印的顺序,同步加载3,(没有定时器的async)1、defer加载2、有定时器的async,异步加载不保证顺序1
同步加载3
(没有定时器的async)1
defer加载2
有定时器的async,异步加载不保证顺序1
因为1.js加入了一段定时器,在事件循环中,它是一段宏任务,我们知道在浏览器处理事件循环中,同步任务>异步任务[微任务 promise>宏任务 setTimeout,事件等],在2.js中用defer标识了自己是异步,但是1.js中有定时器,2.js实际上是等了1.js执行完了,再执行的。
宏任务
2.js
如果我在2.js中也加入定时器呢
console.log('没有定时器的defer加载', 2); setTimeout(() => { console.log('有定时器的defer加载', 2); }, 1000);
我们会发现结果依然是如此
3.js 同步加载 3 1.js 没有定时器的async 1 2.js 没有定时器的defer加载 2 1.js 有定时器的async,异步加载不保证顺序 1 2.js 有定时器的defer加载 2
不难发现 defer中的定时器脚本虽然在async标识的脚本前面,但是,注意两个定时器实际上是会有前后顺序的,跟脚本的顺序没有关系
两个任务都是定时器,都是宏任务,在脚本的执行顺序中第一个定时器会先放到队列任务中,第二个定时器也会放到队列中,遵循先进先出,第一个宏任务(1.js 有定时器)先进队列,然后2.js定时器再进入队列,后面再执行。
但是注意,定时器时间短的优先进入队列。
好了,搞明白defer与async的区别了,总结一句,defer会等其他脚本加载完了再执行,而async是异步的,并不一定是在前面的就先执行。
接下来我们来看看module
module是浏览器直接加载es6,我们注意到加载module中有哪些特性?
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>module</title> </head> <body> <div id="app"></div> <script src="./js/2.js" defer></script> <script src="./js/1.js" async></script> <script src="./js/3.js"></script> <script type="module"> import userInfo, { cityList } from './js/4.js'; console.log(userInfo); // { name: 'Maic', age: 18} console.log(cityList); console.log(this); // undefined /* [ { value: '北京', code: 1 }, { value: '上海', code: 0 } ] */ </script> </body> </html>
在js/4.js中,我们可以看到可以用esModule的方式输出
js/4.js
export default { name: 'Maic', age: 18 }; const cityList = [ { value: '北京', code: 1 }, { value: '上海', code: 0 } ]; export { cityList };
在script用type="module"后,内部顶层this就不再是window对象了,并且引入的外部路径不能省略后缀,且脚本自动采用严格模式。
type="module"
this
window
通常我们在项目中都是es6模块,在nodejs中大量模块代码都是采用commonjs的方式,既然项目里都有用到,那么我们再次回顾下他们有什么区别
es6模块
nodejs
参考module 加载实现中写道
1、commonjs输出的是一个值的拷贝,而es6模块输出的是一个只读值的引用
2、commonjs是在运行时加载,而es6模块是在编译时输出接口
3、commonjs的require()是同步加载,而es6的import xx from xxx是异步加载,有一个独立的模块解析阶段
require()
import xx from xxx
另外我们还要知道commonjs的require引入的是module.exports出来的对象或者属性。而es6模块不是对象,它对外暴露的接口是一种静态定义,在代码解析阶段就已经完成。
require
module.exports
举个例子,commonjs
// 5.js const userInfo = { name: 'Maic', age: 18 }; let count = 1; const countAge = () => { userInfo.age += 1; count++; console.log(`count:${count}`); }; module.exports = { userInfo, countAge, count }; // 6.js const { userInfo, countAge, count } = require('./5.js'); console.log(userInfo); // {name: 'Maic', age: 18} countAge(); // count:2 console.log(userInfo); // {name: 'Maic', age: 19} console.log(count); // 1
node 6.js 从打印里可以看出,一个原始的输出count,外部调用countAge并不会影响count输出的值,但是在内部countAge打印的仍是当前++后的值。
node 6.js
count
countAge
如果是es6模块,我们可以发现
const userInfo = { name: 'Maic', age: 18 }; let count = 1; const countAge = () => { userInfo.age += 1; count++; console.log('count', count); }; export { userInfo, countAge, count };
在页面中引入,我们可以发现
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>module</title> </head> <body> <div id="app"></div> ... <script type="module"> import userInfo, { cityList } from './js/4.js'; import { userInfo as nuseInfo, count, countAge } from './js/7.js'; console.log(userInfo, cityList); console.log(this); // { name: 'Maic', age: 18} countAge(); console.log(nuseInfo, count); // {name: 'Maic', age: 19} 2 </script> </body> </html>
我们发现count导出后的值是实时的改变了。因为它是一个值的引用。
接下来有疑问,比如我有一个工具函数
function Utils() { this.sum = 0; this.add = function () { this.sum += 1; }; this.sub = function () { this.sum-=1; } this.show = function () { console.log(this.sum); }; } export new Utils;
这工具函数,在很多地方会有引用,比如A,B,C...等页面都会引入它,那么它会每次都会实例化Utils?
A,B,C...
Utils
接下来我们实验下
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>module</title> </head> <body> <div id="app"></div> ... <script type="module"> // A import { utils } from './js/7.js'; utils.add(); console.log(utils); </script> <script type="module"> // B import { utils } from './js/7.js'; console.log('sum=', utils.sum); console.log(utils); </script> </body> </html>
// 7.js const userInfo = { name: 'Maic', age: 18 }; let count = 1; const countAge = () => { userInfo.age += 1; count++; console.log('count', count); }; function Utils() { this.sum = 0; this.add = function () { this.sum += 1; }; this.sub = function () { this.sum -= 1; }; this.show = function () { console.log(this.sum); }; } const utils = new Utils(); export { userInfo, countAge, count, utils };
我们会发现在A模块里调用utils.add()后,在B中打印utils.sum是1,那么证明B引入的utils与A是一样的。
A
utils.add()
B
utils.sum
1
utils
如果我输出的仅仅是一个构造函数呢?看下面
// 7.js ... function Utils() { this.sum = 0; this.add = function () { this.sum += 1; }; this.sub = function () { this.sum-=1; } this.show = function () { console.log(this.sum); }; } const utils = new Utils; const cutils = Utils; export { userInfo, countAge, count, utils, cutils };
页面同样引入
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>module</title> </head> <body> <div id="app"></div> ... <script type="module"> // A import { utils, cutils } from './js/7.js'; countAge(); console.log(nuseInfo, count); utils.add(); new cutils().add(); console.log(utils); </script> <script type="module"> // B import { utils, cutils } from './js/7.js'; console.log('sum=', utils.sum); console.log(utils); console.log('sum2=', new cutils().sum); // 0 </script> </body> </html>
我们会发现A中new cutils().add()在B中new cutils().sum)访问,依然是0,所以当模块中导出的是一个构造函数时,每一个模块对应new 导出的构造函数都是重新开辟了一个新的内存空间。
new cutils().add()
new cutils().sum)
0
new 导出的构造函数
因此可以得出结论,在es6模块中直接导出实例化对象的性能开销比直接导出构造函数更小些。
我们初步了解下CommonJS的加载
CommonJS
// A.js module.exports = { a: 1 }; // B.js const { a } = require('./A.js'); console.log(a); // 1
在执行require时,实际上内部会在内存中生成一个对象,require是一个nodejs环境提供的一个全局函数。
{ id: '...', exports: { ... }, loaded: true, ... }
优先会从缓存中取值,缓存中没有就直接从exports中取值。具体更多可以参考这篇文章require 源码解读
exports
另外,我们通常项目里可能会见到下面的代码
// A exports.a = 1; exports.b = 2; // B const a = require('./A.js'); console.log(a); // {a:1, b:2}
以上与下面等价
// A.js module.exports = { a: 1, b: 2 }; // B.js const a = require('./A.js'); console.log(a); // {a:1,b:2}
所以我们可以看出require实际上获取就是module.exports输出{}的一个值的拷贝。
{}
当exports.xxx时,实际上require获取的值结果依旧是module.exports值的拷贝。也就是说,在运行时,当使用exports.xx时实际上会中间悄悄转换成module.exports了。
exports.xxx
exports.xx
1、比较script``type中引入的三种模式defer、async、module的不同。
script``type
2、在module下,浏览器支持es模块,import方式加载模块
es
3、commonjs是在运行时同步加载的,并且导出的值是值拷贝,无法做到像esMoule一样做静态分析,而且esModule导出是值是值引用。
esMoule
4、esModule导出的对象,多个文件引用不会重复实例化,多个文件引入的对象是同一份对象。
5、commonjs加载原理,优先会从缓存中获取,然后再从loader加载模块
loader
6、本文示例code example
以下是笔者对于
模块
理解,希望在实际项目中能给你带来一点思考和帮助。正文开始...
关于
script
加载的那几个标识,defer
、async
、module
我们会发现执行顺序是
3,1,2
defer
与async
是异步的,而同步加载的 3,在页面中优先执行了。在执行顺序中,我们可以知道标识的defer
是等同步的3
与async的1
执行后才最后执行的。为了证明这点,我们在
1.js
中加入一段代码最后我们发现打印的顺序,
同步加载3
,(没有定时器的async)1
、defer加载2
、有定时器的async,异步加载不保证顺序1
因为
1.js
加入了一段定时器,在事件循环中,它是一段宏任务
,我们知道在浏览器处理事件循环中,同步任务>异步任务[微任务 promise>宏任务 setTimeout,事件等],在2.js
中用defer
标识了自己是异步,但是1.js
中有定时器,2.js
实际上是等了1.js
执行完了,再执行的。如果我在
2.js
中也加入定时器呢我们会发现结果依然是如此
不难发现
defer
中的定时器脚本虽然在async
标识的脚本前面,但是,注意两个定时器实际上是会有前后顺序的,跟脚本的顺序没有关系两个任务都是定时器,都是宏任务,在脚本的执行顺序中第一个定时器会先放到队列任务中,第二个定时器也会放到队列中,遵循先进先出,第一个宏任务(1.js 有定时器)先进队列,然后
2.js
定时器再进入队列,后面再执行。但是注意,定时器时间短的优先进入队列。
好了,搞明白
defer
与async
的区别了,总结一句,defer
会等其他脚本加载完了再执行,而async
是异步的,并不一定是在前面的就先执行。module
接下来我们来看看
module
module
是浏览器直接加载es6
,我们注意到加载module
中有哪些特性?在
js/4.js
中,我们可以看到可以用esModule
的方式输出在
script
用type="module"
后,内部顶层this
就不再是window
对象了,并且引入的外部路径不能省略后缀,且脚本自动采用严格模式。es6 的模块与 commonJS 的区别
通常我们在项目中都是
es6模块
,在nodejs
中大量模块代码都是采用commonjs
的方式,既然项目里都有用到,那么我们再次回顾下他们有什么区别参考module 加载实现中写道
1、
commonjs
输出的是一个值的拷贝,而es6模块
输出的是一个只读值的引用2、
commonjs
是在运行时加载,而es6模块
是在编译时输出接口3、
commonjs
的require()
是同步加载,而es6
的import xx from xxx
是异步加载,有一个独立的模块解析阶段另外我们还要知道
commonjs
的require
引入的是module.exports
出来的对象或者属性。而es6
模块不是对象,它对外暴露的接口是一种静态定义,在代码解析阶段就已经完成。举个例子,
commonjs
node 6.js
从打印里可以看出,一个原始的输出count
,外部调用countAge
并不会影响count
输出的值,但是在内部countAge
打印的仍是当前++后的值。如果是
es6模块
,我们可以发现在页面中引入,我们可以发现
我们发现
count
导出后的值是实时的改变了。因为它是一个值的引用。接下来有疑问,比如我有一个工具函数
这工具函数,在很多地方会有引用,比如
A,B,C...
等页面都会引入它,那么它会每次都会实例化Utils
?接下来我们实验下
我们会发现在
A
模块里调用utils.add()
后,在B
中打印utils.sum
是1
,那么证明B
引入的utils
与A
是一样的。如果我输出的仅仅是一个构造函数呢?看下面
页面同样引入
我们会发现
A
中new cutils().add()
在B
中new cutils().sum)
访问,依然是0
,所以当模块中导出的是一个构造函数时,每一个模块对应new 导出的构造函数
都是重新开辟了一个新的内存空间。因此可以得出结论,在
es6
模块中直接导出实例化对象的性能开销比直接导出构造函数更小些。CommonJS 模块的加载原理
我们初步了解下
CommonJS
的加载在执行
require
时,实际上内部会在内存中生成一个对象,require
是一个nodejs
环境提供的一个全局函数。优先会从缓存中取值,缓存中没有就直接从
exports
中取值。具体更多可以参考这篇文章require 源码解读另外,我们通常项目里可能会见到下面的代码
以上与下面等价
所以我们可以看出
require
实际上获取就是module.exports
输出{}
的一个值的拷贝。当
exports.xxx
时,实际上require
获取的值结果依旧是module.exports
值的拷贝。也就是说,在运行时,当使用exports.xx
时实际上会中间悄悄转换成module.exports
了。总结
1、比较
script``type
中引入的三种模式defer
、async
、module
的不同。2、在
module
下,浏览器支持es
模块,import
方式加载模块3、
commonjs
是在运行时同步加载的,并且导出的值是值拷贝,无法做到像esMoule
一样做静态分析,而且esModule
导出是值是值引用。4、
esModule
导出的对象,多个文件引用不会重复实例化,多个文件引入的对象是同一份对象。5、
commonjs
加载原理,优先会从缓存中获取,然后再从loader
加载模块6、本文示例code example