Angular.JS学习之依赖注入$injector详析


Posted in Javascript onOctober 20, 2016

前言

在依赖注入(IoC)之前,我们在程序中需要创建一个对象很简单也很直接,就是在代码中new Object即可,有我们自己负责创建、维护、修改和删除,也就是说,我们控制了对象的整个生命周期,直到对象没有被引用,被回收。诚然,当创建或者维护的对象数量较少时,这种做法无可厚非,但是当一个大项目中需要创建大数量级的对象时,仅仅依靠程序员来进行维护所有对象,这是难以做到的,特别是如果想在程序的整个生命周期内复用一些对象,我们需要自己写一个缓存模块对所有对象进行缓存,这加大了复杂度。当然,IoC的好处并不仅限于此,它也降低了对依赖的耦合度,不必在代码中进行引用或者传参即可操作依赖。

在js中,我们可以这样引入依赖

1、使用全局变量引用

2、在需要的地方通过函数参数传递

使用全局变量的坏处自不必说,污染了全局的名字空间,而通过函参传递引用,也可以通过两种方法实现:

1、闭包传递

2、后台解析出依赖对象,并通过Function.prototype.call进行传参

而在AngularJS中,依赖注入是通过后者实现的,接下来的几节将会介绍IoC模块的具体实现。

获取依赖

var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
var FN_ARG_SPLIT = /,/;
 // 获取服务名
var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/;
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
var $injectorMinErr = minErr('$injector');

function anonFn(fn) {
 // For anonymous functions, showing at the very least the function signature can help in
 // debugging.
 var fnText = fn.toString().replace(STRIP_COMMENTS, ''),
  args = fnText.match(FN_ARGS);
 if (args) {
 return 'function(' + (args[1] || '').replace(/[\s\r\n]+/, ' ') + ')';
 }
 return 'fn';
}

function annotate(fn, strictDi, name) {
 var $inject,
  fnText,
  argDecl,
  last;

 if (typeof fn === 'function') {
 if (!($inject = fn.$inject)) {
  $inject = [];
  if (fn.length) {
  if (strictDi) {
   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);
  }
  fnText = fn.toString().replace(STRIP_COMMENTS, '');
  argDecl = fnText.match(FN_ARGS);
  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)) {
 last = fn.length - 1;
 assertArgFn(fn[last], 'fn');
 $inject = fn.slice(0, last);
 } else {
 assertArgFn(fn, 'fn', true);
 }
 return $inject;
}

annotate函数通过对入参进行针对性分析,若传递的是一个函数,则依赖模块作为入参传递,此时可通过序列化函数进行正则匹配,获取依赖模块的名称并存入$inject数组中返回,另外,通过函数入参传递依赖的方式在严格模式下执行会抛出异常;第二种依赖传递则是通过数组的方式,数组的最后一个元素是需要使用依赖的函数。annotate函数最终返回解析的依赖名称。

注入器的创建

AngularJS的API也提供了injector部分,通过injector部分,通过injector可以使用get,has,instantiate,invoke以及上节提到的annotate等方法,通过源码可以更清晰的理解。

function createInternalInjector(cache, factory) {
 // 对服务注入器 providerInjector而言,只根据服务名获取服务,factory会抛出异常
 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;
   return cache[serviceName] = factory(serviceName, caller);
  } catch (err) {
   if (cache[serviceName] === INSTANTIATING) {
   delete cache[serviceName];
   }
   throw err;
  } finally {
   path.shift();
  }
  }
 }

 function invoke(fn, self, locals, serviceName) {
  if (typeof locals === 'string') {
  serviceName = locals;
  locals = null;
  }

  var args = [],
   // 解析并获取注入服务列表
   $inject = annotate(fn, strictDi, serviceName),
   length, i,
   key;

  for (i = 0, length = $inject.length; i < length; i++) {
  key = $inject[i];
  if (typeof key !== 'string') {
   throw $injectorMinErr('itkn',
     'Incorrect injection token! Expected service name as string, got {0}', key);
  }
  // 注入的服务作为参数传入
  args.push(
   locals && locals.hasOwnProperty(key)
   ? locals[key]
   : getService(key, serviceName)
  );
  }
  if (isArray(fn)) {
  fn = fn[length];
  }

  // http://jsperf.com/angularjs-invoke-apply-vs-switch
  // #5388
  return fn.apply(self, args);
 }

 function instantiate(Type, locals, serviceName) {
  // Check if Type is annotated and use just the given function at n-1 as parameter
  // e.g. someModule.factory('greeter', ['$window', function(renamed$window) {}]);
  // Object creation: http://jsperf.com/create-constructor/2
  var instance = Object.create((isArray(Type) ? Type[Type.length - 1] : Type).prototype);
  var returnedValue = invoke(Type, instance, locals, serviceName);

  return isObject(returnedValue) || isFunction(returnedValue) ? returnedValue : instance;
 }

 return {
  invoke: invoke,
  instantiate: instantiate,
  get: getService,
  annotate: annotate,
  has: function(name) {
  return providerCache.hasOwnProperty(name + providerSuffix) || cache.hasOwnProperty(name);
  }
 };
 }

