1. 程式人生 > >[AngularJS面面觀] 16. 依賴注入 --- 注入器中如何管理物件

[AngularJS面面觀] 16. 依賴注入 --- 注入器中如何管理物件

上一篇文章初次介紹了注入器(Injector),分析了它載入模組的過程以及它是如何執行任務佇列的。這裡需要重申一下的是,所謂任務佇列實際上就是我們在開發一個基於angular的應用時定義的那些constantservicefactory等等,它們通過module型別提供的方法定義,但是定義並不代表立即就建立。它們的建立工作是交給注入器來完成的。

那麼執行了這些任務後,會產生什麼效果,這又和我們討論的主題-依賴注入有什麼關聯呢?這就是我們在這篇文章中需要討論的。

依賴注入總覽

在繼續討論之前,我們需要看看依賴注入到底在angular應用中意味著什麼。眾所周知,在開發一個angular應用時,我們可以直接將需要的服務以引數的形式定義在所定義的各種angular提供的型別中,比如controller

servicefactory等等,下面是一段典型程式碼:

angular.module('test').controller('testController', function($rootScope, testConstant, testFactory) {
  // 利用$rootScope, testConstant, testFactory完成業務邏輯
}); 

在感受到這種業務程式碼編寫方式便利性的時候,你有沒有感覺到哪怕是一絲半毫的不可思議呢?我們需要的各種服務和資料為什麼就能夠以這種直接定義成引數的形式得以實現呢?

angular框架是如何得知上述引數中定義的$rootScope

, testConstant, testFactory是從何而來呢?如果我們把它們換個名字,比如換成$rootScope1, testConstant1, testFactory1,還能不能得到相同的結果呢?

帶著這些問題,我們來看看依賴注入是如何解決這個問題的。所謂依賴注入,實際上是控制反轉(Inverse of Control)設計思想的一種具體模式。而控制反轉的核心思想等同於所謂的”好萊塢原則”:”不要打電話給我們,我們會打電話給你”。套用在依賴注入的上下文中,這句話就演繹成了:”不要去主動尋找和建立需要的服務,給個名字交給注入器幫你搞定”。因此在這個前提條件下,才有了我們在前述程式碼中所看到的那樣,我們在引數列表中聲明瞭我們需要的服務和資料,注入器真的就幫我們搞定了。而且上述controller

定義的function並不是由應用程式來呼叫,它是通過angular框架進行呼叫,這樣才能夠將真正的引數傳入進去。從這個角度而言,它也實踐了控制反轉這一原則。作為應用程式開發者的你,只需要根據規範定義好業務邏輯即可,後續的一切工作全部交給框架處理。

弄明白了這個問題,剩下的問題就變成了:注入器是如何搞定的呢?我們只提供了一個名字,它就搞定了?感覺很神奇吧,其實我們在前面已經給出了這個問題的答案 — 任務佇列。

執行任務佇列的目的

執行任務佇列的目的有兩點:
1. 為了建立這些定義的資料(比如簡單的一點的constantvalue等)。這類資料往往比較簡單,即使直接創建出來也佔用不了太多資源。
2. 為了提供給注入器如何建立服務的”藍圖”(比如複雜一點的如controllerservicefactory等),這樣做的目的是為了實現”懶載入”,因為這類物件往往會比較複雜,如果在注入器在載入模組的時候就一股腦地將它們全部給創建出來了,然而應用中卻沒有使用它們,豈不是很虧?

注入器如何管理物件

然後在需要某個物件的時候,注入器會首先來看它是不是已經存在了。如果存在的話就直接返回,如果不存在就會先建立,然後儲存到快取中並返回。相關程式碼如下所示:

// 所有被注入器管理的物件快取
var instanceCache = {};

// 獲取服務的函式,函式體中的cache就是上面的instanceCache
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陣列中,記錄例項化服務的順序
      path.unshift(serviceName);
      // 將服務標註為"正在例項化"
      cache[serviceName] = INSTANTIATING;
      // 呼叫factory來例項化得到服務物件 --- 此時才真正得到了服務物件
      return cache[serviceName] = factory(serviceName, caller);
    } catch (err) {
      // 發生異常時,清除例項化出錯的服務
      if (cache[serviceName] === INSTANTIATING) {
        delete cache[serviceName];
      }
      throw err;
    } finally {
      // 清除最後的路徑資訊
      path.shift();
    }
  }
}

如註釋所解釋的那樣,以上的程式碼主要乾了這麼幾件事情:
1. 判斷是否發生迴圈依賴,如果發生了是要丟擲異常的
2. 被注入器託管物件的管理 — 建立和獲取

迴圈依賴的檢測

對於第一點,迴圈依賴的問題。相信有一些實際angular開發經驗的同學們一定已經遇到過了。下面這段程式碼重現了這個問題:

