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 相关文章推荐
jQuery EasyUI API 中文文档 可调整尺寸
Sep 29 Javascript
JavaScript加强之自定义callback示例
Sep 21 Javascript
javascript调试之DOM断点调试法使用技巧分享
Apr 15 Javascript
JS验证input输入框(字母,数字,符号,中文)
Mar 23 Javascript
Jquery中.bind()、.live()、.delegate()和.on()之间的区别详解
Aug 01 jQuery
Bootstrap实现翻页效果
Nov 27 Javascript
Element-UI Table组件上添加列拖拽效果实现方法
Apr 14 Javascript
JS实现简单获取最近7天和最近3天日期的方法
Apr 18 Javascript
vue中tab选项卡的实现思路
Nov 25 Javascript
微信小程序使用map组件实现检索(定位位置)周边的POI功能示例
Jan 23 Javascript
微信小程序使用蓝牙小插件
Sep 23 Javascript
vue.js使用v-model实现父子组件间的双向通信示例
Feb 05 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中ADODB类详解
2008/03/25 PHP
CentOS6.5 编译安装lnmp环境
2014/12/21 PHP
带你了解PHP7 性能翻倍的关键
2015/11/19 PHP
Laravel 之url参数,获取路由参数的例子
2019/10/21 PHP
JavaScript 设计模式学习 Singleton
2009/07/27 Javascript
超级酷和最实用的jQuery实例收集(20个)
2010/04/21 Javascript
有趣的javascript数组定义方法
2010/09/10 Javascript
javascript学习笔记(十三) js闭包介绍(转)
2012/06/20 Javascript
jquery 插件学习(一)
2012/08/06 Javascript
jquery为页面增加快捷键示例
2014/01/31 Javascript
超链接的禁用属性Disabled使用示例
2014/07/31 Javascript
Highcharts使用简例及异步动态读取数据
2015/12/30 Javascript
jQuery实现的可编辑表格完整实例
2016/06/20 Javascript
javascript数组对象常用api函数小结(连接,插入,删除,反转,排序等)
2016/09/20 Javascript
Bootstrap Tooltip显示换行和左对齐的解决方案
2017/10/11 Javascript
vue源码入口文件分析(推荐)
2018/01/30 Javascript
vue自定义filters过滤器
2018/04/26 Javascript
[44:41]Fnatic vs Liquid 2018国际邀请赛小组赛BO2 第二场 8.16
2018/08/17 DOTA
Python编程之string相关操作实例详解
2017/07/22 Python
Python基于回溯法子集树模板实现8皇后问题
2017/09/01 Python
python实现拓扑排序的基本教程
2018/03/11 Python
python遍历小写英文字母的方法
2019/01/02 Python
Python除法之传统除法、Floor除法及真除法实例详解
2019/05/23 Python
Python进程池Pool应用实例分析
2019/11/27 Python
py-charm延长试用期限实例
2019/12/22 Python
关于python 的legend图例,参数使用说明
2020/04/17 Python
python 双循环遍历list 变量判断代码
2020/05/04 Python
Pytorch转tflite方式
2020/05/25 Python
基于PyInstaller各参数的含义说明
2021/03/04 Python
Office DEPOT法国官网:欧迪办公用品采购
2018/01/03 全球购物
大学生职业规划范文:象牙塔生活的四年计划
2014/01/14 职场文书
高级编程求职信模板
2014/02/16 职场文书
民事纠纷协议书
2016/03/23 职场文书
JavaScript的Set数据结构详解
2022/02/18 Javascript
Android自定义scrollview实现回弹效果
2022/04/01 Java/Android
vue.js 使用原生js实现轮播图
2022/04/26 Vue.js