angular 指令详解(一)compile与link

原文地址:https://987.tw/2014/09/03/ang...

AngularJS directives是令人惊艳的。它允许你创造高度语意且可重复利用的元件。在某种意义上你可以认为它是极致的web components先驱者。

有许多很棒的文章,甚至是书籍,在教导你如何撰写自己的directives。相较之下,只有少许的资讯谈到有关compile及link函式的差异,更不用说有关pre-link及post-link函式差别。

大多数的导引只有简单地提到compile函式主要由AngularJS在内部使用,并且建议你只要用link函式,应该能够涵盖大多数的使用案例。

这是十分不幸的,因为了解这些函式其中的差异能够提升你的能力,更加的了解AngularJS内部运作,并且订制出更好的directives。

所以跟着我,文章最后你将会正确地了解这些函式是什么以及什么时候该使用它们。

本文假设你已经了解什么是AngularJS directive。如果不了解,我强烈建议你先阅读AngularJS开发者指南的directive章节。

AngularJS如何处理directives?

在我们开始之前,让我打断一下,先了解AngularJS是如何处理directives。

(1)当浏览器渲染(render)页面时,基本地读取HTML标签,建立一个DOM,当DOM准备好时,广播(broadcast)出一个事件(event)。

(2)当你使用<script></script>标签引入你的AngularJS程式码到页面时,AngularJS会监听(listen)该事件,一旦该事件发出,AngularJS便会开始走遍(traversing)DOM,寻找所有元素(element)中的属性(attribute )是否具有ng-app。

(3)一旦找到具有该属性的元素,AngularJS便以该元素作为起始点,进行DOM 处理。如果在html元素的属性内设定ng-app,那么AngularJS将会从html元素开始处理 DOM。

(4)从起始点开始,AngularJS递回地调查所有子元素,从你的AngularJS应用程式中所定义的directives中去找寻相对应的样式。

AngularJS如何处理元素,取决于实际定义directive的物件(译注:directive definition object)。你可以预先定义compile函式或link函式,两者可同时存在。或者选择性的定义pre-link及post-link这两个函式来取代link函式,

所以,这些函式有什么差异?为什么及何时该使用这些函式?
坚持下去...

程式码

为了解释这些差异,我会用程式码来做示范,希望能够更容易的理解。

考虑下列HTML标签:

<level-one>  
    <level-two>
        <level-three>
            Hello {{name}}         
        </level-three>
    </level-two>
</level-one>

以及下列JavaScript:

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处理巢状的三个directives,而每个directive都有自己的compile、pre-link及post-link函式,各函式输出讯息至console,我们可以借此作为识别。

这让我们可以一睹AngularJS是如何在背后处理这些directives。

输出结果:
这是console输出结果的截图:

angular 指令详解(一)compile与link

如果你要自己试试看,开启这个plnkr链接,并在打开浏览器的Console。

开始分析

第一件要注意的是,函式呼叫的顺序:

// COMPILE階段
// levelOne:    compile函式已呼叫
// levelTwo:    compile函式已呼叫
// levelThree:  compile函式已呼叫

// PRE-LINK階段
// levelOne:    pre link函式已呼叫
// levelTwo:    pre link函式已呼叫
// levelThree:  pre link函式已呼叫

// POST-LINK階段 (注意到反向順序)
// levelThree:  post link函式已呼叫
// levelTwo:    post link函式已呼叫
// levelOne:    post link函式已呼叫

这个清除地展示AngularJS一开始compile所有directives,compile阶段尚未连结scope,link阶段也分成pre-link及post-link阶段。

注意到呼叫compile及pre-link的顺序是一致的,但是呼叫post-link的顺序则是相反的。

所以在这里我们可以已经清处的辨别这几个不同的阶段,但是compile与pre-link又有什么不同呢?它们也有同样的顺序,为什么要将它们分开?

文件物件模型(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());
          }
        }
      }
    }
  }
}

app.directive('levelOne', createDirective('levelOne'));  
app.directive('levelTwo', createDirective('levelTwo'));  
app.directive('levelThree', createDirective('levelThree'));

注意到console.log额外的输出讯息。没有任何更动,仍然是最原始的标签。

这应该能让我们更详细的了解函式的来龙去脉。

让我们再次执行程式码。
输出结果:
angular 指令详解(一)compile与link
angular 指令详解(一)compile与link
angular 指令详解(一)compile与link

观察

输出DOM结果透漏某些有趣的东西:compile与pre-link阶段的DOM不一样。

所以,发生什么事?

Compile
我们已经学习到当AngularJS侦测DOM准备好时,会进行DOM处理。

所以,当AngularJS开始走遍DOM,它遇见<level-one>元素,并从它的directive定义(directive definition)中得知需要执行某些行为。

