在AngularJS应用起动前,它们以HTML文本的形式保存在文本编辑器中。应用启动后会进行编译和链接,作用域会同HTML进行绑定,应用可以对用户在HTML中进行的操作进行实时响应。这个神器的效果是如何发生的?创建高效率的应用需要了解什么?
在这个过程中总共有两个主要阶段。
第一个阶段是编译阶段。
在编译阶段,AngularJS会遍历整个HTML文档并根据JavaScript中的指令定义来处理页面上声明的指令。
每一个指令的模板中都可能含有另外一个指令,另外一个指令也可能会有自己的模板。当AngularJS调用HTML文档根部的指令时,会遍历其中所有的模板,模板中也可能包含带有模板的指令。
模板树可能又大又深,但有一点需要注意,尽管元素可以被多个指令所支持或修饰,这些指令本身的模板中也可以包含其他指令,但只有属于最高优先级指令的模板会被解析并添加到模板树中。这里有一个建议,就是将包含模板的指令和添加行为的指令分离开来。如果一个元素已经有一个含有模板的指令了,永远不要对其用另一个指令进行修饰。只有具有最高优先级的指令中的模板会被编译。
一旦对指令和其中的子模板进行遍历或编译,编译后的模板会返回一个叫做模板函数的函数。我们有机会在指令的模板函数被返回前,对编译后的DOM树进行修改。 在这个时间点DOM树还没有进行数据绑定,意味着如果此时对DOM树进行操作只会有很少的性能开销。基于此点,ng-repeat和ng-transclude等内置指令会在这个时候,也就是还未与任何作用域数据进行绑定时对DOM进行操作。
以ng-repeat为例,它会遍历指定的数组或对象,在数据绑定之前构建出对应的DOM结构。
如果我们用ng-repeat来创建无序列表,其中的每一个<li>都会被ng-click指令所修饰,这个过程会使得性能比手动创建列表要快得多,尤其是列表中含有上百个元素时。与克隆<li>元素,再将其与数据进行链接,然后对每个元素都循环进行此操作的过程不同,我们仅需要先将无需列表构造出来,然后将新的DOM(编译后的DOM)传递给指令生命周期中的下一个阶段,即链接阶段。一个指令的表现一旦编译完成,马上就可以通过编译函数对其进行访问,编译函数的签名包含有访问指令声明所在的元素(tElemente)及该元素其他属性(tAttrs)的方法。这个编译函数返回前面提到的模板函数,其中含有完整的解析树。这里的重点是,由于每个指令都可以有自己的模板和编译函数,每个模板返回的也都是自己的模板函数。链条顶部的指令会将内部子指令的模板合并在一起成为一个模板函数并返回,但在树的内部,只能通过模板函数访问其所处的分支。最后,模板函数被传递给编译后的DOM树中每个指令定义规则中指定的链接函数,
compile(对象或函数)
compile选项可以返回一个对象或函数。理解compile和link选项是AngularJS中需要深入讨论的高级话题之一,对于了解AngularJS究竟是如何工作的至关重要。compile选项本身并不会被频繁使用,但是link函数则会被经常使用。本质上,当我们设置了link选项,实际上是创建了一个postLink()链接函数,以便compile()函数可以定义链接函数。通常情况下,如果设置了compile函数,说明我们希望在指令和实时数据被放到DOM中之前进行DOM操作,在这个函数中进行诸如添加和删除节点等DOM操作是安全的。
compile和link选项是互斥的。如果同时设置了这两个选项,那么会把compile所返回的函数当作链接函数,而link选项本身则会被忽略。
1 // ... 2 compile: function(tEle, tAttrs, transcludeFn) { 3 var tplEl = angular.element('<div>' + 4 '<h2></h2>' + 5 '</div>'); 6 var h2 = tplEl.find('h2'); 7 h2.attr('type', tAttrs.type); 8 h2.attr('ng-model', tAttrs.ngModel); 9 h2.val("hello"); 10 tEle.replaceWith(tplEl); 11 return function(scope, ele, attrs) { 12 // 连接函数 13 }; 14 } 15 //...
如果模板被克隆过,那么模板实例和链接实例可能是不同的对象。因此在编译函数内部,我们只能转换那些可以被安全操作的克隆DOM节点。不要进行DOM事件监听器的注册:这个操作应该在链接函数中完成。
编译函数负责对模板DOM进行转换。
链接函数负责将作用域和DOM进行链接。
在作用域同DOM链接之前可以手动操作DOM。在实践中,编写自定义指令时这种操作是非常罕见的,但有几个内置指令提供了这样的功能。了解这个流程对于理解AngularJS真正的工作方式很有帮助。
链接
用link函数创建可以操作DOM的指令。链接函数是可选的。如果定义了编译函数,它会返回链接函数,因此当两个函数都定义了时,编译函数会重载链接函数。如果我们的指令很简单,并且不需要额外的设置,可以从工厂函数(回调函数)返回一个函数来代替对象。如果这样做了,这个函数就是链接函数。
下面两种定义指令的方式在功能上是完全一样的:
1 angular.module('myApp', []) 2 .directive('myDirective', function() { 3 return { 4 pre: function(tElement, tAttrs, transclude) { 5 // 在子元素被链接之前执行 6 // 在这里进行Don转换不安全 7 // 之后调用'lihk'h函数将无法定位要链接的元素 8 }, 9 post: function(scope, iElement, iAttrs, controller) { 10 // 在子元素被链接之后执行 11 // 如果在这里省略掉编译选项 12 //在这里执行DOM转换和链接函数一样安全吗 13 } 14 }; 15 }); 16 angular.module('myApp', []) 17 .directive('myDirective', function() { 18 return { 19 link: function(scope, ele, attrs) { 20 return { 21 pre: function(tElement, tAttrs, transclude) { 22 // 在子元素被链接之前执行 23 // 在这里进行Don转换不安全 24 // 之后调用'lihk'h函数将无法定位要链接的元素 25 }, 26 post: function(scope, iElement, iAttrs, controller) { 27 // 在子元素被链接之后执行 28 // 如果在这里省略掉编译选项 29 //在这里执行DOM转换和链接函数一样安全吗 30 } 31 } 32 } 33 });
当定义了编译函数来取代链接函数时,链接函数是我们能提供给返回对象的第二个方法,也就是postLink函数。本质上讲,这个事实正说明了链接函数的作用。它会在模板编译并同作用域进行链接后被调用,因此它负责设置事件监听器,监视数据变化和实时的操作DOM。
link函数对绑定了实时数据的DOM具有控制能力,因此需要考虑性能问题。回顾一下10.4节中关于性能的考虑,在选择是用编译函数还是链接函数实现功能时,将性能影响考虑进去。
链接函数的签名如下:
link: function(scope, element, attrs) {// 在这里操作DOM}
如果指令定义中有require选项,函数签名中会有第四个参数,代表控制器或者所依赖的指令的控制器。
1 // require 'SomeController', 2 link: function(scope, element, attrs, SomeController) { 3 // 在这里操作DOM,可以访问required指定的控制器 4 }
如果require选项提供了一个指令数组,第四个参数会是一个由每个指令所对应的控制器组成的数组。
下面看一下链接函数中的参数:
scope:指令用来在其内部注册监听器的作用域。
iElementiElement:参数代表实例元素,指使用此指令的元素。在postLink函数中我们应该只操作此元素的子元素,因为子元素已经被链接过了。
iAttrsiAttrs:参数代表实例属性,是一个由定义在元素上的属性组成的标准化列表,可以在所有指令的链接函数间共享。会以JavaScript对象的形式进行传递。
controller:controller参数指向require选项定义的控制器。如果没有设置require选项,那么controller参数的值为undefined。
控制器在所有的指令间共享,因此指令可以将控制器当作通信通道(公共API)。如果设置了多个require,那么这个参数会是一个由控制器实例组成的数组,而不只是一个单独的控制器。