Javascript模块化编程,已经成为一个迫切的需求。理想状况下,开发者只须要实现核心的业务逻辑,其余均可以加载别人已经写好的模块。javascript
Javascript社区作了不少努力,在现有的运行环境中,实现”模块”的效果。css
CommonJS定义的模块分为: 模块引用(require) 模块输出(exports) 模块标识(module)html
CommonJS Modules有1.0、1.一、1.1.1三个版本:前端
Node.js、SproutCore实现了 Modules 1.0java
SeaJS、AvocadoDB、CouchDB等实现了Modules 1.1.1node
SeaJS、FlyScript实现了Modules/Wrappingsjquery
这里的CommonJS规范指的是CommonJS Modules/1.0规范。webpack
CommonJS是一个更偏向于服务器端的规范。NodeJS采用了这个规范。CommonJS的一个模块就是一个脚本文件。require命令第一次加载该脚本时就会执行整个脚本,而后在内存中生成一个对象。es6
{ id: '...', exports: { ... }, loaded: true, ... }
id是模块名,exports是该模块导出的接口,loaded表示模块是否加载完毕。此外还有不少属性,这里省略了。web
之后须要用到这个模块时,就会到exports属性上取值。即便再次执行require命令,也不会再次执行该模块,而是到缓存中取值。
// math.js exports.add = function(a, b) { return a + b; }
var math = require('math'); math.add(2, 3); // 512
因为CommonJS是同步加载模块,这对于服务器端不是一个问题,由于全部的模块都放在本地硬盘。等待模块时间就是硬盘读取文件时间,很小。可是,对于浏览器而言,它须要从服务器加载模块,涉及到网速,代理等缘由,一旦等待时间过长,浏览器处于”假死”状态。
因此在浏览器端,不适合于CommonJS规范。因此在浏览器端又出现了一个规范—AMD(AMD是RequireJs在推广过程当中对模块定义的规范化产出)。
CommonJS解决了模块化的问题,但这种同步加载方式并不适合于浏览器端。
AMD是”Asynchronous Module Definition”的缩写,即”异步模块定义”。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。
这里异步指的是不堵塞浏览器其余任务(dom构建,css渲染等),而加载内部是同步的(加载完模块后当即执行回调)。
AMD也采用require命令加载模块,可是不一样于CommonJS,它要求两个参数:
require([module], callback);1
第一个参数[module],是一个数组,里面的成员是要加载的模块,callback是加载完成后的回调函数。若是将上述的代码改为AMD方式:
require(['math'], function(math) { math.add(2, 3); })
其中,回调函数中参数对应数组中的成员(模块)。
requireJS加载模块,采用的是AMD规范。也就是说,模块必须按照AMD规定的方式来写。
具体来讲,就是模块书写必须使用特定的define()函数来定义。若是一个模块不依赖其余模块,那么能够直接写在define()函数之中。
define(id?, dependencies?, factory);12
id:模块的名字,若是没有提供该参数,模块的名字应该默认为模块加载器请求的指定脚本的名字;
dependencies:模块的依赖,已被模块定义的模块标识的数组字面量。依赖参数是可选的,若是忽略此参数,它应该默认为 ["require", "exports", "module"]
。然而,若是工厂方法的长度属性小于3,加载器会选择以函数的长度属性指定的参数个数调用工厂方法。
factory:模块的工厂函数,模块初始化要执行的函数或对象。若是为函数,它应该只被执行一次。若是是对象,此对象应该为模块的输出值。
假定如今有一个math.js文件,定义了一个math模块。那么,math.js书写方式以下:
// math.js define(function() { var add = function(x, y) { return x + y; } return { add: add } })
加载方法以下:
// main.js require(['math'], function(math) { alert(math.add(1, 1)); })
若是math模块还依赖其余模块,写法以下:
// math.js define(['dependenceModule'], function(dependenceModule) { // ... })
当require()函数加载math模块的时候,就会先加载dependenceModule模块。当有多个依赖时,就将全部的依赖都写在define()函数第一个参数数组中,因此说AMD是依赖前置的。这不一样于CMD规范,它是依赖就近的。
CMD推崇依赖就近,延迟执行。能够把你的依赖写进代码的任意一行,以下:
define(factory)
factory
为函数时,表示是模块的构造方法。执行该构造方法,能够获得模块向外提供的接口。factory 方法在执行时,默认会传入三个参数:require、exports 和 module.
// CMD define(function(require, exports, module) { var a = require('./a'); a.doSomething(); var b = require('./b'); b.doSomething(); })
若是使用AMD写法,以下:
// AMDdefine(['a', 'b'], function(a, b) { a.doSomething(); b.doSomething(); })
这个规范其实是为了Seajs的推广而后搞出来的。那么看看SeaJS是怎么回事儿吧,基本就是知道这个规范了。
一样Seajs也是预加载依赖js跟AMD的规范在预加载这一点上是相同的,明显不一样的地方是调用,和声明依赖的地方。AMD和CMD都是用difine和require,可是CMD标准倾向于在使用过程当中提出依赖,就是无论代码写到哪忽然发现须要依赖另外一个模块,那就在当前代码用require引入就能够了,规范会帮你搞定预加载,你随便写就能够了。可是AMD标准让你必须提早在头部依赖参数部分写好(没有写好? 倒回去写好咯)。这就是最明显的区别。
sea.js经过sea.use()
来加载模块。
seajs.use(id, callback?)
es6模块特性,推荐参看阮一峰老师的:ECMAScript 6 入门 - Module 的语法
提及 ES6 模块特性,那么就先说说 ES6 模块跟 CommonJS 模块的不一样之处。
ES6 模块输出的是值的引用,输出接口动态绑定,而 CommonJS 输出的是值的拷贝
ES6 模块编译时执行,而 CommonJS 模块老是在运行时加载
CommonJS 模块输出的是值的拷贝(原始值的拷贝),也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
// a.js var b = require('./b'); console.log(b.foo); setTimeout(() => { console.log(b.foo); console.log(require('./b').foo); }, 1000); // b.js let foo = 1; setTimeout(() => { foo = 2; }, 500); module.exports = { foo: foo, }; // 执行:node a.js // 执行结果: // 1 // 1 // 1
上面代码说明,b 模块加载之后,它的内部 foo 变化就影响不到输出的 exports.foo 了。这是由于 foo 是一个原始类型的值,会被缓存。因此若是你想要在 CommonJS 中动态获取模块中的值,那么就须要借助于函数延时执行的特性。
// a.js var b = require('./b'); console.log(b.foo()); setTimeout(() => { console.log(b.foo()); console.log(require('./b').foo()); }, 1000); // b.js let foo = 1; setTimeout(() => { foo = 2; }, 500); module.exports = { foo: () => { return foo; }, }; // 执行:node a.js // 执行结果: // 1 // 2 // 2
因此咱们能够总结一下:
CommonJS 模块重复引入的模块并不会重复执行,再次获取模块直接得到暴露的 module.exports 对象
若是你要到处获取到模块内的最新值的话,也能够你每次更新数据的时候每次都要去更新 module.exports 上的值
若是你暴露的 module.exports 的属性是个对象,那就不存在这个问题了
因此若是你要到处获取到模块内的最新值的话,也能够你每次更新数据的时候每次都要去更新 module.exports 上的值,好比:
// a.js var b = require('./b'); console.log(b.foo); setTimeout(() => { console.log(b.foo); console.log(require('./b').foo); }, 1000); // b.js module.exports.foo = 1; // 同 exports.foo = 1 setTimeout(() => { module.exports.foo = 2; }, 500); // 执行:node a.js // 执行结果: // 1 // 2 // 2
然而在 ES6 模块中就再也不是生成输出对象的拷贝,而是动态关联模块中的值。
关于第二点,ES6 模块编译时执行会致使有如下两个特色:
import 命令会被 JavaScript 引擎静态分析,优先于模块内的其余内容执行。
export 命令会有变量声明提早的效果。
import 优先执行:
从第一条来看,在文件中的任何位置引入 import 模块都会被提早到文件顶部。
// a.js console.log('a.js') import { foo } from './b'; // b.js export let foo = 1; console.log('b.js 先执行'); // 执行结果: // b.js 先执行 // a.js
从执行结果咱们能够很直观地看出,虽然 a 模块中 import 引入晚于 console.log('a'),可是它被 JS 引擎经过静态分析,提到模块执行的最前面,优于模块中的其余部分的执行。
因为 import 是静态执行,因此 import 具备提高效果即 import 命令在模块中的位置并不影响程序的输出。
/ a.js import { foo } from './b'; console.log('a.js'); export const bar = 1; export const bar2 = () => { console.log('bar2'); } export function bar3() { console.log('bar3'); } // b.js export let foo = 1; import * as a from './a'; console.log(a); // 执行结果: // { bar: undefined, bar2: undefined, bar3: [Function: bar3] } // a.js
从上面的例子能够很直观地看出,a 模块引用了 b 模块,b 模块也引用了 a 模块,export 声明的变量也是优于模块其它内容的执行的,可是具体对变量赋值须要等到执行到相应代码的时候。(固然函数声明和表达式声明不同,这一点跟 JS 函数性质同样,这里就不过多解释)
好了,讲完了 ES6 模块和 CommonJS 模块的不一样点以后,接下来就讲讲相同点:
模块不会重复执行
这个很好理解,不管是 ES6 模块仍是 CommonJS 模块,当你重复引入某个相同的模块时,模块只会执行一次。
CommonJS 模块循环依赖
// a.js console.log('a starting'); exports.done = false; const b = require('./b'); console.log('in a, b.done =', b.done); exports.done = true; console.log('a done'); // b.js console.log('b starting'); exports.done = false; const a = require('./a'); console.log('in b, a.done =', a.done); exports.done = true; console.log('b done'); // node a.js // 执行结果: // a starting // b starting // in b, a.done = false // b done // in a, b.done = true // a done
结合以前讲的特性很好理解,当你从 b 中想引入 a 模块的时候,由于 node 以前已经加载过 a 模块了,因此它不会再去重复执行 a 模块,而是直接去生成当前 a 模块吐出的 module.exports 对象,由于 a 模块引入 b 模块先于给 done 从新赋值,因此当前 a 模块中输出的 module.exports 中 done 的值仍为 false。而当 a 模块中输出 b 模块的 done 值的时候 b 模块已经执行完毕,因此 b 模块中的 done 值为 true。
从上面的执行过程当中,咱们能够看到,在 CommonJS 规范中,当遇到 require() 语句时,会执行 require 模块中的代码,并缓存执行的结果,当下次再次加载时不会重复执行,而是直接取缓存的结果。正由于此,出现循环依赖时才不会出现无限循环调用的状况。虽然这种模块加载机制能够避免出现循环依赖时报错的状况,但稍不注意就极可能使得代码并非像咱们想象的那样去执行。所以在写代码时仍是须要仔细的规划,以保证循环模块的依赖能正确工做。
因此有什么办法能够出现循环依赖的时候避免本身出现混乱呢?一种解决方式即是将每一个模块先写 exports 语法,再写 requre 语句,利用 CommonJS 的缓存机制,在 require() 其余模块以前先把自身要导出的内容导出,这样就能保证其余模块在使用时能够取到正确的值。好比:
// a.js exports.done = true; let b = require('./b'); console.log(b.done) // b.js exports.done = true; let a = require('./a'); console.log(a.done)
这种写法简单明了,缺点是要改变每一个模块的写法,并且大部分同窗都习惯了在文件开头先写 require 语句。
跟 CommonJS 模块同样,ES6 不会再去执行重复加载的模块,又因为 ES6 动态输出绑定的特性,能保证 ES6 在任什么时候候都能获取其它模块当前的最新值。
// a.js console.log('a starting') import {foo} from './b'; console.log('in b, foo:', foo); export const bar = 2; console.log('a done'); // b.js console.log('b starting'); import {bar} from './a'; export const foo = 'foo'; console.log('in a, bar:', bar); setTimeout(() => { console.log('in a, setTimeout bar:', bar); }) console.log('b done'); // babel-node a.js // 执行结果: // b starting // in a, bar: undefined // b done // a starting // in b, foo: foo // a done // in a, setTimeout bar: 2
ES6 模块在编译时就会静态分析,优先于模块内的其余内容执行,因此致使了咱们没法写出像下面这样的代码:
if(some condition) { import a from './a'; }else { import b from './b'; } // or import a from (str + 'b');
由于编译时静态分析,致使了咱们没法在条件语句或者拼接字符串模块,由于这些都是须要在运行时才能肯定的结果在 ES6 模块是不被容许的,因此 动态引入 import() 应运而生。
import() 容许你在运行时动态地引入 ES6 模块,想到这,你可能也想起了 require.ensure 这个语法,可是它们的用途却大相径庭的。
require.ensure 的出现是 webpack 的产物,它是由于浏览器须要一种异步的机制能够用来异步加载模块,从而减小初始的加载文件的体积,因此若是在服务端的话 require.ensure 就无用武之地了,由于服务端不存在异步加载模块的状况,模块同步进行加载就能够知足使用场景了。 CommonJS 模块能够在运行时确认模块加载。
而 import() 则不一样,它主要是为了解决 ES6 模块没法在运行时肯定模块的引用关系,因此须要引入 import()
咱们先来看下它的用法:
动态的 import() 提供一个基于 Promise 的 API
动态的import() 能够在脚本的任何地方使用
import() 接受字符串文字,你能够根据你的须要构造说明符
举个简单的使用例子:
// a.js const str = './b'; const flag = true; if(flag) { import('./b').then(({foo}) => { console.log(foo); }) } import(str).then(({foo}) => { console.log(foo); }) // b.js export const foo = 'foo'; // babel-node a.js // 执行结果 // foo // foo
固然,若是在浏览器端的 import() 的用途就会变得更普遍,好比 按需异步加载模块,那么就和 require.ensure 功能相似了。
由于是基于 Promise 的,因此若是你想要同时加载多个模块的话,能够是 Promise.all 进行并行异步加载。
Promise.all([ import('./a.js'), import('./b.js'), import('./c.js'), ]).then(([a, {default: b}, {c}]) => { console.log('a.js is loaded dynamically'); console.log('b.js is loaded dynamically'); console.log('c.js is loaded dynamically'); });
还有 Promise.race 方法,它检查哪一个 Promise 被首先 resolved 或 reject。咱们可使用import()来检查哪一个CDN速度更快:
const CDNs = [ { name: 'jQuery.com', url: 'https://code.jquery.com/jquery-3.1.1.min.js' }, { name: 'googleapis.com', url: 'https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js' } ]; console.log(`------`); console.log(`jQuery is: ${window.jQuery}`); Promise.race([ import(CDNs[0].url).then(()=>console.log(CDNs[0].name, 'loaded')), import(CDNs[1].url).then(()=>console.log(CDNs[1].name, 'loaded')) ]).then(()=> { console.log(`jQuery version: ${window.jQuery.fn.jquery}`); });
固然,若是你以为这样写还不够优雅,也能够结合 async/await 语法糖来使用。
async function main() { const myModule = await import('./myModule.js'); const {export1, export2} = await import('./myModule.js'); const [module1, module2, module3] = await Promise.all([ import('./module1.js'), import('./module2.js'), import('./module3.js'), ]); }
动态 import() 为咱们提供了以异步方式使用 ES 模块的额外功能。 根据咱们的需求动态或有条件地加载它们,这使咱们可以更快,更好地建立更多优点应用程序。
一个模块就是一个独立的文件。该文件内部的全部变量,外部没法获取。若是但愿外部文件可以读取该模块的变量,就须要在这个模块内使用export关键字导出变量。如:
// profile.jsexport var a = 1;export var b = 2;export var c = 3;1234
下面的写法是等价的,这种方式更加清晰(在底部一眼能看出导出了哪些变量):
var a = 1;var b = 2;var c = 3; export {a, b, c}1234
import命令能够导入其余模块经过export导出的部分。
var a = 1;var b = 2;var c = 3; export {a, b, c} //main.js import {a, b, c} from './abc'; console.log(a, b, c);
若是想为导入的变量从新取一个名字,使用as关键字(也能够在导出中使用)。
import {a as aa, b, c}; console.log(aa, b, c)12
若是想在一个模块中先输入后输出一个模块,import语句能够和export语句写在一块儿。
import {a, b, c} form './abc';export {a, b, c}// 使用连写, 可读性很差,不建议export {a, b, c} from './abc';12345
使用*关键字。
import * from as abc form './abc';
在export输出内容时,若是同时输出多个变量,须要使用大括号{}
,同时导入也须要大括号。使用export defalut
输出时,不须要大括号,而输入(import)export default
输出的变量时,不须要大括号。
// abc.jsvar a = 1, b = 2, c = 3;export {a, b};export default c;1234
import {a, b} from './abc'; import c from './abc'; // 不须要大括号console.log(a, b, c) // 1 2 3123
本质上,export default
输出的是一个叫作default的变量或方法,输入这个default变量时不须要大括号。
// abc.js export {a as default}; // main.js import a from './abc'; // 这样也是能够的 import {default as aa} from './abc'; // 这样也是能够的 console.log(aa);123456789
就到这里了吧。关于循环加载(模块相互依赖)没写,CommonJS和ES6处理方式不同。
参考文章: