美文网首页
依赖管理

依赖管理

作者: 狐尼克朱迪 | 来源:发表于2016-12-06 14:51 被阅读0次

基本概念

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

参考

模块依赖管理工具对比

相关文章

  • 手写SpringMVC:项目框架搭建(骨架)

    项目框架搭建(骨架) 依赖管理工具Gradle 优点: 自动化管理依赖 解决依赖冲突 不仅仅管理依赖,更管理项目 ...

  • PHP 资源大全

    依赖管理 依赖和包管理库 Composer/Packagist:一个包和依赖管理器Composer Install...

  • [Gradle中文教程系列]-跟我学Gradle-5.0:依赖-

    什么是依赖管理 通常而言,依赖管理包括两部分,对依赖的管理以及发布物的管理;依赖是指构建项目所需的构件(jar包等...

  • 开源库 - 包管理(依赖库管理)

    Swift依赖库管理 Swift依赖库管理有三种方式: CocoaPods (中心化的依赖管理器,CocoaPod...

  • 依赖管理

    依赖管理 一个Java项目总会依赖于第三方,要么是一个第三方类库,比如Apache commons;要么是你自己开...

  • 依赖管理

    基本概念 js语言本身没有依赖管理。 随着CommonJs社区的发展以及Nodejs的出现,形成了CommonJs...

  • 依赖管理

    不等心情,一点一点强行开始 上面博文《解决“单接口,多类”架构设计的回调问题》的源码已经上传Github:http...

  • 依赖管理

    在Unix的设计哲学中,do one thing 被广大软件设计开发人员奉为圭臬,很多底层的基础代码只需要做成库,...

  • 依赖管理

    Maven Gradle 比较maven VS gradle 假如还没有依赖管理工具,我们要自己设计一个,如何入手...

  • 依赖管理

    项目中的依赖是我们不希望发生的事情却又无法避免的,而依赖的增多会导致沟通的增加、任务安排的复杂度增高。从而影响价值...

网友评论

      本文标题:依赖管理

      本文链接:https://www.haomeiwen.com/subject/vqtkmttx.html