createInternalInjector方法创建$injector对象,传递的参数分别为缓存对象和工厂函数。在具体实现中,AngularJS创建了两个injector对象--providerInjector和instanceInjector(这两个对象的不同主要是createInternalInjector方法传递的缓存对象不同),而通过angular.injector()导出的就是instanceInjector。对于providerInjector,主要用来获取服务的提供者,即serviceProvider。而对于instanceInjector而言,主要用于执行从providerInjector获取的provider对象的$get方法,生产服务对象(依赖),并将服务对象传递给相应的函数,完成IoC。

首先从get方法说起,get方法主要获取指定名称的服务,通过angular的injector方法获取的是instanceInjector,而当缓存中没有该服务对象(依赖)时,我们需要执行factory(serviceName, caller)方法,我们看看对于的factory函数:

instanceInjector = (instanceCache.$injector =
   createInternalInjector(instanceCache, function
(serviceName, caller) { var provider = providerInjector.get(serviceName + providerSuffix, caller); return
 instanceInjector.invoke(provider.$get, provider, undefined, serviceName);
   }));

红色部分即为factory函数,它显示通过providerInjector获取相应服务的提供者serviceProvider,然后调用instanceInjector的invoke方法在serviceProvider上下文执行serviceProvider的$get方法,返回服务对象并保存在缓存中。这样,便完成了服务对象(依赖)的获取和缓存。

invoke方法也很简单,它的入参分别问fn, self, locals, serviceName,即要执行的函数,函数执行的上下文,提供的options选项和服务名。首先获取函数的所有依赖名,通过annotate方法完成之后,如果options中提供了对于名称的依赖,则使用,否则通过get方法获取依赖,最后传入函数,并将函数的执行结果返回。invoke返回的结果往往是一个服务对象。

instantiate方法主要根据提供的构造函数创建一个示例,用作依赖或提供服务。值得一提的是并没有通过new关键字创建对象,而是通过ECMA5提供的Object.create来继承函数的原型对象实现,非常巧妙。

has方法则是相继判断serviceProvider和service是否存在于缓存中。

至此,$injector对象创建完毕。

注册服务(依赖)

服务不可能凭空而来,我们需要自己实现或者外部引入服务或依赖。所以,注册服务的模块也是值得深究的。AngularJS提供了多种注册服务的API,但是我们着重关注的是provider方法,其他factory,service方法都是基于此进行构建的。

这些方法(provider,factory等)绑定在providerCache.provide对象上,而我们通过angular.module(′app′,[]).provider(...)方式调用的provider函数,会在module加载期间将调用(该调用抽象成一个数组,即[provider,method,arguments])绑定在内部的invokeQueue数组中,最终在providerCache.provide对象上,而我们通过angular.module(′app′,[]).provider(...)方式调用的provider函数,会在module加载期间将调用(该调用抽象成一个数组,即[provider,method,arguments])绑定在内部的invokeQueue数组中,最终在providerCache.provide对象上调用provider方法,其他的controller,directive等方法类似,不过是绑定在providerCache.controllerProvider,providerCache.controllerProvider,providerCache.compileProvider对象上。

