AngularJS指令compile和link函數的真相

AngularJS太棒了。它容許你建立高度語義化和可重用的組建。從某種意義上說,您能夠將它們視爲Web組件的最終先驅。html

關於如何寫自定義的指令已經出如今不少好的文章和書中了。相反,很騷有涉及到compilelink函數的不一樣,更不要說pre-linkpost-link函數了。git

大多數教程都簡潔的說起到compile函數主要被用在AngularJS內部而且建議你只使用link函數這能夠覆蓋大多數自定義指令使用場景。angularjs

所以,跟誰我,在本文的最後,您將確切知道這些功能是什麼以及什麼時候應該使用它們。github

This article assumes that you already know what an AngularJS directive is. If not, I would highly recommend reading the AngularJS developer guide section on directives first.

AngularJS是如何處理指令的

在咱們開始以前,讓咱們先來分解AngularJS如何處理指令。瀏覽器

當瀏覽器渲染一個頁面時,它必定會去閱讀HTML標籤,建立DOM而且在DOM準備就緒時廣播事件。安全

當你使用<script></script>在你的代碼頁中引入AngulaJS時,AngularJS監聽那事件當它一旦收到事件,它開始遍歷DOM,查詢一個有ng-app屬性的標籤。微信

當這個標籤被查找到,AngularJS開始使用該特定元素做爲起點處理DOM。所以,若是在html元素上設置ng-app屬性,AngularJS將開始處理從html元素開始的DOM。app

從這時起,AngularJS遞歸地查詢全部的子元素,尋找對應於你的AngularJS應用程序中定義的指令的模式。ide

AngularJS如何處理元素取決於實際的指令定義對象。你定義了一個compile函數,一個link函數或者二者。或者你能夠定義一個pre-link函數和一個post-link函數來代替link函數。函數

那麼,全部這些功能之間有什麼區別,以及爲何或何時應用它們?

跟緊我...

代碼演示

爲了解釋差別,我將使用一些但願易於理解的示例代碼。

考慮下面的HTML標記:

<level-one>  
    <level-two>
        <level-three>
            Hello {{name}}         
        </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'));

目的很簡單:使AngularJS處理三個嵌套的指令,每一個指令都有本身的compile,pre-link,post-link函數去log一行文本保證咱們可以辨識它們。

這應該使咱們可以首先了解AngularJS處理指令後發生的狀況。

輸出

如下是控制檯中輸出的屏幕截圖:
圖片描述
本身嘗試一下,僅僅打開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

這清晰的證實了AngularJS是如何第一編譯全部指令在把他們link他們是scope以前,而且在link階段被分解爲了pre-link,post-link階段

注意到compilepre-link函數調用順序是相同的可是post-link與他們恰好相反。

因此在這一點上咱們已經能夠清楚地識別出不一樣的階段,那麼compilepre-link有什麼區別了?他們執行的順序是相同的,那爲何要把他們分開?

The DOM

爲了深刻研究,讓咱們更新JavaScript,以便在每一個函數調用期間輸出元素的DOM:

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());
          }
        }
      }
    }
  }
}

注意console.log行中的額外輸出。沒有其餘更改,原始標記仍然使用。

輸出

如下是新添加的代碼的輸出截圖:
圖片描述

再次,若是你想本身嘗試一下,只需打開this plnkr並看看控制檯。

觀察

打印出的DOM顯示一些有趣的事:DOM在compilepre-link階段是不一樣的。

這裏發生了什麼

編譯

咱們已經知道AngularJS在檢測到DOM已經準備就緒時會處理DOM。

因此當AngularJS開始遍歷DOM,它碰到<level-one>元素而且瞭解到它在匹配咱們定義的指令它有額外的動做須要執行

因爲在evelOne指令定義對象中定義了一個compile函數,他被調用而且相對應的元素DOM被看成參數傳入

若是你仔細查看,在這點上,元素的DOM仍然是最初由瀏覽器使用原始HTML標記建立的DOM。

在AngularJS中 original DOM 常被指代爲template element,所以我我的鐘意使用 tElem做爲參數名,以表明template element。

一旦levelOne指令的compile函數已經運行,AngularJS遞歸地便利深層的DOM並對<level-two><level-three>元素執行一樣的編譯步驟。

Post-link

在深刻pre-link函數以前,讓咱們先看一看post-link函數

若是你建立的指令中只有link 函數,AngularJS將它看成 post-link函數,這也是咱們先討論它的緣由。

在AngularJS向下傳遞DOM並運行全部編譯compile以後,它再次反向遍歷並運行全部相關的post-link函數。

DOM如今在相反的方向上遍歷,所以post-link函數按相反的順序調用。因此,雖然相反的順序幾分鐘前看起來很奇怪,但它如今開始變得很是有意義。
圖片描述

這個相反的順序保證了在父元素的post-link函數運行時全部子元素的post-link函數已經運行。

因此當<level-one>post-link執行時,咱們保證<level-two> <level-three>post-link函數已經執行。

這就是爲何它被認爲是最安全和默認的添加指令邏輯的緣由。

可是元素的DOM呢?這裏爲何不一樣?

一單AngularJS已經調用了compile函數,它建立了一個對應模板元素的instance element併爲instance提供一個scope。The scope能夠爲一個新的scope或者存在的一個,子scope或者隔離的scope,取決於相應指令定義對象的scope屬性。

所以,在link發生時,實例元素和範圍已經可用,而且它們被AngularJS做爲參數傳遞給post-link函數。

所以控制檯輸出有差別

Pre-link

在編寫post-link函數時,能夠保證全部子元素的post-link函數都已經執行。

在大多數狀況下,這是很是有意義的,所以它是編寫指令代碼最經常使用的地方。

然而AngularJS提供了一個附加的鉤子,the pre-link函數,你能夠在全部子元素執行post-link函數以前執行你的代碼。

值得重申的是:
pre-link函數能保證在全部子istance element執行post-link前先執行。

所以,儘管post-link函數以相反的順序調用很是合理,但如今再次以原始順序調用全部pre-link函數是很是有意義的。

元素的pre-link函數被保障在任意子元素的pre-linkpost-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 function

使用compile來實如今AngularJS建立它的實例以前和建立範圍以前更改原始DOM(模板元素)。

雖然能夠有多個元素實例,但只有一個模板元素。ng-repeat指令是這種狀況的一個很好的例子。這使得編譯功能成爲修改DOM的理想場所,之後應該將其應用於全部實例,由於它只會運行一次,所以若是要刪除大量實例,則會極大地提升性能。

模板元素和屬性做爲參數傳遞給編譯函數,但沒有scope可用:

/**
* Compile function
* 
* @param tElem - template element
* @param tAttrs - attributes of the template element
*/
function(tElem, tAttrs){

    // ...

};

Pre-link function

使用pre-link函數來實現當AngularJS已經編譯子元素時,但在子元素的任何post-link函數被調用以前運行的邏輯。

scope,instance 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){

    // ...

};
Here you can see example code of official AngularJS directives that use a pre-link function.

Post-link function

使用post-link函數執行邏輯,全部的子元素已經被編譯而且全部子元素的pre-link post-link已經執行。

因此post-link是被看成最安全和默認的位置放置你的代碼。
scope,instance 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){

    // ...

};

最後

若有任何問題和建議歡迎發送至郵箱討論:<Tommy.White.h.li@gmail.com>
編寫不易,若您以爲對您有幫助,歡迎打賞

微信:圖片描述

支付寶:圖片描述

相關文章