基本概念
js语言本身没有依赖管理。 随着CommonJs社区的发展以及Nodejs的出现,形成了CommonJs标准;而后又出现适用于浏览器的AMD标准和CMD标准。 为了兼容两种标准,我们经常会在js库的开头或者结尾看到如下代码:
if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) {
// AMD. Register as an anonymous module.
define(function() {
return moduleName;
});
} else if (typeof module !== 'undefined' && module.exports) {
module.exports = moduleName;
} else {
window.moduleName = moduleName;
}
两种标准的差异可以概括为:AMD是依赖前置,CMD是依赖就近。 采用AMD,需要把可能依赖到的文件全部放到依赖项中提前加载执行,不论后面的程序用到用不到。 采用CMD,可以在使用到模块时才把它require进来。 两者都是提前异步加载js文件,只是AMD在加载完后立即执行,而CMD是在require的时候才执行。如下是AMD和CMD的典型写法:
// AMD
define('a', ['b'], function(){
// todo
};
// CMD
define(function(require, exports, module) {
// todo
var b = require('b');
// todo
})
本文以 seajs 和 nej 的依赖管理作为CMD和AMD的实现库进行分析,探索两者实现的差异。
AMD
如下是nej中的模块依赖写法:
var f=function(){
window.moduleName = "b";
};
define('b', ['a'], f);
基本流程
依赖管理项,也就是每个js文件分为三个状态:初始状态、加载执行状态、已执行f函数状态。函数的加载是采用 <script>
标签设置src进行加载,并通过监听 script 的 onload 事件判断加载完成。代码加载完成后会直接执行,代码运行时会执行define函数,触发依赖收集。
所有的依赖项都会放到一个数组中,我们暂且称之为依赖数组。在新的依赖进行收集或者一个js文件加载完成时,会判断下当前队列的执行情况。 其执行逻辑如下:
var _doCheckLoading = function(){
if (!__queue.length) return;
for(var i=__queue.length-1,_item;i>=0;){
_item = __queue[i];
if (__cache[_item.n]!==2&&
!_isListLoaded(_item.d)){
i--; continue;
}
__queue.splice(i,1);
if (__cache[_item.n]!==2){
_item.f();
__cache[_item.n] = 2;
console.log('do '+_item.n)
}
i = __queue.length-1;
}
// check circular reference
if (__queue.length>0&&_isFinishLoaded()){
var _item = _doFindCircularRef()||__queue.pop();
_item.f();
__cache[_item.n] = 2;
console.log('do+ '+_item.n)
_doCheckLoading();
}
};
从后往前遍历依赖数组,如果当前项的依赖都已经执行完成,那么执行当前项的f函数,并把当前项从数组中移除,防止重复执行;否则继续等待新的依赖或者等新的代码加载完成后再判断。
循环依赖
如果两个文件相互依赖,那么按照上述逻辑会陷入死循环,这就是循环依赖。循环依赖是每个依赖管理都要面临的问题;如下面场景,a.js
和 b.js
就形成了循环依赖:
// a.js
var f=function(){
window.basename = "a" + window.basename;
};
define('a.js', ['b.js'], f);
// b.js
var f=function(){
window.basename = "b";
};
define('b.js', ['a.js'], f);
对于这种情况,框架会在所有代码都加载完成时检测依赖数组的长度。如果依赖数组的长度不为零,那么意味着有循环依赖的情况。 当有循环依赖时,框架会从依赖数组的最后一项(或者从第一项)开始往前(或往后)遍历;它借用数组缓存当前项,同时判断当前项的依赖项是否存在缓存数组中,如果存在那么执行依赖项,然后再判断当前的循环依赖状态是不是解除。
var _doFindCircularRef = (function(){
var _result;
var _index = function(_array,_name){
for(var i=_array.length-1;i>=0;i--)
if (_array[i].n==_name)
return i;
return -1;
};
var _loop = function(_item){
if (!_item) return;
var i = _index(_result,_item.n);
if (i>=0) return _item;
_result.push(_item);
var _deps = _item.d;
if (!_deps||!_deps.length) return;
for(var i=0,l=_deps.length,_citm;i<l;i++){
_citm = _loop(__queue[_index(__queue,_deps[i])]);
if (!!_citm) return _citm;
}
};
return function(){
_result = [];
return _loop(__queue[__queue.length-1]);
};
})();
上述例子中 a.js
和 b.js
, 如果从后往前解除依赖,那么 b.js
会先执行;反之 a.js
会先执行; 两种执行的结果是不一致的。
CMD
下面是seajs中定义一个module:
define(function(require, exports, module) {
var Spinning = require('./spinning');
var s = new Spinning('#container');
s.render();
});
基本流程
seajs中对模块分为7种状态:初始状态(0)、请求发起还没完成的获取态(1)、模块加载完成(2)、依赖正在加载(3)、依赖加载完成可以执行(4)、模块正在执行(5)、模块执行完成(6)。
一个Module对象如下:
function Module(uri, deps) {
// 当前模块的地址
this.uri = uri
// 模块的依赖项
this.dependencies = deps || []
this.exports = null
this.status = 0 // 初始状态为0
// Who depends on me
this._waitings = {}
// The number of unloaded dependencies
this._remain = 0
}
我们假设主函数通过请求被加载进来。当它加载后会直接执行,由于其被define包裹,因此会进入define函数。define函数的主要功能是收集依赖:
Module.define = function (id, deps, factory) {
...
// Parse dependencies according to the module factory code
if (!isArray(deps) && isFunction(factory)) {
deps = parseDependencies(factory.toString())
}
var meta = {
id: id,
uri: Module.resolve(id),
deps: deps,
factory: factory
}
...
// 保留信息 供onload函数调用
meta.uri ? Module.save(meta.uri, meta) :
// Save information for "saving" work in the script onload event
anonymousMeta = meta
}
函数执行完成后,会进入 onload 回调逻辑。 回调函数会触发模块的 load 函数,load函数会判断依赖项的状态:如果所有的依赖项的状态都大于等于4,那么可以执行当前模块的 onload 逻辑;如果依赖项的状态是0,那么发送请求进行获取; 如果是已经加载完成,那么触发其 load 逻辑:
Module.prototype.load = function() {
var mod = this
// If the module is being loaded, just wait it onload call
if (mod.status >= STATUS.LOADING) {
return
}
mod.status = STATUS.LOADING
// Emit `load` event for plugins such as combo plugin
var uris = mod.resolve()
emit("load", uris, mod)
var len = mod._remain = uris.length
var m
// Initialize modules and register waitings
for (var i = 0; i < len; i++) {
m = Module.get(uris[i])
if (m.status < STATUS.LOADED) {
// Maybe duplicate: When module has dupliate dependency, it should be it's count, not 1
m._waitings[mod.uri] = (m._waitings[mod.uri] || 0) + 1
}
else {
mod._remain--
}
}
// 如果所有的依赖项已经加载并要执行或者已经执行,那么触发onload 进行执行
if (mod._remain === 0) {
mod.onload()
return
}
// Begin parallel loading
var requestCache = {}
// 获取模块
for (i = 0; i < len; i++) {
m = cachedMods[uris[i]]
if (m.status < STATUS.FETCHING) {
m.fetch(requestCache)
}
else if (m.status === STATUS.SAVED) {
m.load()
}
}
// Send all requests at last to avoid cache bug in IE6-9. Issues#808
for (var requestUri in requestCache) {
if (requestCache.hasOwnProperty(requestUri)) {
requestCache[requestUri]()
}
}
}
模块的onload函数会执行模块内部代码,同时更新依赖情况:
Module.prototype.onload = function() {
var mod = this
mod.status = STATUS.LOADED
if (mod.callback) { // 调用exec函数
mod.callback()
}
// Notify waiting modules to fire onload
var waitings = mod._waitings
var uri, m
for (uri in waitings) {
if (waitings.hasOwnProperty(uri)) {
m = cachedMods[uri]
m._remain -= waitings[uri]
if (m._remain === 0) { // 依赖当前模块的模块更新依赖计数 如果所有的依赖都加载完,那么执行onload
m.onload()
}
}
}
}
exec的执行逻辑和Node中的类似,外部传入require、module等参数,然后把module.exports对象作为返回值:
Module.prototype.exec = function () {
var mod = this
// 下面是模块加载逻辑 把require、export作为函数参数传入执行
function require(id) {
return Module.get(require.resolve(id)).exec()
}
require.resolve = function(id) {
return Module.resolve(id, uri)
}
require.async = function(ids, callback) {
Module.use(ids, callback, uri + "_async_" + cid())
return require
}
// Exec factory
var factory = mod.factory
var exports = isFunction(factory) ?
factory(require, mod.exports = {}, mod) :
factory
if (exports === undefined) {
exports = mod.exports
}
return exports
}
循环依赖
seajs没有处理循环依赖的情况,如下情况:
// a.js
define(function(require, exports, module) {
var b = require('./b');
var a = {};
a.name = "a" + b.name;
module.exports = a;
});
// b.js
define(function(require, exports, module) {
var a = require('./a');
var b = {};
b.name = "b";
module.exports = b;
});
a.js
加载后会加载 b.js
, b.js
加载后判断 a.js
的状态,但是由于 a.js
的状态为loading,因此不能触发 load 函数中的进行一步逻辑。
CommonJs
CommonJs的加载机制可以参考Nodejs,它的代码加载是同步的,因此相对来说要简单一些。 函数加载后会包裹上如下代码:
'(function (exports, require, module, __filename, __dirname) { ',
'\n});'
执行时会传入特定的参数。具体可以参考模块加载。
循环依赖
在 Module._load
内部方法里 Node.js 在加载模块之前,首先就会把传模块内的 module 对象的引用给缓存起来(此时它的 exports 属性还是一个空对象),然后执行模块内代码,在这个过程中渐渐为 module.exports
对象附上该有的属性。当出现循环依赖的时候,仅仅只会让循环依赖点取到中间值,而不会让 require 死循环卡住:
// a.js
'use strict'
console.log('a starting')
exports.done = false
var b = require('./b')
console.log(`in a, b.done=${b.done}`)
exports.done = true
console.log('a done')
// b.js
'use strict'
console.log('b start')
exports.done = false
let a = require('./a')
console.log(`in b, a.done=${a.done}`)
exports.done = true
console.log('b done')
// main.js
'use strict'
console.log('main start')
let a = require('./a')
let b = require('./b')
console.log(`in main, a.done=${a.done}, b.done=${b.done}`)
结果是:
main start
a starting
b start
in b, a.done=false => 循环依赖点取到了中间值
b done
in a, b.done=true
a done
in main, a.done=true, b.done=true