function provider(name, provider_) {
 assertNotHasOwnProperty(name, 'service');
 if (isFunction(provider_) || isArray(provider_)) {
  provider_ = providerInjector.instantiate(provider_);
 }
 if (!provider_.$get) {
  throw $injectorMinErr('pget', "Provider '{0}' must define $get factory method.", name);
 }
 return providerCache[name + providerSuffix] = provider_;
 }

 function enforceReturnValue(name, factory) {
 return function enforcedReturnValue() {
  var result = instanceInjector.invoke(factory, this);
  if (isUndefined(result)) {
  throw $injectorMinErr('undef', "Provider '{0}' must return a value from $get factory method.", name);
  }
  return result;
 };
 }

 function factory(name, factoryFn, enforce) {
 return provider(name, {
  $get: enforce !== false ? enforceReturnValue(name, factoryFn) : factoryFn
 });
 }

 function service(name, constructor) {
 return factory(name, ['$injector', function($injector) {
  return $injector.instantiate(constructor);
 }]);
 }

 function value(name, val) { return factory(name, valueFn(val), false); }

 function constant(name, value) {
 assertNotHasOwnProperty(name, 'constant');
 providerCache[name] = value;
 instanceCache[name] = value;
 }
 // 在服务实例化之后进行调用(拦截),拦截函数中注入实例化的服务,可以修改,扩展替换服务。
 function decorator(serviceName, decorFn) {
 var origProvider = providerInjector.get(serviceName + providerSuffix),
  orig$get = origProvider.$get;

 origProvider.$get = function() {
  var origInstance = instanceInjector.invoke(orig$get, origProvider);
  return instanceInjector.invoke(decorFn, null, {$delegate: origInstance});
 };
 }

provider方法需要两个参数,一个是服务名(依赖名),另外是工厂方法或者是一个包含依赖和工厂方法的数组。首先通过providerInjector创建工厂方法的一个实例,并添加到providerCache中,返回。

factory方法只是将第二个参数封装成了一个包含$get方法的对象,即serviceProvider,缓存。并不复杂。

而service方法则嵌套注入了$injector服务,即instanceInjector,它会创建构造函数的实例,作为服务对象。

value方法仅仅封装了一个provider,其$get方法返回value值。

constant方法则将value的值分别存入providerCache和instanceCache中,并不需要invoke获取其value值。

而比较特殊且扩展性较高的decorator方法,是在serviceProvider的get方法后面添加一个拦截函数,并通过传递依赖get方法后面添加一个拦截函数,并通过传递依赖delegate来获取原先invoke $get方法返回的服务对象。我们可以通过decorator来对服务进行扩展,删除等操作。

流程

最后,在基本的实现已经完成的基础上,我们走一遍具体的注入流程,更易于我们的深入理解。

angular.module("app",[])
  .provider("locationService",function(){
  ...
 })
 .controller("WeatherController",function($scope,locationService,$location){
  locationService.getWeather()
   .then(function(data){
    $scope.weather = data;
   },function(e){
    console.log("error message: "+ e.message)
   });
 })

我们不关心具体的代码实现,仅仅使用上述代码作为演示。

首先确定AngularJS上下文的范围,并且获取依赖模块(在此处为空);

继续注册服务(依赖),将serviceProvider缓存至providerCache中;

声明控制器;

在此获取injector示例,通过执行invoke函数,获取[“injector示例,通过执行invoke函数,获取[“scope”,”locationService”,”location”]依赖列表,通过location”]依赖列表,通过injector的get方法获取相应的依赖对象。对于scope和scope和location服务而言,在AngularJS初始化时已经注入到Angular中,因此可以获取相应的provider对象,执行相关的方法返回scope和scope和location对象,而locationService则在provider中进行了声明,因此获取到locationServiceProvider对象,通过调用instanceInjector.invoke(locationServiceProvider.$get, locationServiceProvider, undefined, “locationService”)返回locationService对象。

最后将所有的依赖组装成数组[scope,locationService,scope,locationService,location]作为参数传递给匿名函数执行。

总结

