指令是咱们用来扩展浏览器能力的技术之一。在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结构替换单个指令。这容许指令成为一个能够生成应用程序可重用组件的短标志。
加油!