因为在levelOne的directive定义中,定义了compile函式,所以会呼叫此函式并带入元素DOM作为函式的参数。

如果你靠近一点你会看到,在这个时机点,元素的DOM仍然是最初刚开始的DOM,系由浏览器根据原始HTML标签所创造出来的DOM。

在AngularJS里,经常用样板元素(template element)来提到原始的DOM,因此基于这个原因我个人用tElem来作为compile函式内的参数名称,用来表示样板元素。

当levelOne的compile执行之后,AngularJS更深入且递回地走入DOM,对<level-two>及<level-three>重复相同的编译步骤。

Post-link
在我们深入pre-link函式前,让我们先看一下post-link函式。

如果你产生的directive只有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已经被呼叫过。

这就是为什么它被认为是用来加入你的directive逻辑最安全以及预设的地方。

那元素的DOM呢?为什么在这里它们是不同的?

当AngularJS呼叫了directive的compile函式之后,它会产生一个样板元素(template element)的实例元素(instance element)(通常称之为消灭实体),并且提供一个scope给这个实体。这个scope可以是全新的scope、继承的子scope或孤立的scope,取决于相对应directive定义物件内scope属性设定。

所以,到连结阶段的时候,实例元素及scope已经可以开始使用,并且AngularJS会将它作为函式参数传递到post-link函式。

Pre-link
当撰写post-link函式时,你可以保证所有子元素的post-link函式已经执行过。

在大多数的案例中,这个非常合理,因此它也是最常用来撰写directive程式码的地方。

然而,AngularJS提供了一个附加的钩子,称之为pre-link函式,程式码会先被执行,抢先在所有子元素的post-link被执行之前。

再次强调:

pre-link函式保证所有子元素的post-link被执行前,先执行pre-link函式,并且是在实体元素中执行。

所以当相反顺序的呼叫post-link十分合理,那原始顺序的呼叫pre-link也是十分合理。

回顾

如果我们回顾之前原始输出,我们可以清晰的辨认出发生什么事:

// 这里的元素仍然是最原始的样板标签

// COMPILE 阶段
// levelOne:    原始DOM中呼叫compile函式
// levelTwo:    原始DOM中呼叫compile函式
// levelThree:  原始DOM中呼叫compile函式

// 从这里开始,元素已经实例化且綁定了SCOPE
// (例:NG-REPEAT 已有多重实例)

// PRE-LINK 階段
// levelOne:    元素实例中呼叫pre link函式
// levelTwo:    元素实例中呼叫pre link函式
// levelThree:  元素实例中呼叫pre link函式

// POST-LINK 阶段 (注意到順序相反)
// levelThree:  元素实例中呼叫post link函式
// levelTwo:    元素实例中呼叫post link函式
// levelOne:    元素实例中呼叫post link函式

摘要

回顾中我们可以描述不同的函式及使用案例如下:

Compile函式
在AngularJS产生实例及scope之前,使用compile函式来更动原始DOM(样板元素)。

它可以有多个元素实例,但只会有一个样板元素。ng-repeat就是这个案例的一个完美范例。它让compile成为最佳的地方来进行更动DOM,之后才会套用所有实例,因为只会执行一次,所以当你要消灭很多实例时,可以获得很多效率上的提升。

样板的元素及属性都会作为参数传递到compile函式,但不会有scope传入,因为还没准备好:

/**
* Compile函式
* 
* @param tElem - 样板元素
* @param tAttrs - 样板元素的属性
*/
function(tElem, tAttrs){

    // ...

};

Pre-link函式
当AngularJS已经compile子元素,在任何子元素的post-link执行之前,使用pre-link函式来实作逻辑。

Scope、实例元素及实例属性都会作为参数传递到pre-link函式:

/**
* Pre-link函式
* 
* @param scope - 關連於此實例的scope
* @param iElem - 實例元素
* @param iAttrs - 實例元素的屬性
*/
function(scope, iElem, iAttrs){

    // ...

};

Post-link函式
使用post-link来执行逻辑,该逻辑知道所有子元素已经编译,并且所有子元素的pre-link及post-link都已经被执行。

基于这个理由,post-link认为是最安全及预设的地方来撰写你的程式码。

Scope、实例元素及实例属性都会作为参数传递到post-link函式:

/**
* Post-link函式
*
  • @param scope - 關連於此實例的scope

  • @param iElem - 實例元素

  • @param iAttrs - 實例元素的屬性
    */

function(scope, iElem, iAttrs){

// ...

};

结论

到目前为止,但愿你有清楚的理解关于compile、pre-link及post-link之间的差异。

如果没有且你很认真的在做AngularJS开发,我强烈建议你再读一次文章,直到你有稳固的抓住其运作原理。

了解这个重要的概念将会让你更容易理解原生的AngularJS directive是如何运作,并且如何最佳化你订制的directives。

相关推荐