Angular 2 DI - IoC & DI - 1

IoC 是什么

Ioc - Inversion of Control , 即"控制反转"。在开发中, IoC 意味着你设计好的对象交给容器控制,而不是使用传统的方式,在对象内部直接控制。  编程

如何理解好 IoC 呢?理解好 IoC的关键是要明确"谁控制谁,控制什么,为什么是反转(有反转就应该有正转),哪些方面反转了",咱们来深刻分析一下。  数组

  • 谁控制谁,控制什么: 在传统的程序设计中,咱们直接在对象内部经过 new 的方式建立对象,是程序主动建立依赖对象;而 IoC 是有专门一个容器来建立这些对象,即由 IoC 容器控制对象的建立;谁控制谁?固然是 IoC 容器控制了对象;控制什么?主要是控制外部资源获取。浏览器

  • 为什么是反转了,哪些方面反转了: 有反转就有正转,传统应用程序是由咱们本身在对象中主动控制去获取依赖对象,也就是正转;而反转则是由容器来帮忙建立及注入依赖对象;为什么是反转?由于由容器帮咱们查找及注入依赖对象,对象只是被动的接受依赖对象,因此是反转了;哪些方面反转了?依赖对象的获取被反转了。缓存

IoC 能作什么

Ioc 不是一种技术,只是一种思想,一个重要的面向对象编程法则,它能指导咱们如何设计松耦合、更优良的系统。传统应用程序都是由咱们在类内部主动建立依赖对象,从而致使类与类之间高耦合,难于测试;有了 IoC 容器后,把建立和查找依赖对象的控制权交给了容器,由容器注入组合对象,因此对象之间是松散耦合,这样也便于测试,利于功能复用,更重要的是使得程序的整个体系结构变得很是灵活。  angular2

其实 IoC 对编程带来的最大改变不是从代码上,而是思想上,发生了"主从换位"的变化。应用程序原本是老大,要获取什么资源都是主动出击,但在 IoC思想中,应用程序就变成被动了,被动的等待 IoC 容器来建立并注入它所需的资源了。    框架

IoC 和 DI

DI - Dependency Injection,即"依赖注入":组件之间的依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并不是为软件系统带来更多功能,而是为了提高组件重用的频率,并为系统搭建一个灵活、可扩展的平台。经过依赖注入机制,咱们只须要经过简单的配置,而无需任何代码就可指定目标须要的资源,完成自身的业务逻辑,而不须要关心具体的资源来自何处,由谁实现。  异步

理解 DI 的关键是:"谁依赖了谁,为何须要依赖,谁注入了谁,注入了什么",那咱们来深刻分析一下:  ide

  • 谁依赖了谁:固然是应用程序依赖 IoC 容器函数

  • 为何须要依赖:应用程序须要 IoC 容器来提供对象须要的外部资源学习

  • 谁注入谁:很明显是 IoC 容器注入应用程序依赖的对象

  • 注入了什么:注入某个对象所需的外部资源(包括对象、资源、常量数据)

IoC 和 DI 有什么关系?其实它们是同一个概念的不一样角度描述,因为控制反转的概念比较含糊(可能只是理解为容器控制对象这一个层面,很难让人想到谁来维护依赖关系),因此 2004 年大师级人物 Martin Fowler 又给出了一个新的名字:"依赖注入",相对 IoC 而言,"依赖注入" 明确描述了被注入对象依赖 IoC 容器配置依赖对象。  

总的来讲, 控制反转(Inversion of Control)是说建立对象的控制权发生转移,之前建立对象的主动权和建立时机由应用程序把控,而如今这种权利转交给 IoC 容器,它就是一个专门用来建立对象的工厂,你须要什么对象,它就给你什么对象。有了 IoC 容器,依赖关系就改变了,原先的依赖关系就没了,它们都依赖 IoC容器了,经过 IoC 容器来创建它们之间的关系。  

DI 在 angular1 中的应用  

angular1 中声明依赖项的方式有3种,分为以下:  

// 方式一: 使用 $inject annotation 方式
var fn = function (a, b) {};
fn.$inject = ['a', 'b'];

// 方式二: 使用 array-style annotations 方式
var fn = ['a', 'b', function (a, b) {}];

// 方式三: 使用隐式声明方式 
var fn = function (a, b) {}; // 不推荐

为了支持以上多种声明方式,angular1 内部使用 annotate 函数来解析依赖项,该函数的实现以下:

var FN_ARGS = /^[^\(]*\(\s*([^\)]*)\)/m; // 匹配参数列表
var FN_ARG_SPLIT = /,/; // 参数分隔符
var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/; // 匹配参数项
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; // 去除 // 或 /**/注释

function extractArgs(fn) { // 抽取参数列表
  var fnText = fn.toString().replace(STRIP_COMMENTS, ''), // 去除注释
      args = fnText.match(ARROW_ARG) || fnText.match(FN_ARGS);
  return args;
}

function anonFn(fn) {
  var args = extractArgs(fn);
  if (args) {
    return 'function(' + (args[1] || '').replace(/[\s\r\n]+/, ' ') + ')';
  }
  return 'fn';
}

function annotate(fn, strictDi, name) {
  var $inject,
      argDecl,
      last;
      
  if (typeof fn === 'function') {
    if (!($inject = fn.$inject)) { // 判断是否使用$inject方式声明依赖项
      $inject = [];
      if (fn.length) {
        if (strictDi) { // 使用严格注入模式,即不能使用隐式声明方式
         // 函数名非字符串或为falsy值(如undefined、null),未设置时默认值为undefined 
          if (!isString(name) || !name) { 
            name = fn.name || anonFn(fn);
          }
          throw $injectorMinErr('strictdi',
            '{0} is not using explicit annotation and cannot be 
                 invoked in strict mode', name);
        }
        argDecl = extractArgs(fn); // 处理隐式声明方式
        forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg) {
          arg.replace(FN_ARG, function(all, underscore, name) {
              $inject.push(name);
          });
        });
      }
      fn.$inject = $inject;
    }
  } else if (isArray(fn)) { // 使用 array-style annotations 方式
    last = fn.length - 1; // 获取fn函数
    assertArgFn(fn[last], 'fn');
    $inject = fn.slice(0, last); // 获取依赖项
  } else {
    assertArgFn(fn, 'fn', true);
  }
  return $inject; // 返回依赖数组
}

angular1 内部经过调用 annotate 函数,获取函数的依赖列表(即依赖数组)后,应该如何获取每一个项对应的依赖对象呢?咱们来进一步分析一下:  

假设咱们使用 array-style annotations 方式声明 fn 函数: 

var fn = ['a', 'b', function (a, b) {}]

调用annotate函数后,咱们得到 fn 的依赖列表,即返回 ['a','b']。

获取依赖列表后,咱们就可以根据依赖项的名称来获取对应的依赖对象。所以,依赖名与依赖对象的存储方式应该是使用 Key - Value 的方式进行存储(在 ES5 中咱们可使用对象字面量,如 var cache = {} 实现 K-V 存储)。在 angular1 内部提供了一个 getService 方法,用来获取依赖对象。它的具体实现以下:  

var INSTANTIATING = {}, // 是否实例化中
    providerSuffix = 'Provider', // provider后缀
    path = []; // 依赖路径

var factory = function(serviceName, caller) { // 实例工厂
   var provider = providerInjector.get(serviceName + providerSuffix, caller);
   return instanceInjector.invoke(provider.$get, provider, undefined, 
        serviceName);
});

function getService(serviceName, caller) {
      if (cache.hasOwnProperty(serviceName)) { // 依赖对象已建立
        if (cache[serviceName] === INSTANTIATING) {// 判断是否存在循环依赖
          throw $injectorMinErr('cdep', 'Circular dependency found:   
              {0}',serviceName + ' <- ' + path.join(' <- '));
        }
        return cache[serviceName];
      } else { // 依赖对象未建立
        try {
          path.unshift(serviceName); // 用于跟踪依赖路径
          cache[serviceName] = INSTANTIATING;
          // 实例化 serviceName 对应的依赖对象并存储
          return cache[serviceName] = factory(serviceName, caller);
        } catch (err) {
          if (cache[serviceName] === INSTANTIATING) {
            delete cache[serviceName]; // 实例化失败,从缓存中移除
          }
          throw err;
        } finally {
          path.shift();
        }
      }
    }

经过 getService 的实现方式,咱们能够知道,若依赖对象已存在,咱们直接从缓存中获取,若是依赖对象不存在,咱们经过调用 serviceName 对象的provider来建立依赖对象,而后保存在对象实例缓存中。这样的话,间接说明了一个问题,即在 angular1 中,全部的依赖对象都是单例。  

这里咱们先稍微解释一下Provider,而后再来列举 angular1 DI系统存在的一些问题。  

什么是Provider ?在 angular1 中,Provider是一个包含 $get 属性的普通 JS 对象。建立 provider 有两种方式:  

// 方式一: 使用对象方式
module.provider('a',{
  $get: function () {
     return 42;
   }
});

// 方式二: 使用构造函数方式
module.provider('a', function AProvider() {
   this.$get = function() { return 42; };
});

以上两种方式都是使用 module 对象提供的provider方法来注册 provider,angular1 中 provider 的具体实现以下:  

function provider(name, provider_) {
    // provider 的名称不能为hasOwnProperty
    assertNotHasOwnProperty(name, 'service');
    // 构造函数方式,先进行实例化
    if (isFunction(provider_) || isArray(provider_)) {
      provider_ = providerInjector.instantiate(provider_);
    }
    if (!provider_.$get) { // 判断 provider_ 对象是否存在 $get属性
      throw $injectorMinErr('pget', "Provider '{0}' must define $get factory 
          method.", name);
    }
 // 使用 name + "Provider"做为 Key 值,保存在 providerCache 中,用于建立实例
    return providerCache[name + providerSuffix] = provider_;
  }

angular1 DI 系统存在的问题

  • 内部缓存: angular1 应用程序中全部的依赖项都是单例,咱们不能控制是否使用新的实例

  • 命名空间冲突: 在系统中咱们使用字符串来标识 service 的名称,假设咱们在项目中已有一个 CarService,然而第三方库中也引入了一样的服务,这样的话就容易出现混淆

  • DI 耦合度过高: angular1 中 DI 功能已经被框架集成了,咱们不能单独使用它的 DI 特性

  • 未能和模块加载器结合: 在浏览器环境中,不少场景都是异步的过程,咱们须要的依赖模块并非一开始就加载好的,或许咱们在建立的时候才会去加载依赖模块,再进行依赖建立,而 angualr 的 IoC 容器无法作到这点。  

总结  

本文首先介绍了 IoC 和 DI 的概念及做用,而后讲述了 DI 在 angular1 中的实际应用。此外,简单的介绍了, angular1 DI 的实现方式,但并未深刻介绍 angular1 中的 injector ,有兴趣的同窗能够自行了解一下。最后,咱们介绍了 angular1 DI 系统中存在的问题,这样为咱们后面学习 angular2 DI 系统作好了铺垫,咱们能更好地理解它设计的意图。