Angular 2 DI - IoC & DI - 1
IoC 是什么Ioc - Inversion of Control,即"控制反转"。在开发中, IoC 意味着你设计好的对象交给容器控制,而不是使用传统的方式,在对象内部直接控制。 如何理解好 IoC 呢?理解好 IoC的关键是要明确"谁控制谁,控制什么,为何是反转(有反转就应该有正转),哪些方面反转了",我们来深入分析一下。
IoC 能做什么Ioc 不是一种技术,只是一种思想,一个重要的面向对象编程法则,它能指导我们如何设计松耦合、更优良的系统。传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试;有了 IoC 容器后,把创建和查找依赖对象的控制权交给了容器,由容器注入组合对象,所以对象之间是松散耦合,这样也便于测试,利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活。 其实 IoC 对编程带来的最大改变不是从代码上,而是思想上,发生了"主从换位"的变化。应用程序本来是老大,要获取什么资源都是主动出击,但在 IoC思想中,应用程序就变成被动了,被动的等待 IoC 容器来创建并注入它所需的资源了。 IoC 和 DIDI - Dependency Injection,即"依赖注入":组件之间的依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。 理解 DI 的关键是:"谁依赖了谁,为什么需要依赖,谁注入了谁,注入了什么",那我们来深入分析一下:
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+?)1s*$/; // 匹配参数项 var STRIP_COMMENTS = /((//.*$)|(/*[sS]*?*/))/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(/[srn]+/,' ') + ')'; } 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) {}] 调用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 系统存在的问题
总结本文首先介绍了 IoC 和 DI 的概念及作用,然后讲述了 DI 在 angular1 中的实际应用。此外,简单的介绍了, angular1 DI 的实现方式,但并未深入介绍 angular1 中的 injector ,有兴趣的同学可以自行了解一下。最后,我们介绍了 angular1 DI 系统中存在的问题,这样为我们后面学习 angular2 DI 系统做好了铺垫,我们能更好地理解它设计的意图。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |