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的简单&amp;简陋Tabs插件代码
Feb 09 Javascript
Jquery 扩展方法
May 06 Javascript
调试Node.JS的辅助工具(NodeWatcher)
Jan 04 Javascript
js取消单选按钮选中示例代码
Nov 14 Javascript
iframe窗口高度自适应的实现方法
Jan 08 Javascript
浅谈Javascript线程及定时机制
Jul 02 Javascript
jquery实现select下拉框美化特效代码分享
Aug 18 Javascript
再谈JavaScript异步编程
Jan 27 Javascript
json与jsonp知识小结(推荐)
Aug 16 Javascript
JS获取当前使用的浏览器名字以及版本号实现方法
Aug 19 Javascript
vue编译打包本地查看index文件的方法
Feb 23 Javascript
JavaScript中的函数式编程详解
Aug 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
页面乱码问题的根源及其分析
2013/08/09 PHP
PHP操作mysql数据库分表的方法
2016/06/09 PHP
PHP实现二维数组按某列进行排序的方法
2016/11/18 PHP
详谈PHP中的密码安全性Password Hashing
2017/02/04 PHP
php+mysql+jquery实现简易的检索自动补全提示功能
2017/04/15 PHP
基于jquery的商品展示放大镜
2010/08/07 Javascript
JS下拉框内容左右移动效果的具体实现
2013/07/10 Javascript
JS实现文字链接感应鼠标淡入淡出改变颜色的方法
2015/02/26 Javascript
javascript简单实现类似QQ头像弹出效果的方法
2015/08/03 Javascript
原生js实现图片层叠轮播切换效果
2016/02/02 Javascript
javascript 四十条常用技巧大全
2016/09/09 Javascript
利用yarn代替npm管理前端项目模块依赖的方法详解
2017/09/04 Javascript
Angular2的管道Pipe的使用方法
2017/11/07 Javascript
JS执行控制之节流模式实例分析
2018/12/21 Javascript
使用 Vue cli 3.0 构建自定义组件库的方法
2019/04/30 Javascript
viewer.js一个强大的基于jQuery的图像查看插件(支持旋转、缩放)
2020/04/01 jQuery
Nuxt 项目性能优化调研分析
2020/11/07 Javascript
原生js实现放大镜组件
2021/01/22 Javascript
[02:51]DOTA2英雄基础教程 风暴之灵
2013/12/23 DOTA
[01:14]2014DOTA2展望TI 剑指西雅图newbee战队专访
2014/06/30 DOTA
[06:23]2014DOTA2西雅图国际邀请赛 小组赛7月12日TOPPLAY
2014/07/12 DOTA
[01:24:16]2018DOTA2亚洲邀请赛 4.6 全明星赛
2018/04/10 DOTA
python获取Linux下文件版本信息、公司名和产品名的方法
2014/10/05 Python
python3解析库lxml的安装与基本使用
2018/06/27 Python
解决PyCharm的Python.exe已经停止工作的问题
2018/11/29 Python
Python下简易的单例模式详解
2019/04/08 Python
Virtualenv 搭建 Py项目运行环境的教程详解
2020/06/22 Python
HTML5使用drawImage()方法绘制图像
2014/06/23 HTML / CSS
瑞典轮胎在线:Tirendo.se
2018/06/21 全球购物
关于十八大的演讲稿
2014/09/15 职场文书
商品陈列协议书
2014/09/29 职场文书
客房部经理岗位职责
2015/02/02 职场文书
超市员工管理制度
2015/08/06 职场文书
一篇文章告诉你如何实现Vue前端分页和后端分页
2022/02/18 Vue.js
MySQL学习之基础操作总结
2022/03/19 MySQL
MySql重置root密码 --skip-grant-tables
2022/04/11 MySQL