至此,依赖注入完成。大家对依赖注入$injector有没有进一步的了解呢?以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流。

Javascript 相关文章推荐
js动态设置div的值下例子
Oct 29 Javascript
JQuery获取与设置HTML元素的内容或文本的实现代码
Jun 20 Javascript
Bootstrap基础学习
Jun 16 Javascript
jquery实现点击弹出带标题栏的弹出层(从右上角飞入)效果
Sep 19 Javascript
学习javascript的闭包,原型,和匿名函数之旅
Oct 18 Javascript
JavaScript中实现键值对应的字典与哈希表结构的示例
Jun 12 Javascript
js运动事件函数详解
Oct 21 Javascript
详解Angular 中 ngOnInit 和 constructor 使用场景
Jun 22 Javascript
jQuery中.attr()和.data()的区别分析
Sep 03 jQuery
JS获取指定月份的天数两种实现方法
Jun 22 Javascript
详解JSON Web Token 入门教程
Jul 30 Javascript
vueScroll实现移动端下拉刷新、上拉加载
Mar 22 Javascript
Javascript中内建函数reduce的应用详解
Oct 20 #Javascript
基于AngularJS前端云组件最佳实践
Oct 20 #Javascript
分享javascript、jquery实用代码段
Oct 20 #Javascript
基于原生js淡入淡出函数封装(兼容IE)
Oct 20 #Javascript
纯JavaScript手写图片轮播代码
Oct 20 #Javascript
Zabbix添加Node.js监控的方法
Oct 20 #Javascript
Javascript实现图片懒加载插件的方法
Oct 20 #Javascript
You might like
PHP 危险函数解释 分析
2009/04/22 PHP
UTF8编码内的繁简转换的PHP类
2009/07/09 PHP
PHP imagecreatefrombmp 从BMP文件或URL新建一图像
2012/07/16 PHP
php递归使用示例(php递归函数)
2014/02/14 PHP
zen_cart实现支付前生成订单的方法
2016/05/06 PHP
CI框架常用函数封装实例
2016/11/21 PHP
CodeIgniter框架常见用法工作总结
2017/03/16 PHP
CentOS7.0下安装PHP5.6.30服务的教程详解
2018/09/29 PHP
URI、URL和URN之间的区别与联系
2006/12/20 Javascript
javascript实现unicode和字符的互相转换
2007/07/18 Javascript
自己用jQuery写了一个图片的马赛克消失效果
2014/05/04 Javascript
JavaScript和HTML DOM的区别与联系及Javascript和DOM的关系
2015/11/15 Javascript
JavaScript判断浏览器和hack滚动条的写法
2017/07/23 Javascript
jquery ajax加载数据前台渲染方式 不用for遍历的方法
2018/08/09 jQuery
vue路由缓存的几种实现方式小结
2020/02/02 Javascript
python条件变量之生产者与消费者操作实例分析
2017/03/22 Python
python使用fork实现守护进程的方法
2017/11/16 Python
python实现树形打印目录结构
2018/03/29 Python
Python排序算法之选择排序定义与用法示例
2018/04/29 Python
Python实现自定义函数的5种常见形式分析
2018/06/16 Python
kaggle+mnist实现手写字体识别
2018/07/26 Python
python实现简单登陆系统
2018/10/18 Python
基于Python中Remove函数的用法讨论
2020/12/11 Python
美国牙科折扣计划:DentalPlans.com
2019/08/26 全球购物
附答案的Java面试题
2012/11/19 面试题
土木工程专业大学毕业生求职信
2013/10/13 职场文书
医院实习介绍信
2014/01/12 职场文书
公司庆典邀请函范文
2014/01/13 职场文书
班组长岗位职责
2014/03/03 职场文书
2014最新开业庆典策划方案(5篇)
2014/09/15 职场文书
转正申请报告格式
2015/05/15 职场文书
搞笑欢迎词大全
2015/09/30 职场文书
《游戏公平》教学反思
2016/02/20 职场文书
Java并发编程必备之Future机制
2021/06/30 Java/Android
Python pandas之求和运算和非空值个数统计
2021/08/07 Python
详解Python中__new__方法的作用
2022/03/31 Python