歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux編程 >> Linux編程 >> AngularJS開發指南4:指令的詳解

AngularJS開發指南4:指令的詳解

日期:2017/3/1 9:34:06   编辑:Linux編程

指令是我們用來擴展浏覽器能力的技術之一。在DOM編譯期間,和HTML元素關聯著的指令會被檢測到,並且被執行。這使得指令可以為DOM指定行為,或者改變它。

AngularJS有一套完整的、可擴展的、用來幫助web應用開發的指令集,它使得HTML可以轉變成“特定領域語言(DSL)”。

指令可以做為HTML中的元素名,屬性名,類名,或者注釋。下面是一些等效調用myDir指令的例子:

<span my-dir="exp"></span>
<span class="my-dir: exp;"></span>
<my-dir></my-dir>
<!-- directive: my-dir exp -->

angular在編譯期間,編譯器會用$interpolate服務去檢查文本中是否嵌入了表達式。這個表達式會被當成一個監視器一樣注冊,並且作為$digest循環中的一部分,它會自動更新。

HTML的編譯分為三個階段:

  1. 首先浏覽器會用它的標准API將HTML解析成DOM。 你需要認清這一點,因為我們的模板必須是可被解析的HTML。這是AngularJS和那些“以字符串為基礎而非以DOM元素為基礎的”模板系統的區別之處。

  2. DOM的編譯是由$compile方法來執行的。 這個方法會遍歷DOM並找到匹配的指令。一旦找到一個,它就會被加入一個指令列表中,這個列表是用來記錄所有和當前DOM相關的指令的。 一旦所有的指令都被確定了,會按照優先級被排序,並且他們的compile方法會被調用。 指令的$compile()函數能修改DOM結構,並且要負責生成一個link函數(後面會提到)。$compile方法最後返回一個合並起來的鏈接函數,這時鏈接函數是每一個指令的compile函數返回的鏈接函數的集合。

  3. 通過調用上一步所說的鏈接函數來將模板與作用域鏈接起來。這會輪流調用每一個指令的鏈接函數,讓每一個指令都能對DOM注冊監聽事件,和建立對作用域的的監聽。這樣最後就形成了作用域的DOM的動態綁定。任何一個作用域的改變都會在DOM上體現出來。

var html = '<div ng-bind='exp'></div>';

// Step 1: parse HTML into DOM element
var template = angular.element(html);

// Step 2: compile the template
var linkFn = $compile(template);

// Step 3: link the compiled template with the scope.
linkFn(scope);

你可能會疑惑為什麼編譯過程和鏈接過程要分離。要明白其中的原因,你可以先看下面這個帶有“重復指令”的例子:

Hello {{user}}, you have these actions:
<ul>
  <li ng-repeat="action in user.actions">
    {{action.description}}
  </li>
</ul>

當上面的例子被編譯後,編譯器會遍歷所有節點來尋找指令。例如{{user}}是一個替換式指令,ngRepeat是另一個指令。但是ngRepeat有一個難題。他需要為user.actions中的每一個action 構造一個li。這意味著它先要保存一個“干淨”的li元素來用作克隆,然後等新的action插入進來時,克隆這個干淨的li元素,把克隆出來的li元素插入到ul中。但是僅僅克隆li的話工作還沒完。他還需要編譯這個li才能把其中的{{action.descriptions}}的替換式替換成相應作用域下的值。我們可以用一個簡單的方法來克隆和插入li元素,然後編譯它。但是要編譯每一個li的話,速度會很慢, 因為編譯的工程需要我們遍歷DOM樹。如果我們在一個需要循環100次循環體內執行編譯的話,性能問題就會馬上凸現出來。

而我們的解決方案就是將編譯工程分為兩個階段。編譯階段將指令識別出來並按優先級排序,鏈接階段將作用域中的實例和li進行鏈接。

