這一期中,我不會分析源碼,只是翻譯一下"https://docs.angularjs.org/api/ng/service/$compile",當然不是逐字逐句翻譯,講解指令應該如何編寫,下一期再接着講$compile的源碼。我覺得,懂得如何使用angular可能對童鞋們更有用。
先說點廢話:上一期更新的時間是11月25日,一停就是相隔兩周多了。1.是由於公司的網站上線(給公司打個廣告(美好學院)[http://www.meihaoxueyuan.com]),2.是由於家里發生了一些事,3.是$compile做為angular的一個關鍵服務,tmd代碼也太復雜了。
$compile
$compile的功能是,將一個html字符串或者一個DOM進行編譯,最后返回一個鏈接函數,這個鏈接函數可以用於將作用域(Scope)和模版"鏈接"到一起.編譯的過程,其實質是遍歷DOM樹,匹配和處理DOM上的各種指令的過程.
注意:這個篇文檔將深入介紹指令的各種選項,如果只是想通過例子來簡單了解指令,可以參考(directive guide)[https://docs.angularjs.org/guide/directive]
全面的指令接口(Comprehensive Directive API)
對於指令(directive)有很多的不同選項.
你可以通過directive定義的工廠函數返回一個"指令定義對象(Directive Definition Object)(用於定義指令的各種屬性,如下)",或者僅僅返回一個postLink(所有其他屬性將使用默認值);
推薦使用Directive Definition Object形式
這里有一個實例,用Directive Definition Object形式申明的指令
var myModule = angular.module(...);
myModule.directive('directiveName', function factory(injectables) {
var directiveDefinitionObject = {
priority: 0,
template: '<div></div>', // or // function(tElement, tAttrs) { ... },
// or
// templateUrl: 'directive.html', // or // function(tElement, tAttrs) { ... },
transclude: false,
restrict: 'A',
templateNamespace: 'html',
scope: false,
controller: function($scope, $element, $attrs, $transclude, otherInjectables) { ... },
controllerAs: 'stringIdentifier',
bindToController: false,
require: 'siblingDirectiveName', // or // ['^parentDirectiveName', '?optionalDirectiveName', '?^optionalParent'],
compile: function compile(tElement, tAttrs, transclude) {
return {
pre: function preLink(scope, iElement, iAttrs, controller) { ... },
post: function postLink(scope, iElement, iAttrs, controller) { ... }
}
// or
// return function postLink( ... ) { ... }
},
// or
// link: {
// pre: function preLink(scope, iElement, iAttrs, controller) { ... },
// post: function postLink(scope, iElement, iAttrs, controller) { ... }
// }
// or
// link: function postLink( ... ) { ... }
};
return directiveDefinitionObject;
});
注意:沒有申明的屬性選項將使用默認值,你可以在下面看到他們的默認值
下面是一個可能簡單一點的例子:
var myModule = angular.module(...);
myModule.directive('directiveName', function factory(injectables) {
var directiveDefinitionObject = {
link: function postLink(scope, iElement, iAttrs) { ... }
};
return directiveDefinitionObject;
// or
// return function postLink(scope, iElement, iAttrs) { ... }
});
指令定義對象(Directive Definition Object)
指令定義對象將給編譯器提供編譯必要的說明。它包含這些屬性:
multiElement (多元素)
當這個屬性設置為true的時候,html編譯器將會在含有directive-name-start
和directive-name-end
屬性的Dom節點間收集Dom節點,被收集到的節點集合將作為一個整體被用作指令的元素(elements).推薦在沒有嚴格行為(如ngClick)且不會操作或者替換子元素的的指令編寫時,使用該屬性.通過這個屬性,我們可以給指令的操作元素制定一個范圍,具體怎么用就看想象力了.
priority (優先級)
當大多數情況指令都被定義成只處理一個獨立的DOM元素,有時候給指令排一個順序是很有必要的.這個priority
將用於編譯函數執行前的指令排序.priority
被定義為一個數值,這個數字越大,指令的優先級越高,就越是會被優先執行.Pre-link
函數也會按照這個順序來執行,但是post-link
將會按照相反的順序來執行.如果這個屬性沒有定義的話,默認值將是0.我們在查看angularjs的api文檔中的指令時,可以注意一下每個指令的執行的優先級,比如ngIf的優先級是600,而ngShow的優先級是0,有的時候優先級會很重要.
terminal (終結者,請允許我這么翻譯它)
如果這個屬性設置為true,所有優先級小於該指令的指令都不會在執行了,注意和該指令優先級相同的元素依然會得到執行.
scope (作用域)
這個scope屬性可以設置為true和對象或者falsy值(與false相等的值):
- falsy:不會為指令建立新的作用域,指令將使用父作用域.
- true :一個原型將繼承父作用域的新的子作用域將會為這個指令建立.如果一個dom元素的多個指令都要求創建新的作用域,只有一個作用域會被創建.新建的作用規則將不再適用於模版的上層元素,因為模版總是新建一個作用域.
- {...}(一個對象哈希):一個新的"孤立"作用域將會為指令創建."孤立"作用域不同與常規的作用,它不會在原型上繼承父作用域.這對於創建一個可重用的組件很有幫助,因為它讓指令不會意外的讀到或修改到父作用域的數據.
"孤立"作用域對象哈希定義了一個本地作用域的屬性集合,這些屬性能夠從使用指令的元素的屬性上派生出.這些本地的屬性值,對於模版中的別名很有用.在對象哈希映射的鍵將會被定義為"孤立作用域"的的屬性;映射的值將定義怎么和父作用域綁定到一起,通過匹配在使用的指令的的元素的屬性.(為什么,翻譯出來就這么繞呢,還是自己英語太搓了?)
- @ 或者 @attr :綁定一個本地作用域到一個DOM的屬性值.這回導致所有得到的值都是一個字符串.如果
attr
沒有,綁定的屬性將和本地屬性同名.比如,有這樣的一個DOM節點:<widget my-attr="hello {{name}}">
;並且widget指令中有如此定義scope:{localName:'@myAttr'}
,那么widget的作用scope的屬性localName
將是hello {{name}}
計算后的值.當name
屬性的值改變后,localName
的值也會相應的改變.name
則是從父作用域中獲取的. - = 或者 =attr :設置雙向綁定在作用域和父作用域間.如果
attr
沒有定義,那么綁定的作用域名和使用指令的元素的屬性名相同.當綁定指令的元素是<widget my-attr="parentModel">
並且widget指令作用域定義為scope: { localModel:'=myAttr' }
,那么widget作用域屬性localModel
將映射為父作用域的parentModel
,當改變parentModel時localModel也會相應改變,同樣localModel改變時parentModel也會改變.如果父作用域中的屬性不存在,angular會拋出一個NON_ASSIGNABLE_MODEL_EXPRESSION的異常.我們可以通過=?
或者=?attr
,來避免異常的拋出.如果你希望對屬性使用"shallow watch",你可以使用=*
或者=*attr
(=*?
或者=*?attr
,如果屬性是只是可選的).對於"shallow watch"在https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$watchCollection有說明."shallow watch"將監視對象上的所有屬性,任何屬性變化都會導致回調. - & 或者 &attr :提供了一種執行父作用域下的表達式的方式.如果不指明attr,子作用域屬性和dom元素的屬性的名字將一致.當有使用的指令的dom元素
<widget my-attr="count = count + value">
和指令定義的作用域scope: { localFn:'&myAttr' }
,那么孤立作用域屬性localFn
將指向一個函數,這個函數包裝了count = count + value
表達式.這種方式比較適合子作用域通過表達式的方式從父作用域中獲取值
一般來說,可能多個指令同時作用於一個元素,但是在指令對作用域類型的要求上會存在一些限制.下面幾點能有助於解釋這些限制.為了簡單起見,我們這里以兩個指令為例,當然也適用於多個指令的情況:
- no scope + no scope => 兩個指令都沒有要求自己的作用域,那么都會使用父作用域
- child scope + no scope => 會共享使用獨立的統一個子作用域
- child scope + child scope => 會共享使用獨立的統一個子作用域
- isolated scope + no scope => 前者使用"孤立"作用域,后者使用父作用域
- isolated scope + child scope =>不會正常工作
- isolated scope + isolated scope => 不會正常工作
bindToController
當一個"孤立"作用域被用於一個組件,並且controllerAs
也被使用了,bindToController: true
將允許一個組件把他的屬性綁定到控制器上,而不是scope上.當控制器被實例化,這些"孤立"作用域上的綁定的值就已經被初始化完成了.
controller
控制器構造函數.這個控制器將在pre-linking 階段前被初始化,並且可以被其他的指令訪問(參看require屬性).這允許指令間的交互和協調相互的行為.控制器是可以使用下面本體變量進行"依賴注入的:
- $scope 當前綁定到DOM元素的作用域
- $element 當前的DOM元素
- $attrs 當前的元素屬性對象
- $transclude 一個transclude鏈接函數預先綁定正確的transclusion 作用域:
function([scope], cloneLinkingFn, futureParentElement, slotName)
:- scope: (可選)重載作用域
- cloneLinkingFn:(可選)參數用於創建原始的transcluded 內容的克隆元素
- futureParentElement (可選)
- slotName:(可選)
require
要求另一個指令並且注入其控制器作為第4個參數傳入鏈接函數.require這個屬性要求傳入的是一個指令名字的字符串或者字符串的數組.如果使用數組,注入的書序講師數組中元素的順序.如果相應的指令沒有找到,指令沒有控制器,一個錯誤將產生(除非鏈接函數沒有定義,錯誤檢測可以被忽略).指令名稱可以使用的前綴:
- 沒有前綴 需要的指令在當前的DOM元素上.沒有找到就拋出一個錯誤.
?
嘗試在當前的DOM元素上查找指令的controller,如果沒有找到鏈接函數的第四個參數將被傳入一個null^
在當前的DOM元素和其父級元素上查找,如果沒有找到報錯^^
只在父級元素上查找,沒找到報錯?^
在當前DOM元素和父級元素上找,沒有找到不報錯,給link函數第四個參數傳入一個null?^^
在父級元素上找,沒有找到不報錯,給link函數第四個參數傳入一個null
controllerAs
標識名用於引用指令作用域中的控制器.這允許控制器可以被在指令模版中被引用到.這尤其有用當指令作為一個組件來使用時,比如使用一個"孤立"作用域.這也是可能的,運用一個不使用"孤立"作用域的指令,但是你需要意識到,controllerAs
引用可能重寫父類的一些已經存在的屬性.
restrict
要求一個字符串,這個字符串是 EACM
的子集,這個用來限制指令使用樣式.如果沒有定義,默認是元素形式和元素屬性形式
- E Element name(default) :
<my-directive></my-directive>
- A Atrribute(default) :
<div my-directive="exp"></div>
- C Class:`
- M Commment:
<!-- directive:my-directive exp -->
templateNamespace
一個用於代表模版中標記語言使用的文檔類型的字符.angular js 需要這些信息用於元素的創建和克隆的特殊處理,當他們被在外部定義為使用容器<svg>
和<math>
.
- html 所有的根節點在模版中是html.根節點也可以是同樣級別的元素比如
<svg>
或者<math>
- svg 根節點在模版中是svg元素(不包括
<math>
) - math 根節點在模版中是MathML元素(不包括
<svg>
)
如果templateNamespace 沒有定義,默認是html.
template
HTML 標記,可以這樣:
- 替代指令元素的內容(默認)
- 替代指令元素自己(如果replace是true ,但是這個屬性可能在新版中會被刪除)
- 包裹指令元素的內容(如果transclude是true)
其形式可以如下: - 一個字符串:比如
<div red-on-hover>{{delete_str}}</div>
- 一個包含兩個參數(tElement,tAttrs)的函數,但是必須返回一個字符串
templateUrl
這個參數和template
的功能類似,但是是通過一個明確的URL地址異步加載模版.
因為模版加載是異步的,所以編譯器將在模版被解析出來后才對元素上的執行進行編譯.這意味着對於兄弟元素和父級元素的編譯和鏈接想繼續,就像這個元素上沒有指令一樣.
編譯器不支持整個編譯過程等待模版的加載,是因為這將導致整個應用"假死"直到模版被異步加載成功,特別是當只有一個深度嵌套的指令擁有一個templateUrl屬性的情況.
模版的加載是異步的,即使模版被預先加載到了$templateCache
中.
你可以給templateUrl
指定一個代表URL的字符串,也可以指定一個攜帶兩個參數(tElment和tAttrs)(在后面的compile
函數中將解釋)並且返回一個url字符串的函數.在兩種情況中,模版的Url都會通過$sce.getTrustedResourceUrl的處理.
replace ([不贊成使用],將會在下一個主版本中移除,即v2.0)
指出模版將替換什么內容,默認為false
- true 模版將替換指令所在的元素
- false 模版將替換指令所在元素所包含的內容
替換處理程序將會將之前元素上的所有的屬性和類(css的)都移到替換后的元素上.事例請參見(Directives Guide)[https://docs.angularjs.org/guide/directive#template-expanding-directive].
對於應用模塊中,元素替換的需求只有很少的場景,主要的一中情況就是使用了svg上下文的可重用的定制組件(因為svg在DOM樹中的定制元素是無法運行的).
transclude
提取元素內容在指令出現的地方,並且使之對指令有效.這里內容會被編譯並且提供給指令作為transclusion函數,參看下面的Transclusion部分.
compile
function compile(tElement,tAttrs,transclude){ ... }
編譯函數用於對模版DOM的改造處理.然而,多數指令都沒有對模版的改造處理,它不常被使用.這個編譯函數接收的參數如下:
- tElement 模版元素,就是指令聲明所在的dom元素.僅對模版的改造處理是安全的在元素和子元素上.
- tAttrs 模版屬性,常規化后的屬性列表在指令聲明的元素上在指令所有指令的編譯函數上共享
- transclude [不推薦使用]一個transclude鏈接函數:
function(scope,cloneLinkingFn)
這個編譯函數能夠有一個返回值,可以是一個函數或者一個對象
- 返回一個 (post-link)函數 這等同於注冊一個鏈接函數通過
link
屬性,當編譯函數為空的時候. - 返回一個對象攜帶
pre
和post
屬性注冊的函數 允許你控制當鏈接函數被調用時在鏈接階段.參看下面pre-linking 和post-linking函數的信息.
link
這個屬性只會在`compile函數沒有定義的情況有效.
function link(scope, iElement, iAttrs, controller, transcludeFn) { ... }
鏈接函數負責注冊DOM事件用於更新DOM.它會在模版被克隆后執行.這是大多數指令實現邏輯的地方.
scope
作用域. 作用域用於指令注冊監聽器(watches).iElement
實例元素 使用指令的元素.處理元素的子節點只有在postLink
函數中是安全的,因為子節點已經被克隆了.iAttrs
實例屬性 使用指令的元素的屬性列表.controller
指令所用要求的控制器的實例,這些實例沒元素上的所有指令所共享,這允許指令將控制器作用一個通信頻道使用.這個值的獲取依賴於指令的require
屬性:- 沒有控制器被要求:如果指令自己沒有控制器,那么這個值講師
undefined
. string
: 這個控制器的實例array
: 控制器的數組
如果被要求的控制沒有找到,並且它又是被定義了的,這個實例就是null,否則一個"Missing Required Controller`錯誤就會被拋出.
注意你可以要指令自身的控制器,這個要求其他控制器是一樣的
- 沒有控制器被要求:如果指令自己沒有控制器,那么這個值講師
transcludeFn
一個transclude鏈接函數預綁定在正確的transclusion 作用域上。這個和指令控制器中$transclude
是一樣的,詳情參考function([scope], cloneLinkingFn, futureParentElement)
Pre-linking function
在鏈接前執行。執行DOM的修改時不安全的,因為可能會導致編譯鏈接函數在鏈接定位正確的元素失敗.
Post-linking function
執行在子元素被鏈接后
注意,子元素包含使用了templateUrl指令的情況將不會被編譯和鏈接,直到等到他們的模版被異步加載成功后,而他們的編譯和鏈接過程都很被掛起直到加載成功.
在post-linking函數中處理DOM是安全的,不用等待異步模版被解析.