github 地址: 戳这里
模块化机制
CommonJS规范
应用于服务器端,同步加载
nodejs有一个模块加载系统
每个文件被视为一个独立的模块
module.exports属性可以被赋予一个新的值(例如函数或对象)
module.filename === __filename
module === require.main
require.resolve返回路径
module.exports
module.exports 对象是由模块系统创建, 可以导出一个对象或者函数。
module.exports === exports // true
也就是说exports和module.exports是指向同一内存的。而模块导出的时候实际是导出module.exports的值
缓存:
模块在第一次加载后会被缓存,是基于其文件名来缓存的
核心模块:
在nodejs/lib目录下,require(),会优先加载核心模块
循环:
当循环调用require()时,一个模块可能在为完成执行被返回
例子:
a.js
console.log('a 开始');
exports.done = false;
const b = require('./b.js');
console.log('在 a 中,b.done = %j', b.done);
exports.done = true;
console.log('a 结束');
b.js
console.log('b 开始');
exports.done = false;
const a = require('./a.js');
console.log('在 b 中,a.done = %j', a.done);
exports.done = true;
console.log('b 结束');
main.js
console.log('main 开始');
const a = require('./a.js');
const b = require('./b.js');
console.log('在 main 中,a.done=%j,b.done=%j', a.done, b.done);
结果:
main 开始
a 开始
b 开始
在 b 中,a.done = false
b 结束
在 a 中,b.done = true
a 结束
在 main 中,a.done=true,b.done=true
在a.js中require b.js, 之后又在b.js中require a.js,此时的
const a = require('./a.js');a是在a.js中require(b.js)语句之前导出的对象
官方解释:
当 main.js 加载 a.js 时,a.js 又加载 b.js。 此时,b.js 会尝试去加载 a.js。 为了防止无限的循环,会返回一个 a.js 的 exports 对象的 未完成的副本 给 b.js 模块。 然后 b.js 完成加载,并将 exports 对象提供给 a.js 模块。
文件模块:
如果没有找到确切的模块,文件拓展名会尝试加上.js、.json 或 .node
从 node_modules 目录加载
当require一个非核心模块,也没有'/' 、 '../' 或 './' 开头。则会从当前模块的父目录开始,尝试从它的 /node_modules 目录里加载模块。没有找到就继续父目录
AMD/CMD规范
用于浏览器端,由于要保证效率,所以需要异步加载
AMD规范实现: RequireJS
AMD规范的实现
特点:
-
预加载(加载某个模块前,其依赖模块需要先加载),在并行加载
js文件同时,还会解析执行该模块 -
当在
head中引用js时,是阻塞的,只也是为什么推荐放在body尾。但引用了RequireJS就能解决这个问题,引入RequireJS异步加载
判断是否支持AMD规范
typeof define === "function" && define.amd
起步:
- 在
index.html中引入requireJS
<script data-main="./js/app.js" src="./js/require.js"></script>
data-main指向config文件
// ./js/app.js
require.config({
baseUrl:'../',
paths: {
co: '../node_modules/co/index'
}
});
require(['./js/a.js'], function (data) {
console.log(data)
})
// a.js是定义的一个模块,用define定义
define(function () {
var result = {
a: 1,
b: 2
}
alert('haha')
return result
})
- 在引用模块时,可以
require(arr, callback),也可以用define(arr, callback),再次导出一个对象,供别的模块使用 - 对于第三方模块,需要用shim来加载非AMD模块
require.config({
shim: {
"underscore" : {
exports : "_";
}
}
})
原理
RequireJS使用head.appendChild()将每一个依赖加载为一个script标签。
RequireJS等待所有的依赖加载完毕,计算出模块定义函数正确调用顺序,然后依次调用它们。
如: <script type="text/javascript" charset="utf-8" async="" data-requirecontext="_" data-requiremodule="co" src="./js/../node_modules/co/index.js"></script>
标注async属性的脚本是异步脚本,即异步下载脚本时,不会阻塞文档解析,但是一旦下载完成后,立即执行,阻塞文档解析, 不能保证执行顺序。
优先级高于defer
因此需要确认脚本间没有依赖关系
优点
加载快速,尤其遇到多个大文件,因为并行解析,所以同一时间可以解析多个文件。
缺点
并行加载,异步处理,处理顺序不一定,可能会造成一些困扰,甚至为程序埋下大坑。
CMD规范实现:SeaJS
判断:
typeof define === "function" && define.cmd
特点:
- 就近加载
- 模块导出
既可以用exports对象来导出,也可以用return导出
模块定义:
define(id?, deps?, function(require, exports, module) {
// 模块代码
});
数组 deps 是模块依赖,带有id和deps的define不属于CMD规范
异步加载
require.async
使用范例:
// index.html
<script src="./js/sea.js"></script>
<script>
seajs.config({
base: "./",
alias: {
"c": "js/c.js",
"d": "js/d.js",
"jquery": "node_modules/co/index.js"
}
});
seajs.use('c');
</script>
// c.js
define(function (require, exports, module) {
require.async('d', function (obj) {
console.log(obj)
})
console.log('sucess')
// success
// {a:1,b:2}
})
// d.js
define((function (require, exports, module) {
return {
a:1,
b:2
}
}))
如何工作
SeaJS采用了和Node很相似的CMD规范
Seajs分为模块加载期和执行期,加载期需要将执行期所有用到的模块从服务端同步过来,在再执行期按照代码的逻辑顺序解析执行模块。
动态添加script标签, 在network中能看到js加载,但是Element中没有插入
优点
只有在使用的时候才会解析执行js文件,因此,每个JS文件的执行顺序在代码中是有体现的,是可控的
缺点
执行等待时间会叠加。因为每个文件执行时是同步执行(串行执行),因此时间是所有文件解析执行时间之和,尤其在文件较多较大时,这种缺点尤为明显。
ES6模块化
解决在node中import报错问题
npm install --save-dev babel-cli
npm babel-node your-script.js
或者
js文件命名为.mjs
node --experimental-modules your-script.mjs
模块加载
- 运行时加载
如CommonJS规范,在require的时候是全部加载的
- 编译时加载(静态加载)
如import,只加载导入的变量或方法,其它未导入的不加载
import
好处
- 不再需要UMD格式
- 不再需要对象最为命名空间,如Math
使用严格模式
use strict
- 变量必须声明后再使用
- 函数的参数不能有同名属性,否则报错
- 不能使用with语句
- 不能对只读属性赋值,否则报错
- 不能使用前缀 0
- 表示八进制数,否则报错
- 不能删除不可删除的属性,否则报错
- 不能删除变量delete prop,会报错,只能删除属性delete global[prop]
- eval不会在它的外层作用域引入变量
- eval和arguments不能被重新赋值
- arguments不会自动反映函数参数的变化
- 不能使用arguments.callee
- 不能使用arguments.caller
- 禁止this指向全局对象
- 不能使用fn.caller和fn.arguments获取函数调用的堆栈
- 增加了保留字(比如protected、static和interface)
export
export var a = 1;
export const b = function () {}
或者
var a = 1;
var b = 2;
var c = 3;
export { a, b ,c}
或者
function v1 () {}
export {
v1 as a
}
错误应用及修正
function f () {}
export f // 报错
export function f () {} // 正确
export default function() {} // 正确, 加载时整体加载
等同于
export {add as default};
注意
export 输出的是与值动态绑定的接口
// b.js
var a = 1
function add () {
a++
}
export {a, add}
// a.js
import {
add,
a
} from './b.js'
console.log(a);
add();
console.log(a)
而CommonJS中输出的是值的缓存
// b.js
var a = 1
function add () {
a++
}
module.exports = {a, add}
// a.js
var obj = require('./b.js')
console.log(obj.a); // 1
obj.add();
console.log(obj.a) // 1
export只能出现在模块顶层
import
加载的变量是const常量,是只读的。
import命令具有提升效果,意思加载的函数可以再之前执行
import是静态执行,不能使用表达式或者变量
import和export同时应用
export { foo, bar } from 'my_module';
foo, 和bar在当前模块不能用,相当于转发出去了
import和export命令只能在模块的顶层,不能在代码块之中
UMD中判断模块如何加载
- AMD或者CMD
typeof define === "function"
- CommonJS规范
typeof module === "object" && typeof module.exports === "object"
es6模块和CommonJS的区别
- ES6 模块之中,顶层的this指向undefined;CommonJS 模块的顶层this指向当前模块,这是两者的一个重大差异
- CommonJS是运行时加载,ES6模块是编译时加载
- ES6模块没有
arguments,require,module,exports,__filename,__dirname - 不允许import CommonJS模块,如
import {readFile} from 'fs' - 对于循环加载,CommonJS加载有缓存机制(输出值的拷贝),循环加载时是加载已经到处的变量,
// CommonJS
// a.js
exports.done = false;
console.log('begin'); // 在循环加载a.js的时候不再执行,只是取得已经导出的
var b = require('./b.js');
console.log('在 a.js 之中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 执行完毕');
// b.js
exports.done = false;
var a = require('./a.js');
console.log('在 b.js 之中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 执行完毕');
ES6模块循环加载时,那些变量不会被缓存,而是指向被加载模块的引用
// a.mjs
let done = 1
console.log('haha')
let foo = function foo () {
console.log('foo')
}
export {foo, done};
import './b.mjs'
// b.mjs
import {foo} from './a.mjs'
console.log('b====')
foo();
// b====
// ReferenceError: foo is not defined
解决:写成函数声明,函数具有提升作用
// a.js
let done = 1
console.log('haha')
function foo () {
console.log('foo')
}
export {foo, done};
import './b.mjs'
AMD和CMD的区别
AMD推崇依赖前置,在定义模块的时候就要声明其依赖的模块 ,CMD推崇就近依赖,只有在用到某个模块的时候再去require- CMD加载完某个依赖模块后并不执行,只是下载而已,在所有依赖模块加载完成后进入主逻辑,遇到
require语句的时候才执行对应的模块,这样模块的执行顺序和书写顺序是完全一致的。AMD在加载模块完成后就会执行改模块,所有模块都加载执行完后会进入require的回调函数, 但是主逻辑一定在所有依赖加载完成后才执行。
也就是说,AMD先加载,先执行,CMD先加载,延迟执行