AngularJS 1.0 源码学习笔记(二)- 依赖注入和module加载

最近创业的公司要上线新的产品功能,一直没能有机会静下来好好再继续Angularjs的源码阅读计划

这两天终于是找了点机会拜读完了Angularjs中最根基的几个部分之一 – 也就是Angularjs的依赖注入功能。

依赖注入(Dependency Injection)是一种常用的控制反转的设计原则。基本思想就是当我的代码需要某一个功能对象时,一个全局的调控对象会把相应的对象调用给我。

而我不需要去关心这个东西是什么时候创建的,生命周期又是如何被控制的,这都不关我的事情。打个比方,当我想看电视时,我只要优雅的葛优瘫在沙发上,说着,给我把遥控器拿来,那么就会有这么一个勤勤恳恳的人替我装好电池,调好频段,然后把遥控器交到我的手里。

而这个任劳任怨的家伙在Angular里面就是一个叫做$inject的Service.

不过在讲$inject之前,我们首先需要了解一下Angularjs的大家都很熟悉的angular.module系列函数.

angular.module的运作过程

angular.module就像它本身的名字一样,它是用来管理angular中的各个模块的函数。它具备了两个功能

  1. 创建新的module instance
  2. 获得已有的module instance并对其进行设置,比如创建service,运行Run block等等..

为了了解它具体是什么工作的,我们需要阅读源码中的 src/loader.js (注:本系列的链接都为我本人fork中进行自行中文注释的源码)。

module创建的过程

  1. 使用angular.module系列函数进行初始化
  2. 被依赖时进行加载,实例化provider,运行config
  3. 运行run函数中的内容

获取和初始化

angular.module实际上是一个绑定在window上面的全局函数,在创建时,在它的闭包中创建了一个变量modules,用来存储所有的已经加载完成的module.

当我们调用angular.module时,angular会首先根据我们给的module名字检查在window.angular.module的闭包变量modules中有没有已经初始化过这个module。如果已经初始化,那么直接返回这个module对象,如果没有,那么就会以我们传入的第二个参数(通常是一个数组),对它进行初始化。

在这个文件中有一个ensure函数被用了很多次,其实是一个很简单的懒加载的函数,但是很实用

接下来重点就是要看一下创造module的factory函数.

首先它会检查是否存在第二个参数, 也就是这个module的依赖module。 这里如果我们在有初始化这个module的同时,又在使用angular.module来获取它时没有传第二个参数,angular就会报错,说这个module不存在。

因此在使用一个module之前,一定要先调用一次添加第二个参数的方法来初始化它!

执行队列和配置接口

在初始化module的过程当中,angular也对一些常用的配置接口进行了初始化,这些接口就是我们常用的factory, service, controller等等用于实现我们angular程序核心业务功能的接口。

从源码中我们可以看到,当我们调用这些接口时,实际上我们所传入的函数并不是马上执行用于初始化相关的service的,而是通过一些函数将这些用于配置业务的任务放入了一个执行队列中。 它们是通过invokeLater, invokeLaterAndSetModuleName实现的。

module存在有三个执行队列,分别是invokeQueue,configBlocks和runBlocks。一个module在未被其他module依赖或者是被angular app本身bootstrap之前,所有通过调用配置接口(factory, service, controller等)所定义的操作并不会马上执行,而是会被加入这些队列中(会根据不同的方法加入不同的队列,而且是加入头部和尾部也有相应区别,具体可以参照源码,在此就不一一给出了)。

这么做的原因是因为在module刚刚初始化后调用这些函数时,此时往往angular还没有进行bootstrap(angular 调用bootstrap 进行 init的的时机是在 document.ready之后,并且要求存在ng-app的元素)。而执行这些函数内初始化的操作所需要的一些关键基础的provider,比如$provide,都是存在于angularjs的核心module ng模块中。ng模块会在进行bootstrap时被自动加入当前module的依赖的第一个,因此会是第一个被加载的模块。

对于上面所说的三个队列,invokeQueue,configBlock会在module被初始化时立即执行,而runBlock里的操作则将会在所有module需要加载的module加载完成之后再执行。

加载过程

(以上代码位于src/auto/injector.js中)

当module A 被 module B 所依赖时,angular在加载module B时,发现需要加载moduleA。此时如果module A已经被加载,那么会直接返回,如果还没有,那么angular就会按照如下的顺序进行初始化

  1. 加载module A的依赖项(按照这里所说的步骤),加载完成后将获得的runblock加到当前runblock的尾端,然后再加上A的runblock
  2. 执行invokeQueue,这时provider会被实例化
  3. 执行configBlocks, 运行所有对provider的配置项
  4. 返回当前的runblock

因此angular的module加载过程就是根据依赖的关系,进行一系列的provider实例化和运行相关config的过程。

在所有的module都被加载完成之后,会返回一个把所有的module中的runblock连接起来的大runblock(其中的排序则根据module间的依赖关系排列,被依赖的在前)。

此时,module就会执行runBlock中的内容,将需要执行的命令顺序执行一遍,而此时由于provider都已经初始化完成,则可以传入一些实例依赖比如service等等。

依赖注入的实现方法 —— angular.inject

Angular 的依赖注入主要是由 injector实例来控制的,而一般来说一个ng-app只会在bootstrap时创建一个injector, 对于这个ng-app来说,injector和其他service一样是一个单例.

同时,injector也和其他service一样能够被依赖,在创建injector的途中,首先被创建的的就是$injector service.

injector相关的函数都位于src/auto/injector.js,其中有一大段关于inject使用的注释,而这些大部分在angular js的官方文档上有,我稍微翻译了一些,一些就没管,没多大意思。

我们主要需要看的就是createInjector这个函数。

基本上整个inject的文件都是这个函数。 createInjector顾名思义,就是创建一个injector。它在ng-app进行bootstrap时调用。

可以看到,在创建injector的过程中,首先先创建了两个内部的injector:

  • providerInjector 用于注入provider
  • protoInstanceInjector 用于注入实例service

同时也创建了他们对应的cache:

  • providerCache 用于保存已经实例化的provider
  • instanceCache 用于保存已经实例化的service

从这种区分来看, provider和service所得到的待遇是完全不同的,而更细节的东西我们可以从两个内部injector创建时的不同中看出

这里createInternalInjector函数就是他们的创建用的函数,第一个参数为cache,第二个为创建内部成员的工厂函数。

当我们对一个通过createInternalInjector创造的injector请求一个内部成员时,

它会首先检查在cache中是否存在相应的成员,如果没有,就使用定义的factory创造一个。

从这里两个injector在创建时不同的factory我们可以看出,需要创建一个service,必定需要从providerInjector中取出相应的provider。而对于providerInjector,如果因为使用了一个还没放在providerCache里面的provider而调用factory函数时,就直接报错。

因此,所有的provider必须在创建相应service之前就完成实例化,并且实例化的过程的顺序有严格要求。这个过程由$provide.provider完成,并且在我们使用angualr.module(‘app’).provider时,相应的provider就已经进入实例化队列(上文说的module的invokeQueue),因此其中使用的依赖provider必须在此之前就已经进入实例化队列(一般来说就是调用$provide.provider)

 

provider的实例化过程

在创建完这两个内部injector之后,此时providerInjector中只有$provide这么一个用于创建其他provider的provider(真拗口),而protoInstanceInjector里面则是什么都没有。

接下来我们就要进行相关provider的实例化。

而这个过程就像前文所说,会在module初始化的过程中完成。由于createInjector是在bootstrap中调用,此时要加载的module便是ng-app所对应的module.

现在我们可以来看一下loadModules这个函数。

这个函数会根据module的依赖关系进行递归调用,最终完成所有module的加载并返回最终要执行的runBlocks。

这个过程我们已经在之前module的加载中讲过,因此我们把目光聚焦在之前所说的invokeQueue和configBlock是如何被执行的。

还记得我们之前说过的invokeLater函数么?这里的runInvokeQueue就解释了它的秘密。

为了方便解释我们就以module.factory举例:

当我们调用module.factory(blablabla)时,实际上就是把[‘$provide’, ‘factory’,arguments(blablabla)]加入了invokeQueue, 当这里在运行runInvokeQueue时,

它首先使用providerInjector.get(‘$provide’) 获取到准备好的$provide服务,然后以我们刚刚的blablabla作为参数执行$provide[‘factory’],最终就创建了相应的provider。

看到这里有些聪明的小伙伴可能就要问了,$provide.factory函数不是用来创建service实例的吗?

然而实际上$provide.factory是用来帮助我们创建service实例的,它实际上创建的是一个serviceName+’Provider’的provider实例,其中的$get方法就是我们传入的函数。

关于这一点,我们之后在剖析$provide服务时,可能大家会有更直观的理解。

回到正题,在runInvokeQueue时,我们才真正地根据我们之前对angular.module中调用,将相应的provider进行了实例化。

 

service的实例化过程

那么真正Service的实例化是在什么时候呢?答案是:当它被别的service或者runBlock需要的时候。

整个angular的injector的思想,便是懒加载一切。(懒得可以)

关于这个思想,我们可以观察一下createInternalInjector中的invoke和 injectionArgs函数:

结合之前给出的protoInstanceInjector定义的factory函数(会使用provider和上面的invoke函数),可以看出它是一个懒加载的过程。

当实例化Service A需要Service B时,如果B没被实例化,就会先实例化B,而如果这时候B正好又需要C,和D,就回去实例化C和D,如此继续下去,

就形成了一个依赖链。(终于知道错误信息里面unkownProvider :C <-B<-A是什么鬼了吧? )

最快的Service实例化时机就是在loadModule完成之后,执行的runblock中。其他则是在各种情况下被需要的时候,比如controller,directive等等。

 

$provide服务

最后要来说一下$provide这个服务。$provide是完成injector最基本功能的关键,它是用于创建provider的关键角色。

要注意的是,虽然$provide.factory, $provide.service 看似是创建了一个service实例,但是它实际上创建的是provider。

此处以factory为例(同时附上$provide.provider函数)

这里可以看到,factory函数最终还是调用了provider,实际上是包裹了一个$get函数为我们传入参数的的provider,并且这个provider的名字就是serviceName+’Provider’。

它所定义的service和其他service一样,当没有被使用时,并不会进行实例化。

发表评论