指令是咱們用來擴展瀏覽器能力的技術之一。在DOM編譯期間,和HTML元素關聯着的指令會被檢測到,而且被執行。這使得指令能夠爲DOM指定行爲,或者改變它。css
AngularJS有一套完整的、可擴展的、用來幫助web應用開發的指令集,它使得HTML能夠轉變成「特定領域語言(DSL)」。html
指令能夠作爲HTML中的元素名,屬性名,類名,或者註釋。下面是一些等效調用myDir指令的例子:web
<span my-dir="exp"></span> <span class="my-dir: exp;"></span> <my-dir></my-dir> <!-- directive: my-dir exp -->
angular在編譯期間,編譯器會用$interpolate服務去檢查文本中是否嵌入了表達式。這個表達式會被當成一個監視器同樣註冊,而且做爲$digest循環中的一部分,它會自動更新。express
HTML的編譯分爲三個階段:瀏覽器
首先瀏覽器會用它的標準API將HTML解析成DOM。 你須要認清這一點,由於咱們的模板必須是可被解析的HTML。這是AngularJS和那些「以字符串爲基礎而非以DOM元素爲基礎的」模板系統的區別之處。安全
DOM的編譯是由$compile
方法來執行的。 這個方法會遍歷DOM並找到匹配的指令。一旦找到一個,它就會被加入一個指令列表中,這個列表是用來記錄全部和當前DOM相關的指令的。 一旦全部的指令都被肯定了,會按照優先級被排序,而且他們的compile
方法會被調用。 指令的$compile()
函數能修改DOM結構,而且要負責生成一個link函數(後面會提到)。$compile方法最後返回一個合併起來的連接函數,這時連接函數是每個指令的compile函數返回的連接函數的集合。異步
經過調用上一步所說的連接函數來將模板與做用域連接起來。這會輪流調用每個指令的連接函數,讓每個指令都能對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);
你可能會疑惑爲何編譯過程和連接過程要分離。要明白其中的緣由,你能夠先看下面這個帶有「重複指令」的例子:post
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中的任意一個字母。它是用來限制指令的聲明格式的。若是沒有這一項。那就只容許使用屬性形式的指令。
<my-directive></my-directive>
<div my-directive="exp"> </div>
<div class="my-directive: exp;"></div>
<!-- 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 - 這就是後面將要講到的編譯函數。
function compile(tElement, tAttrs, transclude) { ... }
編譯函數是用來處理須要修改模板DOM的狀況的。由於大部分指令都不須要修改模板,因此這個函數也不經常使用。須要用到的例子有ngTrepeat
,這個是須要修改模板的,還有ngView
這個是須要異步載入內容的。編譯函數接受如下參數。
tElement - template element - 指令所在的元素。對這個元素及其子元素進行變形之類的操做是安全的。
tAttrs - template attributes - 這個元素上全部指令聲明的屬性,這些屬性都是在編譯函數裏共享的。
transclude - 一個嵌入的連接函數function(scope, cloneLinkingFn)
。
注意:在編譯函數裏面不要進行任何DOM變形以外的操做。 更重要的,DOM監聽事件的註冊應該在連接函數中作,而不是編譯函數中。
編譯函數能夠返回一個對象或者函數。
返回函數 - 等效於在編譯函數不存在時,使用配置對象的link
屬性註冊的連接函數。
返回對象 - 返回一個經過pre
或post
屬性註冊了函數的對象。參考下面pre-linking
和post-liking
函數的解釋。
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 全部元素都被連接後執行。
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結構替換單個指令。這容許指令成爲一個能夠生成應用程序可重用組件的短標誌。
加油!