1. 程式人生 > >[AngularJS面面觀] 21. 依賴注入 --- constant到底如何而來

[AngularJS面面觀] 21. 依賴注入 --- constant到底如何而來

在上一篇文章中,我們終於見到了angular中依賴注入的總體結構圖。從這幅圖中我們可以知道在angular內部是有兩個注入器協同工作來實現我們習以為常的依賴注入特性的。

angular注入器工作流程和原理

結合上圖簡單回顧一下angular依賴注入的組成和工作流程。

首先,在臺面上的注入器名為例項注入器(Instance Injector),它裡面含有一個名為例項快取(Instance Cache)的字典物件,該快取的作用是儲存被託管的物件,每個被注入器例項化得到的物件都會被儲存在其中。所謂的依賴注入,實際上就是從例項注入器的快取中拿去我們需要的物件。當然,凡事都有第一次,當我們需要的物件並不在該快取中時,也就是說該物件還沒有被例項化。那麼這個時候例項注入器就要像provider注入器求援了。因為在這個provider注入器中儲存的都是用於例項化物件的”菜譜”,而這些”菜譜”就定義在了每個provider

物件的$get方法中。因此,呼叫該物件的$get方法,例項注入器就能夠獲取到需要的物件,接下來就是儲存該物件到例項注入器的快取中並將該物件注入到需要它的地方。

基於provider的高層API

然而,我們在真正地應用angular框架來完成我們的業務邏輯時,直接使用$injector以及定義各種provider並非不行,而是太底層了。所以angular給我們封裝了各種各樣的服務,比如factoryservicecontrollervalueconstant等等。別看它們一個個名字洋氣得很,其實萬變不離其宗,在幕後都有一個provider在默默的支援著。所以,從本文開始我們將系統地討論這些封裝好了的服務,揭開它們華麗的外衣,還原其本質。既然提到了這些服務本質上都是基於provider

的,所以首先你就應該搞清楚provider是怎麼一回事,可以參考這篇文章依賴注入 — Provider是個啥,裡面對provider作出了一些介紹。

constant的一生

按照慣例,還是先挑軟柿子捏,最簡單的服務非constant莫屬。在前面的文章中,我們也一直拿constant作為例子來討論依賴注入的工作原理和實現細節。但是一直都沒有正兒八經地看看constant是如何實現的。所以,我們就先來看看constant的一生:它是如何被定義,如何被建立,又是如何被注入的。

定義

我們都知道要宣告constant,使用的就是module.constant方法:

constant: invokeLater('$provide'
, 'constant', 'unshift') function invokeLater(provider, method, insertMethod, queue) { if (!queue) queue = invokeQueue; // 還是利用柯里化將多個引數的函式轉換為少數引數的函式 return function() { // arguments才是我們在宣告constant時實際傳入的引數 queue[insertMethod || 'push']([provider, method, arguments]); return moduleInstance; }; } // 將上面程式碼還原一下,constant的實際定義是這樣的: constant: function() { invokeQueue['unshift'](['$provide', 'constant', arguments]); return moduleInstance; }

這裡仍然使用了JavaScript中一個被經常使用的模式,即函式的”柯里化”,它的主要作用是減少函式的引數數量,是一個函數語言程式設計中經常會被使用到的模式。

所以對於constant的定義,就是往任務佇列裡面增加一條記錄。只不過,它這裡用到的insertMethodunshift而並非預設的push,這一點值得留意,它將constant的定義放在了任務佇列的頭部。所以不管應用程式是以何種順序來定義constant的,當注入器載入模組的時候總是會優先執行代表constant的任務。為何需要優先執行constant任務呢?其實原因很簡單:因為constant很簡單不是嘛。它又不需要依賴別的被託管物件,因此一開始就執行它們準沒錯!

那麼我們來看看當執行任務佇列的時候會發生什麼,這段程式碼我們已經提過很多次了,算是注入器實現中的核心程式碼:

// 執行模組中的任務佇列
runInvokeQueue(moduleFn._invokeQueue);

// 對照定義constant的引數:(['$provide', 'constant', arguments])
// invokeArgs[0]: '$provide'
// invokeArgs[1]: 'constant':
// invokeArgs[2]: 類陣列物件arguments
function runInvokeQueue(queue) {
  var i, ii;
  // 以此從任務佇列中拿到任務,然後拿到對應的provider並進行呼叫
  for (i = 0, ii = queue.length; i < ii; i++) {
    var invokeArgs = queue[i],
        provider = providerInjector.get(invokeArgs[0]);

    provider[invokeArgs[1]].apply(provider, invokeArgs[2]);
  }
}

將定義constant所使用到的引數代入到上述for迴圈中,可以得到這麼一段:

// 假設我們定義了一個constant如下所示:
module.constant('a', 'aConstant');

// 代入到任務執行階段:
var invokeArgs = ['$provide', 'constant', ['a', 'aConstant']],
      provider = providerInjector.get('$provide');

    provider['constant'].apply(provider, ['a', 'aConstant']);

注意其中的['a', 'aConstant']並不是一個真正的陣列物件,它是一個由arguments所代表的類陣列物件(Array-like Object),關於類陣列物件的定義,可以參考MDN對於它的定義

因此從這裡我們就可以很明確地發現,負責提供constant這一菜譜的正是$provide.constant方法。也就是說,我們對constant的定義最後會被$provide.constant('a', 'aConstant')所落實。那讓我們看看這一方法又做了些什麼工作:

// $provide直接被定義到了provider注入器的快取中
providerCache = {
  $provide: {
      constant: supportObject(constant),
      // ......
    }
}

function supportObject(delegate) {
  return function(key, value) {
    if (isObject(key)) {
      forEach(key, reverseParams(delegate));
    } else {
      return delegate(key, value);
    }
  };
}

function constant(name, value) {
  // 確保常量的名字不叫做'hasOwnProperty'
  assertNotHasOwnProperty(name, 'constant');

  // 將常量直接定義到provider注入器和instance注入器的快取中
  providerCache[name] = value;
  instanceCache[name] = value;
}

function assertNotHasOwnProperty(name, context) {
  if (name === 'hasOwnProperty') {
    // 丟擲badname異常
    throw ngMinErr('badname', "hasOwnProperty is not a valid {0} name", context);
  }
}

可以發現,$provide.constant方法又被一個名為supportObject的函式給包裝了一下。框架就是這樣的,為了增加程式碼的複用性,總是會將一個函式層層包裝,來達到最終的目的。其實這個supportObject函式做的事情也很簡單,只是對key引數為物件的情況進行特殊處理。這個我們暫且不深究,等遇到了再作分析不遲。如果key不是物件,那麼就直接交給delegate進行處理。放到constant這個上下文中,delete就是上述程式碼中的constant函式。

constant函式也只有寥寥三行程式碼。第一行確保constant的名字不為hasOwnProperty,因為hasOwnProperty本身就是JavaScript所有物件都擁有的一個方法,通過該方法可以判斷一個物件是否擁有某個欄位或者方法。所以不能將constant命名為這個名字,否則有可能會覆蓋物件中的同名方法導致程式出現異常。

第二行和第三行做的事情就更簡單直白了。將constant對應的value直接放入到provider注入器和instance注入器的快取中。因此不僅在諸如factoryservice等服務中我們可以宣告constant依賴,在provider的構造器宣告方式中也能夠宣告constant作為依賴,前文在介紹provider的時候討論過如下一段程式碼:

function provider(name, provider_) {
  assertNotHasOwnProperty(name, 'service');
  if (isFunction(provider_) || isArray(provider_)) {
    // provider的例項化由provider注入器完成,並非由instance注入器完成
    provider_ = providerInjector.instantiate(provider_);
  }
  if (!provider_.$get) {
    throw $injectorMinErr('pget', "Provider '{0}' must define $get factory method.", name);
  }
  return providerCache[name + providerSuffix] = provider_;
}

從程式碼中可以得知,provider的例項化由provider注入器完成,並非由instance注入器完成。所以在provider的建構函式中宣告的依賴只能是儲存在provider注入器的快取中存在的依賴。也就是說,provider可以依賴於provider注入器快取中的物件,也就是各種provider以及constant,但不能依賴其它存在於例項注入器快取中的factoryservice等等。畢竟二者的抽象層次不再一個級別上。provider作為factoryservice等服務的生產者,可以看作是它們的”長輩”,”長輩”之間可以依賴,但是”長輩”不可以依賴它們的”晚輩”,是不是很有”長輩”的範。

所以,constant雖然簡單,但是也有其特殊性,即兩個注入器都會保留一份常量的例項,下面兩種注入方式都是可行的:

// 在provider的構造器函式中直接宣告常量依賴
module.provider('b', function BProvider(a) {
  this.$get = function() {
    return 'constant: ' + a;
  };
});

// 在service中宣告常量依賴
module.service('aService', function(a) {
  // ......
});

// 定義在最後也沒關係:別忘了常量任務會通過unshift操作放到任務佇列的頭部
module.constant('a', 'aConstant');

通過上面的分析,想必對依賴注入和angular注入器內部的實現方式有更深入的瞭解了吧。在後續的文章中,會繼續分析定義在module中的各種我們在開發angular應用時經常會使用到的方法。