AngularJS指令中的compile與link函數解析
通常大家在使用
ng
中的指令的時候,用的連結函數最多的是
link
屬性,下面這篇文章将告訴大家
complie
,
pre-link
,
post-link
的用法與差別.
原文位址
angularjs
裡的指令非常神奇,允許你建立非常語義化以及高度重用的元件,可以了解為
web components
的先驅者.
網上已經有很多介紹怎麼使用指令的文章以及相關書籍,互相比較的話,很少有介紹
compile
與
link
的差別,更别說
pre-link
與
post-link
了.
大部分教程隻是簡單的說下
compile
會在
ng
内部用到,而且建議大家隻用
link
屬性,大部分指令的例子裡都是這樣的
這是非常不幸的,因為正确的了解這些函數的差別會提高你對
ng
内部運作機理的了解,有助于你開發更好的自定義指令.
是以跟着我一起來看下面的内容一步步的去了解這些函數是什麼以及它們應該在什麼時候用到
- 本文假設你已經對指令有一定的了解了,如果沒有的話強烈建議你看看這篇文章AngularJS developer guide section on directives
NG中是怎麼樣處理指令的
開始分析之前,先讓我們看看
ng
中是怎麼樣處理指令的.
當浏覽器渲染一個頁面時,本質上是讀
html
辨別,然後建立
dom
節點,當
dom
樹建立完畢之後廣播一個事件給我們.
當你在頁面中使用
script
标簽加載
ng
應用程式代碼時,
ng
監聽上面的
dom
完成事件,查找帶有
ng-app
屬性的元素.
當找到這樣的元素之後,
ng
開始處理
dom
以這個元素的起點,是以假如
ng-app
被添加到
html
元素上,則
ng
就會從
html
元素開始處理
dom
.
從這個起點開始,
ng
開始遞歸查找所有子元素裡面,符合應用程式裡定義好的指令規則.
ng
怎樣處理指令其實是依賴于它定義時的對象屬性的,你可以定義一個
compile
或者一個
link
函數,或者用
pre-link
和
post-link
函數來代替
link
.
是以這些函數的差別呢?為什麼要使用它?以及什麼時候使用它呢?
帶着這些問題跟着我一步一步來解答這些迷團吧
一段代碼
為了解釋這些函數的差別,下面我将使用一個簡單易懂的例子
- 如果您有任何的問題,請不要猶豫趕緊在下面加上你的評論吧.
看看下面一段
html
标簽代碼
<level-one>
<level-two>
<level-three>
Hello
</level-three>
</level-two>
</level-one>
然後是一段
js
代碼
var app = angular.module('plunker', []);
function createDirective(name){
return function(){
return {
restrict: 'E',
compile: function(tElem, tAttrs){
console.log(name + ': compile');
return {
pre: function(scope, iElem, iAttrs){
console.log(name + ': pre link');
},
post: function(scope, iElem, iAttrs){
console.log(name + ': post link');
}
}
}
}
}
}
app.directive('levelOne', createDirective('levelOne'));
app.directive('levelTwo', createDirective('levelTwo'));
app.directive('levelThree', createDirective('levelThree'));
結果非常簡單:讓
ng
來處理三個嵌套指令,并且每個指令都有自己的
complile
,
pre-link
,
post-link
函數,每個函數都會在控制台裡列印一行東西來辨別自己.
這個例子能夠讓我們簡單的了解到
ng
在處理指令時,内部的流程
代碼輸出
下面是一個在控制台輸出結果的截圖
如果想自己試一下這個例子的話,請點選this plnkr,然後在控制台檢視結果.
分析代碼
第一個要注意的是這些函數的調用順序:
// COMPILE PHASE
// levelOne: compile function is called
// levelTwo: compile function is called
// levelThree: compile function is called
// PRE-LINK PHASE
// levelOne: pre link function is called
// levelTwo: pre link function is called
// levelThree: pre link function is called
// POST-LINK PHASE (Notice the reverse order)
// levelThree: post link function is called
// levelTwo: post link function is called
// levelOne: post link function is called
這個例子清晰的顯示出了
ng
在
link
之前編譯所有的指令,然後
link
要又分為了
pre-link
與
post-link
階段.
注意下,
compile
與
pre-link
的執行順序是依次執行的,但是
post-link
正好相反.
是以上面已經明确辨別出了不同的階段,但是
compile
與
pre-link
有什麼差別呢,都是相同的執行順序,為什麼還要分成兩個不同的函數呢?
DOM
為了挖的更深一點,讓我們簡單的修改一下上面的代碼,它也會在各個函數裡列印參數清單中的
element
變量
var app = angular.module('plunker', []);
function createDirective(name){
return function(){
return {
restrict: 'E',
compile: function(tElem, tAttrs){
console.log(name + ': compile => ' + tElem.html());
return {
pre: function(scope, iElem, iAttrs){
console.log(name + ': pre link => ' + iElem.html());
},
post: function(scope, iElem, iAttrs){
console.log(name + ': post link => ' + iElem.html());
}
}
}
}
}
}
app.directive('levelOne', createDirective('levelOne'));
app.directive('levelTwo', createDirective('levelTwo'));
app.directive('levelThree', createDirective('levelThree'));
注意下
console.log
裡的輸出,除了輸出原始的
html
标記基本沒别的改變.
這個應該能夠加深我們對于這些函數上下文的了解.
再次運作代碼看看
輸出
下面是一個在控制台輸出結果的截圖
假如你還想自己運作看看效果,可以點選this plnkr,然後在控制台裡檢視輸出結果.
觀察
輸出
dom
的結果可以暴露一些有趣的事情:
dom
内容在
compile
與
pre-link
兩個函數中是不一樣的
是以發生了什麼呢?
Compile
我們已經知道當
ng
發現
dom
建構完成時就開始處理
dom
.
是以當
ng
在周遊
dom
的時候,碰到
level-one
元素,從它的定義那裡了解到,要執行一些必要的函數
因為
compile
函數定義在
level-one
指令的指令對象裡,是以它會被調用并傳遞一個
element
對象作為它的參數
如果你仔細觀察,就會看到,浏覽器建立這個
element
對象時,仍然是最原始的
html
标記
- 在
中,原始ng
通常用來辨別dom
,是以我在定義template element
函數參數時就用到了compile
名字,這個變量指向的就是tElem
.template element
一旦運作
levelone
指令中的
compile
函數,
ng
就會遞歸深度周遊它的
dom
節點,然後在
level-two
與
level-three
上面重複這些操作.
Post-link
深入了解
pre-link
函數之前,讓我們來看看
post-link
函數.
- 如果你在定義指令的時候隻使用了一個
函數,那麼link
會把這個函數當成ng
來處理,是以我們要先讨論這個函數post-link
當
ng
周遊完所有的
dom
并運作完所有的
compile
函數之後,就反向調用相關聯的
post-link
函數.
dom
現在開始反向,并執行
post-link
函數,是以,在之前這種反向的調用看起來有點奇怪,其實這樣做是非常有意義的.
當運作包含子指令的指令
post-link
時,反向的
post-link
規則可以保證它的子指令的
post-link
是已經運作過的.
是以,當運作
level-one
指令的
post-link
函數的時候,我們能夠保證
level-two
和
level-three
的
post-link
其實都已經運作過了.
這就是為什麼人們都認為
post-link
是最安全或者預設的寫業務邏輯的地方.
但是為什麼這裡的
element
跟
compile
裡的又不同呢?
一旦
ng
調用過指令的
compile
函數,就會建立一個
template element
的
element
執行個體對象,并且為它提供一個
scope
對象,這個
scope
有可能是新執行個體,也有可能是已經存在,可能是個子
scope
,也有可能是獨立的
scope
,這些都得依賴指令定義對象裡的
scope
屬性值
是以當
linking
發生時,這個執行個體
element
以及
scope
對象已經是可用的了,并且被
ng
作為參數傳遞到
post-link
函數的參數清單中去.
- 我個人總是使用
名稱來定義一個iElem
函數的參數,并且它是指向link
執行個體的element
是以
post-link
(
pre-link
)函數的
element
參數對象是一個
element
執行個體而不是一個
template element
.
是以上面例子裡的輸出是不同的
Pre-link
當寫了一個
post-link
函數,你可以保證在執行
post-link
函數的時候,它的所有子級指令的
post-link
函數是已經執行過的.
在大部分的情況下,它都可以做的更好,是以通常我們都會使用它來編寫指令代碼.
然而,
ng
為我們提供了一個附加的
hook
機制,那就是
pre-link
函數,它能夠保證在執行所有子指令的
post-link
函數之前.運作一些别的代碼.
這句話是值得反複推敲的
pre-link
函數能夠保證在
element
執行個體上以及它的所有子指令的
post-link
運作之前執行.
是以它使的
post-link
函數反向執行是相當有意義的,它自己是原始的順序執行
pre-link
函數
這也意為着
pre-link
函數運作在它所有子指令的
pre-link
函數之前,是以完整的理由就是:
一個元素的
pre-link
函數能夠保證是運作在它所有的子指令的
post-link
與
pre-link
運作之前執行的.見下圖
回顧
如果我們回頭看看上面原始的輸出,就能清楚的認出到底發生了什麼:
// HERE THE ELEMENTS ARE STILL THE ORIGINAL TEMPLATE ELEMENTS
// COMPILE PHASE
// levelOne: compile function is called on original DOM
// levelTwo: compile function is called on original DOM
// levelThree: compile function is called on original DOM
// AS OF HERE, THE ELEMENTS HAVE BEEN INSTANTIATED AND
// ARE BOUND TO A SCOPE
// (E.G. NG-REPEAT WOULD HAVE MULTIPLE INSTANCES)
// PRE-LINK PHASE
// levelOne: pre link function is called on element instance
// levelTwo: pre link function is called on element instance
// levelThree: pre link function is called on element instance
// POST-LINK PHASE (Notice the reverse order)
// levelThree: post link function is called on element instance
// levelTwo: post link function is called on element instance
// levelOne: post link function is called on element instance
概要
回顧上面的分析我們可以描述一下這些函數的差別以及使用情況:
Compile 函數
使用
compile
函數可以改變原始的
dom
(
template element
),在
ng
建立原始
dom
執行個體以及建立
scope
執行個體之前.
可以應用于當需要生成多個
element
執行個體,隻有一個
template element
的情況,
ng-repeat
就是一個最好的例子,它就在是
compile
函數階段改變原始的
dom
生成多個原始
dom
節點,然後每個又生成
element
執行個體.因為
compile
隻會運作一次,是以當你需要生成多個
element
執行個體的時候是可以提高性能的.
template element
以及相關的屬性是做為參數傳遞給
compile
函數的,不過這時候
scope
是不能用的:
下面是函數樣子
/**
* Compile function
*
* @param tElem - template element
* @param tAttrs - attributes of the template element
*/
function(tElem, tAttrs){
// ...
};
Pre-link 函數
使用
pre-link
函數可以運作一些業務代碼在
ng
執行完
compile
函數之後,但是在它所有子指令的
post-link
函數将要執行之前.
scope
對象以及
element
執行個體将會做為參數傳遞給
pre-link
函數:
下面是函數樣子
/**
* Pre-link function
*
* @param scope - scope associated with this istance
* @param iElem - instance element
* @param iAttrs - attributes of the instance element
*/
function(scope, iElem, iAttrs){
// ...
};
Post-link 函數
使用
post-link
函數來執行業務邏輯,在這個階段,它已經知道它所有的子指令已經編譯完成并且
pre-link
以及
post-link
函數已經執行完成.
這就是被認為是最安全以及預設的編寫業務邏輯代碼的原因.
scope
執行個體以及
element
執行個體做為參數傳遞給
post-link
函數:
下面是函數樣子
/**
* Post-link function
*
* @param scope - scope associated with this istance
* @param iElem - instance element
* @param iAttrs - attributes of the instance element
*/
function(scope, iElem, iAttrs){
// ...
};
總結
現在你應該對
compile
,
pre-link
,
post-link
這此函數之間的差別有了清晰的認識了吧.
如果還沒有的話,并且你是一個認真的
ng
開發者,那麼我強烈建議你重新把這篇文章讀一讀直到你了解為止
了解這些概念非常重要,能夠幫助你了解
ng
原生指令的工作原理,也能幫你優化你自己的自定義指令.
如果還有問題的話,歡迎大家在下面評論裡加上你的問題
以後還會接着分析關于指令裡的其它兩個問題:
- 指令使用
屬性是怎麼工作的?transclusion
- 指令的
函數是怎麼關聯的controller
最後,如果發現本文哪裡有不對的,請及時給我發評論
謝謝