ngRepeat 會阻止li子元素{{action.description}}的編譯,取而代之的是 ngRepeat指令會單獨對li進行編譯,編譯時,會生成多個li元素組成的模板。這個編譯結束後會生成一個鏈接函數,這個函數在執行時,會對整個模板進行編譯,然後為每一個li元素創建一個新的作用域,並把它和對應的作用域鏈接上。這裡,我們只需要編譯一次(對模板進行一次統一的編譯就行了),只是在鏈接的時候,需要鏈接多次,而鏈接操作並不消耗性能。

如何寫一個指令

var myModule = angular.module(...);
myModule.directive('directiveName', function factory(injectables) {
  var directiveDefinitionObject = {
    priority: 0,
    template: '<div></div>',
    templateUrl: 'directive.html',
    replace: false,
    transclude: false,
    restrict: 'A',
    scope: false,
    compile: function compile(tElement, tAttrs, transclude) {
      return {
        pre: function preLink(scope, iElement, iAttrs, controller) { ... },
        post: function postLink(scope, iElement, iAttrs, controller) { ... }
      }
    },
    link: function postLink(scope, iElement, iAttrs) { ... }
  };
  return directiveDefinitionObject;
});

大部分情況下你不需要控制這麼多細節,要簡化上面的代碼,我們首先需要依賴基本選項的默認值。如果使用默認值的話,上面的代碼可以簡化成:

var myModule = angular.module(...);
myModule.directive('directiveName', function factory(injectables) {
  var directiveDefinitionObject = {
    compile: function compile(tElement, tAttrs) {
      return function postLink(scope, iElement, iAttrs) { ... }
    }
  };
  return directiveDefinitionObject;
});

由於大部分的指令只關心實例,並不需要將模板進行變形,所以我們還可以簡化成:

var myModule = angular.module(...);
myModule.directive('directiveName', function factory(injectables) {
  return function postLink(scope, iElement, iAttrs) { ... }
});

上面代碼中的factory函數,我們叫工廠函數,它是用來創建指令的。它只會被調用一次:就是當編譯器第一次匹配到相應指令的時候,你可以在其中進行任何初始化的工作。調用它時使用的是 $injector.invoke , 所以它遵循所有注入器的規則。

指令定義對象,也就是上面代碼中的directiveDefinitionObject對象,給編譯器提供了生成指令需要的細節。這個對象的屬性有:

  • 名稱name - 當前作用域的名稱。

  • 優先級priority - 當一個DOM上有多個指令時,就會需要指定指令執行的順序。 這個優先級就是用來在執行指令的compile函數前,先排序的。高優先級的先執行。

  • terminal - 如果被設置為true,那麼該指令就會在同一個DOM的指令集中最後被執行。

  • 作用域scope- 如果被定義成:

    • true - 那麼就會為當前指令創建一個新的作用域。如果有多個在同一個DOM上的指令要求創建新作用域,那麼只有一個新的會被創建。 這一創建新作用域的規則不適用於模板的根節點,因為模板的根節點總是會得到一個新的作用域。

    • {},對象哈希 - 那麼一個新的“孤立的”作用域就會被創建。這個“孤立的”作用域區別於一般作用域的地方在於,它不會以原型繼承的方式直接繼承自父作用域。這對於創建可重用的組件是非常有用的,因為可重用的組件一般不應該讀或寫父作用域的數據。 這個“孤立的”作用域使用一個對象哈希來表示,這個哈希定義了一系列本地作用域屬性,這些屬性的值可以有以下幾種方式。

      • @ 或 @attr - 將本地作用域成員和DOM屬性綁定。綁定結果總是一個字符串,因為DOM的屬性就是字符串。那@和@attr的區別是什麼呢?舉個例子:@的方式:<widget flater="hello {{name}}"> 和作用域對象: { flater:'@' }。當DOM屬性flater的name值改變的時候, 作用域中的flater也會改變,因為本地作用域成員flater綁定了此指令widget的DOM屬性flater,同時DOM屬性flater的name的值是從父作用域中讀來的,也就是說���作用域有name屬性。@attr的方式:<widget my-attr="hello {{name}}"> 和作用域對象: { localName:'@myAttr' }。當name值改變的時候, 作用域中的LocalName也會改變。它的特點是:父作用域傳遞一個屬性給子作用域。

      • = 或 =expression - 在本地作用域屬性和父作用域屬性間建立一個雙向的綁定。 =的方式: <widget flater="parentModel"> 和作用域對象: { flater:'=' }, 本地屬性flater會反映父作用域中parentModel的值,flater和parentModel的任一方改變都會影響對方,原理就是:flater是子作用域的屬性,parentModel是父作用域的屬性,它們進行了雙向綁定,一方改變,另一方也會改變,它們是通過DOM屬性flater聯系在一起的。=expression的方式: <widget my-attr="parentModel"> 和作用域對象: { localModel:'=myAttr' }, 本地屬性localModel會反映父作用域中parentModel的值。它的特點:父作用域下的屬性跟子作用域下的屬性進行雙向綁定。

      • & 或 &attr - 比如:<widget flater="sayHello(name)">和作用域對象:{flater:'&'},這時本地屬性flater綁定了父作用域下的sayHello方法。這時,你在子作用域下調用flater方法其實就是調用父作用域下的sayHello方法。如果需要傳參的話,是通過flater({name:"chaojidan"})。它的特點:父作用域下傳遞一個函數給子作用域。

  • controller - 這個是指令內部的controller,跟angular中的controller不一樣。它的作用是暴露此指令的一些方法給其他指令使用。這個控制器函數是在預編譯階段被執行的,並且它是共享的,這就使得指令間可以互相交流來擴大自己的能力。

  • require - 請求將另一個指令,假設為direct2,中的內部controller作為參數傳入到當前指令的鏈接函數link中,這樣在當前指令的link函數中,就可以調用direct2指令中的內部controller中定義的方法了。 這個請求需要傳遞被請求指令的名字。如果沒有找到,就會觸發一個錯誤。請求的名字可以加上下面兩個前綴:

    • ? - 不要觸發錯誤,這只是一個可選的請求。
    • ^ - 沒找到的話,在父元素的作用域裡面去查找有沒有。
  • restrict - EACM中的任意一個字母。它是用來限制指令的聲明格式的。如果沒有這一項。那就只允許使用屬性形式的指令。

    • E - 元素名稱:<my-directive></my-directive>
    • A - 屬性: <div my-directive="exp"> </div>
    • C - 類名:<div class="my-directive: exp;"></div>
    • M - 注釋: <!-- directive: my-directive exp -->
  • 模板template - 將當前的元素替換掉。 這個替換過程會自動將元素的屬性和css類名添加到新元素上。

  • 模板地址templateUrl - 和template屬性一樣,只不過這裡指示的是一個模板的URL。因為模板加載是異步的,所有編譯和鏈接都會等到加載完成後再執行。

  • 替換replace - 如果被設置成true,那麼頁面上指令內部裡面的內容會被模板替換。比如:<hello><div>這是指令內部的內容</div></hello>,hello指令內部的div內容將會被模板替換掉。

  • transclude - 如果不想讓指令內部的內容被模板替換,可以設置這個值為true。一般情況下需要和ngTransclude指令一起使用。 比如:template:"<div>hello every <div ng-transclude></div></div>",這時,指令內部的內容會嵌入到ng-transclude這個div中。也就是變成了<div>hello every <div>這是指令內部的內容</div></div>

  • 編譯compile - 這就是後面將要講到的編譯函數。

  • 鏈接link - 這就是後面將要講到的鏈接函數。

編譯函數 Compile function

function compile(tElement, tAttrs, transclude) { ... }

編譯函數是用來處理需要修改模板DOM的情況的。因為大部分指令都不需要修改模板,所以這個函數也不常用。需要用到的例子有ngTrepeat,這個是需要修改模板的,還有ngView這個是需要異步載入內容的。編譯函數接受以下參數。

  • tElement - template element - 指令所在的元素。對這個元素及其子元素進行變形之類的操作是安全的。

  • tAttrs - template attributes - 這個元素上所有指令聲明的屬性,這些屬性都是在編譯函數裡共享的。

  • transclude - 一個嵌入的鏈接函數function(scope, cloneLinkingFn)

注意:在編譯函數裡面不要進行任何DOM變形之外的操作。 更重要的,DOM監聽事件的注冊應該在鏈接函數中做,而不是編譯函數中。

編譯函數可以返回一個對象或者函數。

  • 返回函數 - 等效於在編譯函數不存在時,使用配置對象的link屬性注冊的鏈接函數。

  • 返回對象 - 返回一個通過prepost屬性注冊了函數的對象。參考下面pre-linkingpost-liking函數的解釋。

鏈接函數 Linking function

function link(scope, iElement, iAttrs, controller) { ... }

鏈接函數負責注冊DOM事件和更新DOM。它是在模板被克隆之後執行的,它也是大部分指令邏輯代碼編寫的地方。

  • scope - 指令需要監聽的作用域。

  • iElement - instance element - 指令所在的元素。只有在postLink函數中對元素的子元素進行操作才是安全的,因為那時它們才已經全部鏈接好。

  • iAttrs - instance attributes - 實例屬性,一個標准化的、所有聲明在當前元素上的屬性列表,這些屬性在所有鏈接函數間是共享的。

  • controller - 控制器實例,也就是當前指令通過require請求的指令direct2內部的controller。比如:direct2指令中的controller:function(){this.addStrength = function(){}},那麼,在當前指令的link函數中,你就可以通過controller.addStrength進行調用了。

Pre-linking function 在子元素被鏈接前執行。不能用來進行DOM的變形,以防鏈接函數找不到正確的元素來鏈接。

Post-linking function 所有元素都被鏈接後執行。

屬性 Attributes

The Attributes object屬性對象 - 作為參數傳遞給鏈接函數和編譯函數。這使得下列資源可以被使用。

  • 標准化的屬性名: 因為指令的名稱,如ngBind可以有很多種變形表示,如ng:bind,或者x-ng-bind,這個對象使得可以用標准的名稱獲取到相應的屬性。

  • 指令間通信:所有指令間共享同一個屬性對象的實例,這使得指令可以通過這個屬性對象通信。

  • 支持替換式:屬性中若包含替換式,那麼其他指令能夠讀到替換式的值。

  • 監視替換式屬性:使用$observe,能監視使用了替換式的屬性(比如 src="{{bar}}")。這是一種高效的,也是唯一的方法來獲取變量的值。因為在鏈接階段替換式還沒有被替換成值前,所有變量此時是undefined。

 function linkingFn(scope, elm, attrs, ctrl) {
   // get the attribute value
   console.log(attrs.ngModel);
   // change the attribute
   attrs.$set('ngModel', 'new value');
   // observe changes to interpolated attribute
   attrs.$observe('ngModel', function(value) {
       console.log('ngModel has changed value to ' + value);
   });
 }

創建組件

通常需要使用更復雜的DOM結構替換單個指令。這允許指令成為一個可以生成應用程序可重用組件的短標志。

AngularJS權威教程 清晰PDF版 http://www.linuxidc.com/Linux/2015-01/111429.htm

希望你喜歡,並分享我的工作~帶你走近AngularJS系列

  1. 帶你走近AngularJS - 基本功能介紹 http://www.linuxidc.com/Linux/2014-05/102140.htm
  2. 帶你走近AngularJS - 體驗指令實例 http://www.linuxidc.com/Linux/2014-05/102141.htm
  3. 帶你走近AngularJS - 創建自定義指令 http://www.linuxidc.com/Linux/2014-05/102142.htm

如何在 AngularJS 中對控制器進行單元測試 http://www.linuxidc.com/Linux/2013-12/94166.htm

在 AngularJS 應用中通過 JSON 文件來設置狀態 http://www.linuxidc.com/Linux/2014-07/104083.htm

AngularJS 之 Factory vs Service vs Provider http://www.linuxidc.com/Linux/2014-05/101475.htm

AngularJS —— 使用 ngResource、RESTful APIs 和 Spring MVC 框架提交數據 http://www.linuxidc.com/Linux/2014-07/104402.htm

AngularJS 的詳細介紹:請點這裡
AngularJS 的下載地址:請點這裡

Copyright © Linux教程網 All Rights Reserved