<html ng-app="test">
<head>
    <title>Angular Circular Dependency Example</title>
</head>
<body ng-controller="testController">
    Test
</body>
<script src="//cdn.bootcss.com/angular.js/1.5.8/angular.js"></script>
<script type="text/javascript">
    var module = angular.module('test', []);

    module.service('service1', function(service2) {});
    module.service('service2', function(service1) {});

    module.controller('testController', function(service1) {});
</script>
</html>

上面定義的兩個service互相依賴於對方。但是根據注入器”懶載入”的特性,如果僅僅定義了兩個service而不定義在哪使用它們的話,也是不會觸發注入器的例項化操作的。因此還定義了一個controller,並在body元素上聲明瞭使用該控制器,用來觸發注入器例項化的行為,進而觸發我們所期待的迴圈依賴異常。

如果執行這個例子就會出現下面的異常:

angular.js:13920 Error: [$injector:cdep] Circular dependency found: service1 <- service2 <- service1
http://errors.angularjs.org/1.5.8/$injector/cdep?p0=service1%20%3C-%20service2%20%3C-%20service1
    at angular.js:68
    at getService (angular.js:4656)
    at injectionArgs (angular.js:4688)
    at Object.instantiate (angular.js:4730)
    at Object.<anonymous> (angular.js:4573)
    at Object.invoke (angular.js:4718)
    at Object.enforcedReturnValue [as $get] (angular.js:4557)
    at Object.invoke (angular.js:4718)
    at angular.js:4517
    at getService (angular.js:4664)

值得一提的是,上面的示例程式中使用的是未經過壓縮混淆的angular原始碼。如果你使用的是壓縮混淆過的angular.min.js。輸出就不會這麼詳盡了:

Error: [$injector:cdep] http://errors.angularjs.org/1.5.8/$injector/cdep?p0=service1%20%3C-%20service2%20%3C-%20service1
    at Error (native)
    at http://cdn.bootcss.com/angular.js/1.5.8/angular.min.js:6:412
    at d (http://cdn.bootcss.com/angular.js/1.5.8/angular.min.js:40:349)
    at e (http://cdn.bootcss.com/angular.js/1.5.8/angular.min.js:41:158)
    at Object.instantiate (http://cdn.bootcss.com/angular.js/1.5.8/angular.min.js:42:24)
    at Object.<anonymous> (http://cdn.bootcss.com/angular.js/1.5.8/angular.min.js:42:352)
    at Object.invoke (http://cdn.bootcss.com/angular.js/1.5.8/angular.min.js:41:456)
    at Object.$get (http://cdn.bootcss.com/angular.js/1.5.8/angular.min.js:39:142)
    at Object.invoke (http://cdn.bootcss.com/angular.js/1.5.8/angular.min.js:41:456)
    at http://cdn.bootcss.com/angular.js/1.5.8/angular.min.js:43:265

大家可以對比一下輸出的不同。除了呼叫棧不同之外,前者還多了迴圈依賴的詳細資訊:

Circular dependency found: service1 <- service2 <- service1

而以上迴圈依賴的資料來源正是path陣列。因此,在開發一個angular應用的時候,也建議大家使用angular.js,而非angular.min.js。因為在發生異常時前者能夠提供更多的資訊。關於angular異常的封裝,可以參考我的另外一篇文章,對這個問題進行了探討。

發生迴圈依賴的本質還是在於注入器在例項化服務物件的時候,採用的演算法也是深度優先遍歷,這一點在原理上和注入器處理模組的載入是別無二致的。因為一個服務物件也可能需要首先依賴更多的其它服務物件,這樣逐層深入下去,就成了一張服務物件依賴關係圖。這張單向圖需要是一張有向無環圖(DAG),否則就無法解釋到底是誰依賴誰了。因此,這也算是拓撲排序演算法在angular中的一個簡單應用吧。關於拓撲排序,有興趣的同學還可以看這篇文章,歡迎大家來探討。

被託管物件的建立和獲取

關於第二點,注入器對被託管物件的建立和獲取。
就建立而言,分為簡單物件和複雜物件。像諸如constantvalue這樣的簡單物件,直接定義到快取中即可。對於複雜物件,呼叫通過執行任務佇列得到的”藍圖”即可,也就是getService方法中的factory函式。就獲取而言,它和建立其實是相輔相成的,建立了之後才能獲取。

結語

本篇文章介紹了依賴注入的原理,以及angular是如何實踐這一原理的。當然,這還沒完。現在介紹的只是angular的注入器是如何管理被託管物件的,離實際應用還差了一點料。這個料就是下一篇文章的主題 — 註解的定義與實現。正是這個料最終促成了angular中依賴注入服務$injector的誕生。