在AngularJS應用起動前,它們以HTML文本的形式保存在文本編輯器中。應用啟動后會進行編譯和鏈接,作用域會同HTML進行綁定,應用可以對用戶在HTML中進行的操作進行實時響應。這個神器的效果是如何發生的?創建高效率的應用需要了解什么?
在這個過程中總共有兩個主要階段。
第一個階段是編譯階段。
在編譯階段,AngularJS會遍歷整個HTML文檔並根據JavaScript中的指令定義來處理頁面上聲明的指令。
每一個指令的模板中都可能含有另外一個指令,另外一個指令也可能會有自己的模板。當AngularJS調用HTML文檔根部的指令時,會遍歷其中所有的模板,模板中也可能包含帶有模板的指令。
模板樹可能又大又深,但有一點需要注意,盡管元素可以被多個指令所支持或修飾,這些指令本身的模板中也可以包含其他指令,但只有屬於最高優先級指令的模板會被解析並添加到模板樹中。這里有一個建議,就是將包含模板的指令和添加行為的指令分離開來。如果一個元素已經有一個含有模板的指令了,永遠不要對其用另一個指令進行修飾。只有具有最高優先級的指令中的模板會被編譯。
一旦對指令和其中的子模板進行遍歷或編譯,編譯后的模板會返回一個叫做模板函數的函數。我們有機會在指令的模板函數被返回前,對編譯后的DOM樹進行修改。 在這個時間點DOM樹還沒有進行數據綁定,意味着如果此時對DOM樹進行操作只會有很少的性能開銷。基於此點,ng-repeat和ng-transclude等內置指令會在這個時候,也就是還未與任何作用域數據進行綁定時對DOM進行操作。
以ng-repeat為例,它會遍歷指定的數組或對象,在數據綁定之前構建出對應的DOM結構。
如果我們用ng-repeat來創建無序列表,其中的每一個<li>都會被ng-click指令所修飾,這個過程會使得性能比手動創建列表要快得多,尤其是列表中含有上百個元素時。與克隆<li>元素,再將其與數據進行鏈接,然后對每個元素都循環進行此操作的過程不同,我們僅需要先將無需列表構造出來,然后將新的DOM(編譯后的DOM)傳遞給指令生命周期中的下一個階段,即鏈接階段。一個指令的表現一旦編譯完成,馬上就可以通過編譯函數對其進行訪問,編譯函數的簽名包含有訪問指令聲明所在的元素(tElemente)及該元素其他屬性(tAttrs)的方法。這個編譯函數返回前面提到的模板函數,其中含有完整的解析樹。這里的重點是,由於每個指令都可以有自己的模板和編譯函數,每個模板返回的也都是自己的模板函數。鏈條頂部的指令會將內部子指令的模板合並在一起成為一個模板函數並返回,但在樹的內部,只能通過模板函數訪問其所處的分支。最后,模板函數被傳遞給編譯后的DOM樹中每個指令定義規則中指定的鏈接函數,
compile(對象或函數)
compile選項可以返回一個對象或函數。理解compile和link選項是AngularJS中需要深入討論的高級話題之一,對於了解AngularJS究竟是如何工作的至關重要。compile選項本身並不會被頻繁使用,但是link函數則會被經常使用。本質上,當我們設置了link選項,實際上是創建了一個postLink()鏈接函數,以便compile()函數可以定義鏈接函數。通常情況下,如果設置了compile函數,說明我們希望在指令和實時數據被放到DOM中之前進行DOM操作,在這個函數中進行諸如添加和刪除節點等DOM操作是安全的。
compile和link選項是互斥的。如果同時設置了這兩個選項,那么會把compile所返回的函數當作鏈接函數,而link選項本身則會被忽略。
1 // ... 2 compile: function(tEle, tAttrs, transcludeFn) { 3 var tplEl = angular.element('<div>' + 4 '<h2></h2>' + 5 '</div>'); 6 var h2 = tplEl.find('h2'); 7 h2.attr('type', tAttrs.type); 8 h2.attr('ng-model', tAttrs.ngModel); 9 h2.val("hello"); 10 tEle.replaceWith(tplEl); 11 return function(scope, ele, attrs) { 12 // 連接函數 13 }; 14 } 15 //...
如果模板被克隆過,那么模板實例和鏈接實例可能是不同的對象。因此在編譯函數內部,我們只能轉換那些可以被安全操作的克隆DOM節點。不要進行DOM事件監聽器的注冊:這個操作應該在鏈接函數中完成。
編譯函數負責對模板DOM進行轉換。
鏈接函數負責將作用域和DOM進行鏈接。
在作用域同DOM鏈接之前可以手動操作DOM。在實踐中,編寫自定義指令時這種操作是非常罕見的,但有幾個內置指令提供了這樣的功能。了解這個流程對於理解AngularJS真正的工作方式很有幫助。
鏈接
用link函數創建可以操作DOM的指令。鏈接函數是可選的。如果定義了編譯函數,它會返回鏈接函數,因此當兩個函數都定義了時,編譯函數會重載鏈接函數。如果我們的指令很簡單,並且不需要額外的設置,可以從工廠函數(回調函數)返回一個函數來代替對象。如果這樣做了,這個函數就是鏈接函數。
下面兩種定義指令的方式在功能上是完全一樣的:
1 angular.module('myApp', []) 2 .directive('myDirective', function() { 3 return { 4 pre: function(tElement, tAttrs, transclude) { 5 // 在子元素被鏈接之前執行 6 // 在這里進行Don轉換不安全 7 // 之后調用'lihk'h函數將無法定位要鏈接的元素 8 }, 9 post: function(scope, iElement, iAttrs, controller) { 10 // 在子元素被鏈接之后執行 11 // 如果在這里省略掉編譯選項 12 //在這里執行DOM轉換和鏈接函數一樣安全嗎 13 } 14 }; 15 }); 16 angular.module('myApp', []) 17 .directive('myDirective', function() { 18 return { 19 link: function(scope, ele, attrs) { 20 return { 21 pre: function(tElement, tAttrs, transclude) { 22 // 在子元素被鏈接之前執行 23 // 在這里進行Don轉換不安全 24 // 之后調用'lihk'h函數將無法定位要鏈接的元素 25 }, 26 post: function(scope, iElement, iAttrs, controller) { 27 // 在子元素被鏈接之后執行 28 // 如果在這里省略掉編譯選項 29 //在這里執行DOM轉換和鏈接函數一樣安全嗎 30 } 31 } 32 } 33 });
當定義了編譯函數來取代鏈接函數時,鏈接函數是我們能提供給返回對象的第二個方法,也就是postLink函數。本質上講,這個事實正說明了鏈接函數的作用。它會在模板編譯並同作用域進行鏈接后被調用,因此它負責設置事件監聽器,監視數據變化和實時的操作DOM。
link函數對綁定了實時數據的DOM具有控制能力,因此需要考慮性能問題。回顧一下10.4節中關於性能的考慮,在選擇是用編譯函數還是鏈接函數實現功能時,將性能影響考慮進去。
鏈接函數的簽名如下:
link: function(scope, element, attrs) {// 在這里操作DOM}
如果指令定義中有require選項,函數簽名中會有第四個參數,代表控制器或者所依賴的指令的控制器。
1 // require 'SomeController', 2 link: function(scope, element, attrs, SomeController) { 3 // 在這里操作DOM,可以訪問required指定的控制器 4 }
如果require選項提供了一個指令數組,第四個參數會是一個由每個指令所對應的控制器組成的數組。
下面看一下鏈接函數中的參數:
scope:指令用來在其內部注冊監聽器的作用域。
iElementiElement:參數代表實例元素,指使用此指令的元素。在postLink函數中我們應該只操作此元素的子元素,因為子元素已經被鏈接過了。
iAttrsiAttrs:參數代表實例屬性,是一個由定義在元素上的屬性組成的標准化列表,可以在所有指令的鏈接函數間共享。會以JavaScript對象的形式進行傳遞。
controller:controller參數指向require選項定義的控制器。如果沒有設置require選項,那么controller參數的值為undefined。
控制器在所有的指令間共享,因此指令可以將控制器當作通信通道(公共API)。如果設置了多個require,那么這個參數會是一個由控制器實例組成的數組,而不只是一個單獨的控制器。