zoukankan      html  css  js  c++  java
  • 【原创】angularjs1.3.0源码解析之service

    Angular服务

    在angular中,服务(service)是以提供特定的功能的形式而存在的。

    angular本身提供了很多内置服务,比如:

    1. $q: 提供了对promise的支持。
    2. $http: 提供了对ajax的支持。
    3. $location: 提供了对URL的解析。
    4. ...

    这些服务,或多或少地会出现在我们控制器(controller)、指令(directive)或者某个被依赖注入的函数中,帮助我们实现特定的功能。

    当然,除了调用官方的服务,我们也可以定义适合于自己业务的“服务”,这个后面会讲到。

    服务的本质

    从代码层面来看,服务其实一个单例(可以是任何类型),被所有的调用者所共享(在一个angular应用生命周期内,它只会被初始化一次)。

    服务的调用

    所有的服务,都是通过依赖注入的方式被调用的。

    像这样,简单地调用$timeout服务(代码在这里):

    var app = angular.module('myApp', []);
    
    // 注入$timeout服务
    app.run(['$timeout', function ($timeout) {
    
        $timeout(function () {
            console.log('hello');
        },2000).then(function () {
            console.log('world');
        });
    }]);
    

    显然,要想弄清楚服务,必然得先说一说依赖注入,往下看~~

    依赖注入(DI)

    angular在前端引入了DI的概念,目的是更快更轻巧地引入函数执行时所需要的依赖项(这些依赖项就是前面说到的服务)。

    设想:我们在写一个异步请求数据的函数,我们会调用库异步api:

    用jquery,我们会这样写:

    function getData () {
        $.ajax({
            ...
        });
    }
    

    用angular,我们可以这样写:

    function getData ($http) {
        $http({
            ...
        });
    }
    
    1. 从功能层面上看:都一样。
    2. 从代码层面上看:jquery方面,通过$.ajax调用(万一$是全局的且函数嵌套很深呢?);而angular方面,则是通过形参$http直接调用且更清晰。
    3. 从性能方面上看:jquery方面,$.ajax方法在程序开始就会被声明初始化;而angular方面,事实上,只在$http第一次被调用时才会被初始化。

    另外:设想一下,如果angular像jquery这样做:

    angular.$http = function () {...}
    

    那么对于越来越多的自定义服务而言:

    angular.$xxx = function () {...}
    

    这样会使得,angular这个全局变量一开始就会变得很臃肿而且受到到污染,在调用方面也更是多了一层全局访问。

    综上:angular采用依赖注入服务的形式来使用特定功能更为恰当(当然,以上都是个人理解)。


    那么来看看依赖注入的几种方式(代码在这里):

    1.隐式注入(通过函数toString,正则匹配出依赖项,如:$log

    var ng = angular.module('ng');
    
    // 隐式注入
    ng.run(function ($log) {
        $log.error('隐式注入');
    });
    
    

    2.显示注入(通过数组或者fn.$inject,指明依赖项,如:$log

    var ng = angular.module('ng');
    
    // 显示注入
    ng
      // 数组指明依赖项
      .run(['$log', function (log) {
        log.error('数组显示注入');
      }])
      // fn.$inject指明依赖项
      .run(fn);
    
    function fn (log) {
        log.error('$inject显示注入');
    }
    
    fn.$inject = ['$log'];
    

    问:angular是如何解析函数的依赖项的?

    答:通过一个叫做annotate的函数解析,解析的步骤如下:

    1. 如果目标是函数fn
      • 优先获取fn.$inject作为依赖项(显示注入)
      • 其次是通过fn.toString(),然后正则匹配出参数作为依赖项(隐式注入)
    2. 如果目标是数组arr,那么arr.slice(0, arr.length - 1)作为依赖项(显示注入)

    我们可以这样做测试:

    function fn ($log, $timeout) {
        // ...
    }
    console.log(angular.injector.$$annotate(fn)); // ["$log", "$timeout"]
    

    注意:由于线上代码往往会压缩混肴,所以这时只能使用显示依赖注入的方式。

    ok,到这里我们知道可以通过annotate函数解析出一个函数(或数组)的依赖项,那么如何实现注入的呢?

    答案:一切的一切都跟内置的服务$injector有关。


    是否还记得angular程序启动时,会执行这句:

    var injector = createInjector(modules, config.strictDi);
    

    这是能够使用依赖注入的根本原因,因为它创建了$injector服务。


    仔细的分析createInjector方法到底做了些什么:

    1. 创建了两个injector实例以及两个cache,用于依赖注入

      • providerInjector(服务提供者的依赖注入)
      • instanceInjector(服务的依赖注入)
      • providerCache(服务提供者缓存)
      • instanceCache(服务缓存)
    2. 加载程序所需要的所有(依赖)模块

      • 注册依附在模块上的指令,服务,控制器,过滤器等
      • 执行模块的配置函数(即myModule.config(fn)
      • 执行模块的运行函数(即myModule.run(fn)

    这里就需要说明下 服务提供者(provider)服务(instance)的关系?

    服务的提供者:

    1. 顾名思义,是用来提供服务的(从程序的角度来说就是,初始化服务实例并返回)
    2. 另外,它还有一个作用就是:配置服务

    $rootScope服务为例:它对应的服务提供者就是$rootScopeProvider(注意:$injector$provide除外)。

    来个例子吧(代码在这里):

    服务的注入:

    var app = angular.module('myApp', []);
    
    app
        .run(function ($rootScope) {
            $rootScope.msg = 'hello world';
        });
    
    

    服务提供者的注入:

    var app = angular.module('myApp', []);
    
    app
        .config(function ($rootScopeProvider) {
            $rootScopeProvider.digestTtl(5);   // 配置digest循环的ttl
        });
    
    

    总结:上面的两个例子进行了依赖注入,不同的只是注入的对象(一个是服务,一个是服务提供者)。

    造成这种不同的根本原因是什么?

    1. run函数在内部使用instanceInjector.invoke(fn),获取的是依赖的服务实例,并传递给fn函数,供调用,提供服务功能。

    2. config函数在内部使用providerInjector.invoke(fn),获取服务提供者,并传递给fn函数,供调用,提供服务配置。

    所以说,就有了两个维度上的差别:

    1. instanceInjector:针对的是服务实例的注入。
    2. providerInjector:针对的是服务提供者的注入。

    知道了原理后,对于上面的两个注入的例子我们可以这样改写,代码在这里

    服务的注入:

    var app = angular.module('myApp', []);
    
    app
        .config(function ($injector) {  // 获取providerInjector
            $injector.invoke(function ($rootScopeProvider) {
                $rootScopeProvider.digestTtl(5);  // 配置digest循环的ttl
                console.log($rootScopeProvider)
            });
        });
    

    服务提供者的注入:

    var app = angular.module('myApp', []);
    
    app
        .run(function ($injector) {  // 获取instanceInjector
            $injector.invoke(function ($rootScope) {
                $rootScope.msg = 'hello world';
            });
        });
    
    

    另外:对于instanceInjector,我们也可以通过angular.injector(...)自己创建,所以我们还可以这样改写,(代码在这里):

    var injector = angular.injector(['ng']);
    
    injector.invoke(function ($rootScope, $compile) {
          $rootScope.$apply(function () {
              $compile(document.body)($rootScope);
              $rootScope.msg = 'hello world';
          });  
    });
    

    注意:以上的改写只是为了说明原理,一般都不建议这样做,因为angular会帮我们做这部分工作,我们只要按要求引入依赖即可。


    再然后,说说服务提供者如何提供服务?

    依旧拿$rootScope举例:

    $rootScope的提供者$rootScopeProvider有一个(可以依赖注入的)$get函数(或者数组),负责返回服务实例(可以是任何类型)。


    再说说cache的问题?

    之前说过angular的服务实例是一个单例,归根结底的原因就是cache的存在,才使得服务实例的构造函数只会执行一次。

    上面说过了,有两个cache对象:

    1. providerCache(服务提供者缓存)
    2. instanceCache(服务实例缓存)

    当我们注册一个服务,叫做'xx'(准确的说是服务提供者)时,大部分情况会经过包装(或者实例化)转换成包含$get函数(或者数组)的对象,然后存储到providerCache中,所以:

    1. 如果是对服务提供者(xxProvider)的依赖,其实都是根据键值'xxProvider'providerCache中获取的,如果获取不到则报错。
    2. 如果是对服务(xx)的依赖,则是优先根据键值'xx'instanceCache中获取,
      • 如果获取到,则返回。
      • 如果获取不到,那么就根据键值'xxProvider'获取服务提供者,然后通过调用它的$get方法返回服务实例,最后再缓存进instanceCache中。

    服务的注册

    之前,我们举例说的都是内置服务,现在我们看看如何自定义属于适合自己业务的服务?

    angular给我们提供了以下几种注册服务的方式:

    1. provider:注册可配置的服务。

    2. factory:注册服务的快捷方式,基于provider方法,不可配置。

    3. service:利用构造函数注册服务,基于factory方法。

    4. value:注册简单的返回值服务,不支持依赖注入,基于factory方法。

    5. constant:注册常量服务,且在config函数中可调用。

    6. decorator:拦截装饰现有服务,用于扩展或者重写服务。


    我们举例来说明,(代码在这里):

    1.provider方法

    var app = angular.module('myApp', []);
    
    // provider 可配置服务
    
    app.provider('A', ['$logProvider', function (LogProvider) {
        
        var isDebug = true;
        
        this.debugEnabled = function (flag) {
            LogProvider.debugEnabled(!!flag);
        };
        
        this.$get = ['$log', function (log) {
            return {
                debug: function (msg) {
                    log.debug(msg);
                }
            };
        }];
    }]);
    
    

    provider方法创建了可配置的服务A,它的可配置体现在:我们可以在config函数中调用它的服务提供者(AProvider)的debugEnabled方法进行配置,如下:

    app
        .config(function(AProvider) {
            AProvider.debugEnabled(false); // 配置服务
        })
    

    另外,可以看到AProvider还有一个必不可少的(可依赖注入的)$get数组(当然也可以是函数),它则负责返回服务实例(就像之前说的那样),因此,我们在run函数中才可以这样调用:

    app
        .run(function (A) {
           A.debug('A provider') ;
        })
    

    2.factory方法

    var app = angular.module('myApp', []);
    
    // factory 
    
    app.factory('B', function ($log) {
       return {
           debug: function (msg) {
               $log.debug(msg);
           }
       };    
    });
    
    

    factory方法是注册了服务B,和服务A基本一样,唯一不同的就是它不可配置

    其实factory方法是基于provider方法的,其实是注册服务的快捷方式,内部实现是这样的:

    function factory(name, factoryFn) { return provider(name, { $get: factoryFn }); }
    

    也就是说,我们在factory中注册的函数,在provider中被当作$get,那么函数的返回值自然就是服务实例,所以可以这样调用:

    app
        .run(function (B) {
           B.debug('B factory');
        });
    

    3.service方法

    var app = angular.module('myApp', []);
    
    // service 构造函数
    
    app.service('C', function ($log) {
        this.debug = function (msg) {
            $log.debug(msg);
        };
    });
    
    

    service方法注册了服务C,与factory方法不同的是,它的函数不需要返回值,而是以构造函数实例化的形式生成服务实例,

    所以可以这样调用:

    app.
        .run(function (C) {
           C.debug('C service');
        });
    

    其实service方法是基于factory方法的,归根结底也是基于provider方法,内部实现是这样的:

    function service(name, constructor) {
      return factory(name, ['$injector', function($injector) {
        return $injector.instantiate(constructor); // 实例化构造函数
      }]);
    }
    

    4.value方法

    var app = angular.module('myApp', []);
    
    // value
    
    app.value('E', {
        debug: function (msg) {
            console.log(msg);
        }
    });
    

    value方法注册了服务E,服务实例就是注册的值,这里是一个对象(当然,可以是其他任何类型)。

    比如:

    app.value('F', 'hello world');  // 一个字符串
    app.value('G', function () {    // 或者一个函数
        console.log('hello world');
    });
    

    其实value方法,也是基于factory方法的,内部实现如下:

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

    可见,value方法就是将这个注册的值作为服务实例返回,只是中间过程不再有依赖注入

    使用方法:

    app
        .run(function (E) {
           E.debug('E value');
        });
    

    5.constant方法

    var app = angular.module('myApp', []);
    
    // constant 常量
    
    app.constant('D', 'D constant');
    app.constant('D', 'modified'); // 无效
    
    

    constant方法是用来定义常量服务的,它的值可以使任何类型。

    说说跟value方法的区别:

    • constant方法定义的服务不可修改(毕竟是常量)

    • constant方法定义的服务还可以在config函数中被调用


    说说constant定义的服务不可修改的原因?

    首先得看看模块对象的constant方法的实现:

    constant: invokeLater('$provide', 'constant', 'unshift')
    

    不清楚invokeLater方法的可以看我上一篇写的《指令》里有介绍。

    这里我们要注意的是:数组的unshift方法,从前插入,这就意味着在循环数组时,第一个执行app.constant()方法的总是最后一个执行$provide.constant()方法,

    所以这样就做到了常量的效果,实质上每行代码都执行了,只是值覆盖顺序反过来而已。


    说说为什么可以在config函数中被调用常量服务?

    这就需要看下$provide.constant方法的实现:

    function constant(name, value) {
      assertNotHasOwnProperty(name, 'constant');
      providerCache[name] = value; // 因为这句,config函数可以直接注入,并且不需要加后缀Provider
      instanceCache[name] = value;
    }
    

    所以,最终我们可以这样调用服务D:

    app
        .config(function(D) {
            console.log('config: ' + D);
        })
        .run(function (D) {
           console.log(D);
        });
    

    6.decorator方法

    var app = angular.module('myApp', []);
    
    // decorator
    
    app.config(function ($provide) {
        $provide.decorator('A', function ($delegate, $log) {
            $delegate.error = function (msg) {
                $log.error(msg);
            };
            return $delegate;   // 注意:一定要返回修改的(或新的)服务实例
        });  
    });
    
    

    decorator方法不是用来注册服务,而是用来装饰现在有服务的(换句话说,就是扩展服务或者重写服务)。

    上面的代码是对服务A进行了扩展,增加了error方法。

    所以,我们便可以这样调用了:

    app
        .run(function (A) {
           A.error('A decorator') ;
        });
    

    另外,需要注意的是,它的调用方式并不是app.decorator,而是$provide.decorator,因为angular的模块对象并没有提供decorator方法。


    服务注册的本质

    上面我们都是使用app.provider()app.factory()等模块对象的方法注册服务,只有decorator例外,用的是$provide.decorator()方法。

    我想说的是:真正的服务注册都是调用$provide这个服务提供者对应的同名方法。

    其实,之前在《指令》中就说过这个问题,就是app.directive()$compileProvider.directive()的关系,不清楚的可以看看。

    最后

    以上就是我对angular服务的理解,如果有误,欢迎留言纠正讨论~~

  • 相关阅读:
    浅析C# new 和Override的区别
    用流打开open office ods 文件
    两个自己写的合并GridView 行的方法
    TSQL 日期格式化
    页面刷新后滚动条定位
    解决 TextBox 的 ReadOnly 属性为 true 时,刷新页面后值丢失的方法
    Sql server 查询条件中将通配符作为文字使用
    window.open 弹出页面回写父页面值及触发父页面Button事件
    注册光标丢失的事件
    模态对话框对父页面控件回写值
  • 原文地址:https://www.cnblogs.com/lovesueee/p/4142069.html
Copyright © 2011-2022 走看看