依赖管理

基本概念

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
})

本文以 seajsnej依赖管理作为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.jsb.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.jsb.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.jsb.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

参考

模块依赖管理工具对比

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容