在编写Angular指令时,你可以使用以下任意函数来操作声明指令的元素的DOM行为、内容和外观:
对于应该使用哪个函数,似乎有些困惑。这个问题包括:
当post-link函数被调用时,之前的所有步骤都已经发生——绑定、传输等。
post-link
这通常是进一步操作呈现的DOM的地方。
如果要使用这四个函数,指令将遵循以下形式:
myApp.directive( 'myDirective', function () { return { restrict: 'EA', controller: function( $scope, $element, $attrs, $transclude ) { // Controller code goes here. }, compile: function compile( tElement, tAttributes, transcludeFn ) { // Compile code goes here. return { pre: function preLink( scope, element, attributes, controller, transcludeFn ) { // Pre-link code goes here }, post: function postLink( scope, element, attributes, controller, transcludeFn ) { // Post-link code goes here } }; } }; });
注意,compile返回一个同时包含pre-link和post-link函数的对象;在Angular术语中,我们说compile函数返回模板函数。
如果不需要pre-link, compile函数可以简单地返回post-link函数,而不是一个定义对象,如下所示:
pre-link
myApp.directive( 'myDirective', function () { return { restrict: 'EA', controller: function( $scope, $element, $attrs, $transclude ) { // Controller code goes here. }, compile: function compile( tElement, tAttributes, transcludeFn ) { // Compile code goes here. return function postLink( scope, element, attributes, controller, transcludeFn ) { // Post-link code goes here }; } }; });
有时,在定义了(post) link方法之后,希望添加compile方法。为此,我们可以使用:
link
compile
myApp.directive( 'myDirective', function () { return { restrict: 'EA', controller: function( $scope, $element, $attrs, $transclude ) { // Controller code goes here. }, compile: function compile( tElement, tAttributes, transcludeFn ) { // Compile code goes here. return this.link; }, link: function( scope, element, attributes, controller, transcludeFn ) { // Post-link code goes here } }; });
如果不需要编译函数,则可以完全跳过它的声明,并在指令配置对象的link属性下提供post-link函数:
myApp.directive( 'myDirective', function () { return { restrict: 'EA', controller: function( $scope, $element, $attrs, $transclude ) { // Controller code goes here. }, link: function postLink( scope, element, attributes, controller, transcludeFn ) { // Post-link code goes here }, }; });
在上面的任何例子中,如果不需要,可以简单地删除controller函数。例如,如果只需要post-link函数,可以使用:
controller
myApp.directive( 'myDirective', function () { return { restrict: 'EA', link: function postLink( scope, element, attributes, controller, transcludeFn ) { // Post-link code goes here }, }; });
事实上,Angular允许DOM操作,这意味着编译过程中的输入标记有时与输出标记不同。特别是,一些输入标记在呈现给DOM之前可能会被克隆几次(比如使用ng-repeat)。
ng-repeat
Angular的术语有点不一致,但它仍然区分了两种类型的标记:
下面的标记演示了这一点:
<div ng-repeat="i in [0,1,2]"> <my-directive>\{\{i}}</my-directive> </div>
源html定义了
<my-directive>\{\{i}}</my-directive>
作为源模板。
但是由于它被包装在ng-repeat指令中,这个源模板将被克隆(在我们的例子中是3次)。这些克隆是实例模板,每个都将出现在DOM中,并被绑定到相关的作用域。
基于下面的砰砰作响,考虑下面的HTML标记:
<body> <div log='some-div'></div> </body>
使用以下指令声明:
myApp.directive('log', function() { return { controller: function( $scope, $element, $attrs, $transclude ) { console.log( $attrs.log + ' (controller)' ); }, compile: function compile( tElement, tAttributes ) { console.log( tAttributes.log + ' (compile)' ); return { pre: function preLink( scope, element, attributes ) { console.log( attributes.log + ' (pre-link)' ); }, post: function postLink( scope, element, attributes ) { console.log( attributes.log + ' (post-link)' ); } }; } }; });
控制台输出将是:
some-div (compile) some-div (controller) some-div (pre-link) some-div (post-link)
我们可以看到compile首先被执行,然后是controller,然后是pre-link,最后是post-link。
注意:以下内容不适用于在link函数中呈现子函数的指令。相当多的Angular指令是这样做的(比如ngIf、ngRepeat或任何带有transclude的指令)。这些指令的link函数被称为之前,它们的子指令被称为compile。
transclude
原始的HTML标记通常由嵌套的元素组成,每个元素都有自己的指令。就像下面的标记(参见砰砰作响):
<body> <div log='parent'> <div log='..first-child'></div> <div log='..second-child'></div> </div> </body>
控制台输出如下所示:
// The compile phase parent (compile) ..first-child (compile) ..second-child (compile) // The link phase parent (controller) parent (pre-link) ..first-child (controller) ..first-child (pre-link) ..first-child (post-link) ..second-child (controller) ..second-child (pre-link) ..second-child (post-link) parent (post-link)
我们可以在这里区分两个阶段——编译阶段和链接阶段。
当加载DOM时,Angular开始编译阶段,在这个阶段,它自顶向下遍历标记,并对所有指令调用compile。从图形上来说,我们可以这样表达:
值得一提的是,在此阶段,compile函数获得的模板是源模板(而不是实例模板)。
DOM实例通常只是一个源模板被呈现给DOM的结果,但它们可能是由ng-repeat创建的,或者是动态引入的。
每当一个带有指令的元素的新实例被呈现给DOM时,链接阶段就开始了。
在这个阶段,Angular会对所有指令调用controller、pre-link、迭代子函数,并调用post-link,如下所示:
各种指令函数在另外两个名为$compile的角函数(指令的compile在这里执行)和一个名为nodeLinkFn的内部函数(指令的controller、preLink和postLink在这里执行)中执行。在调用指令函数之前和之后,angular函数中会发生各种事情。也许最值得注意的是子递归。下面的简化图显示了编译和链接阶段的关键步骤:
$compile
nodeLinkFn
preLink
postLink
为了演示这些步骤,让我们使用下面的HTML标记:
<div ng-repeat="i in [0,1,2]"> <my-element> <div>Inner content</div> </my-element> </div>
使用以下指令:
myApp.directive( 'myElement', function() { return { restrict: 'EA', transclude: true, template: '<div>\{\{label}}<div ng-transclude></div></div>' } });
compile API看起来像这样:
compile: function compile( tElement, tAttributes ) { ... }
参数通常以t作为前缀,表示所提供的元素和属性是源模板的元素和属性,而不是实例的元素和属性。
t
在对compile的调用之前,删除被传输的内容(如果有的话),并将模板应用于标记。因此,提供给compile函数的元素将如下所示:
<my-element> <div> "\{\{label}}" <div ng-transclude></div> </div> </my-element>
请注意,此时未重新插入所传输的内容。
在调用指令的.compile之后,Angular将遍历所有子元素,包括指令刚刚引入的那些元素(例如模板元素)。
.compile
在我们的例子中,上面的源模板的三个实例将被创建(通过ng-repeat)。因此,下面的序列将执行三次,每个实例一次。
controller API包括:
controller: function( $scope, $element, $attrs, $transclude ) { ... }
进入链接阶段,通过$compile返回的链接函数现在具有作用域。
首先,如果需要,link函数创建一个子作用域(scope: true)或一个隔离作用域(scope: {...})。
scope: true
scope: {...}
然后执行控制器,并提供实例元素的作用域。
pre-link API看起来像这样:
function preLink( scope, element, attributes, controller ) { ... }
实际上,在调用指令的.controller和.preLink函数之间没有发生任何事情。Angular仍然提供了如何使用它们的建议。
.controller
.preLink
在.preLink调用之后,link函数将遍历每个子元素——调用正确的link函数并将当前作用域(用作子元素的父作用域)附加到它。
post-link API类似于pre-link函数:
function postLink( scope, element, attributes, controller ) { ... }
也许值得注意的是,一旦一个指令的.postLink函数被调用,它的所有子元素的链接过程就完成了,包括所有子元素的.postLink函数。
.postLink
这意味着在.postLink被调用时,子对象已经“活”了。这包括:
这个阶段的模板看起来是这样的:
<my-element> <div class="ng-binding"> "\{\{label}}" <div ng-transclude> <div class="ng-scope">Inner content</div> </div> </div> </my-element>
每个指令的compile函数只在Angular启动时被调用一次。
正式来说,这是执行不涉及范围或数据绑定的(源)模板操作的地方。
这样做主要是为了优化;考虑下面的标记:
<tr ng-repeat="raw in raws"> <my-raw></my-raw> </tr>
<my-raw>指令将呈现一组特定的DOM标记。所以我们可以:
<my-raw>
如果raws集合中有1000个项,后一个选项可能比前一个选项更快。
raws
每当一个新的相关元素被实例化时,每个指令的controller函数都会被调用。
正式来说,controller函数是:
同样,重要的是要记住,如果指令涉及一个孤立的作用域,那么其中继承自父作用域的任何属性都还不可用。
每当一个新的相关元素被实例化时,每个指令的pre-link函数都会被调用。
正如前面在编译顺序一节中看到的,pre-link函数被称为父-然后子函数,而post-link函数被称为child-then-parent。
child-then-parent
pre-link函数很少使用,但在特殊情况下可能有用;例如,当子控制器将自己注册到父控制器,但必须以parent-then-child的方式注册(ngModelController是这样做的)。
parent-then-child
ngModelController