Skip to content

Angular 2 DI - 控制反转和依赖注入及在Angular 1.x 中的应用 #10

@semlinker

Description

@semlinker

IoC 是什么

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

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

  • 谁控制谁,控制什么: 在传统的程序设计中,我们直接在对象内部通过 new 的方式创建对象,是程序主动创建依赖对象;而 IoC 是有专门一个容器来创建这些对象,即由 IoC 容器控制对象的创建;谁控制谁?当然是 IoC 容器控制了对象;控制什么?主要是控制外部资源获取。
  • 为何是反转了,哪些方面反转了: 有反转就有正转,传统应用程序是由我们自己在对象中主动控制去获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象;为何是反转?因为由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转了;哪些方面反转了?依赖对象的获取被反转了。

IoC 能做什么

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

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

IoC 和 DI

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

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

  • 谁依赖了谁:当然是应用程序依赖 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 系统做好了铺垫,我们能更好地理解它设计的意图。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions