关于angularjs:Angular指令 – 何时以及如何使用编译,控制器,预链接和后链接

Angular directives - when and how to use compile, controller, pre-link and post-link

在编写Angular指令时,可以使用以下任何函数来操作声明指令的元素的DOM行为,内容和外观:

  • 调节器
  • 前链路
  • 后链接

对于应该使用哪种功能似乎存在一些混淆。这个问题包括:

指令基础知识

  • 如何申报各种功能?
  • 源模板和实例模板有什么区别?
  • 执行指令函数的顺序是什么?
  • 这些函数调用之间还会发生什么?

功能性,做和不做

  • 调节器
  • 预链接
  • 帖子链接

相关问题:

  • 指令:链接vs编译与控制器。
  • 定义angular.js指令时,'controller','link'和'compile'函数之间的区别。
  • angularjs中的编译和链接函数有什么区别。
  • AngularJS指令中的pre-compile和post-compile元素之间的区别?
  • Angular JS指令 - 模板,编译或链接?
  • 在Angular js指令中发布链接与预链接。


执行指令函数的顺序是什么?

对于单个指令

基于以下plunk,请考虑以下HTML标记:

1
2
3
<body>
   
</body>

使用以下指令声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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)'  );
                }
            };
         }
     };  

});

控制台输出将是:

1
2
3
4
some-div (compile)
some-div (controller)
some-div (pre-link)
some-div (post-link)

我们可以看到先执行compile,然后执行controller,然后执行pre-link,最后执行post-link

对于嵌套指令

Note: The following does not apply to directives that render their children in their link function. Quite a few Angular directives do so (like ngIf, ngRepeat, or any directive with transclude). These directives will natively have their link function called before their child directives compile is called.

原始HTML标记通常由嵌套元素组成,每个元素都有自己的指令。如下面的标记(参见plunk):

1
2
3
4
5
6
<body>
   
       
       
   
</body>

控制台输出如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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。从图形上看,我们可以这样表达:

An image illustrating the compilation loop for children

或许重要的是要提到,在这个阶段,编译函数获取的模板是源模板(而不是实例模板)。

链接阶段

DOM实例通常只是将源模板呈现给DOM的结果,但它们可以由ng-repeat创建,或者即时引入。

每当具有指令的元素的新实例被呈现给DOM时,链接阶段就开始了。

在这个阶段,Angular调用controllerpre-link,迭代子节点,并在所有指令上调用post-link,如下所示:

An illustration demonstrating the link phase steps


这些函数调用之间还会发生什么?

各种指令函数在另外两个名为$compile的角度函数(执行指令的compile)和一个名为nodeLinkFn的内部函数中执行(其中指令的controllerpreLinkpostLink是执行)。在调用指令函数之前和之后,角函数内发生了各种各样的事情。也许最值得注意的是儿童递归。以下简化图显示了编译和链接阶段的关键步骤:

An illustration showing Angular compile and link phases

为了演示这些步骤,我们使用以下HTML标记:

1
2
3
    <my-element>
        Inner content
    </my-element>

使用以下指令:

1
2
3
4
5
6
7
myApp.directive( 'myElement', function() {
    return {
        restrict:   'EA',
        transclude: true,
        template:   '{{label}}'
    }
});

compile API看起来像这样:

1
compile: function compile( tElement, tAttributes ) { ... }

参数通常以t为前缀,以表示提供的元素和属性是源模板的元素,而不是实例的元素和属性。

在调用compile之前,删除了已转换的内容(如果有),并将模板应用于标记。因此,提供给compile函数的元素将如下所示:

1
2
3
4
5
6
<my-element>
   
       "{{label}}"
       
   
</my-element>

请注意,此时不会重新插入已转换的内容。

在调用指令的.compile之后,Angular将遍历所有子元素,包括那些可能刚刚被指令引入的子元素(例如,模板元素)。

实例创建

在我们的例子中,将创建上面的源模板的三个实例(通过ng-repeat)。因此,以下序列将执行三次,每个实例一次。

调节器

controller API涉及:

1
controller: function( $scope, $element, $attrs, $transclude ) { ... }

进入链接阶段,通过$compile返回的链接功能现在提供了范围。

首先,如果请求,链接函数创建子范围(scope: true)或隔离范围(scope: {...})。

然后执行控制器,提供实例元素的范围。

预链接

pre-link API看起来像这样:

1
function preLink( scope, element, attributes, controller ) { ... }

在对指令的.controller.preLink函数的调用之间几乎没有任何反应。 Angular仍然建议如何使用每个。

.preLink调用之后,链接函数将遍历每个子元素 - 调用正确的链接函数并将当前作用域(作为子元素的父作用域)附加到其上。

帖子链接

post-link API类似于pre-link函数的API:

1
function postLink( scope, element, attributes, controller ) { ... }

也许值得注意的是,一旦调用了指令的.postLink函数,其所有子元素的链接过程就完成了,包括所有孩子的.postLink函数。

这意味着,当.postLink被调用时,孩子们"活着"就准备好了。这包括:

  • 数据绑定
  • 应用包含
  • 附加范围

因此,此阶段的模板将如下所示:

1
2
3
4
5
6
7
8
<my-element>
   
       "{{label}}"
                       
            Inner content
       
   
</my-element>


如何申报各种功能?

编译,控制器,预链接和后链接

如果要使用全部四个函数,该指令将遵循以下形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 lingo中我们说compile函数返回一个模板函数。

编译,控制器和后链接

如果不需要pre-link,则编译函数可以简单地返回post-link函数而不是定义对象,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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方法。为此,可以使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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函数:

1
2
3
4
5
6
7
8
9
10
11
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函数,可以使用:

1
2
3
4
5
6
7
8
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)。

角度术语有点不一致,但它仍然区分两种类型的标记:

  • 源模板 - 如果需要,可以克隆的标记。如果克隆,则此标记将不会呈现给DOM。
  • 实例模板 - 要呈现给DOM的实际标记。如果涉及克隆,则每个实例都将是克隆。

以下标记演示了这一点:

1
    <my-directive>{{i}}</my-directive>

源html定义

1
    <my-directive>{{i}}</my-directive>

它充当源模板。

但由于它包含在ng-repeat指令中,因此将克隆此源模板(在我们的示例中为3次)。这些克隆是实例模板,每个都将出现在DOM中并绑定到相关范围。


编译功能

当Angular bootstraps时,每个指令的compile函数只调用一次。

正式地说,这是执行(源)模板操作的地方,不涉及范围或数据绑定。

首先,这是为了优化目的;考虑以下标记:

1
2
3
<tr ng-repeat="raw in raws">
    <my-raw></my-raw>
</tr>

指令将呈现一组特定的DOM标记。所以我们可以:

  • 允许ng-repeat复制源模板(),然后修改每个实例模板的标记(在compile函数之外)。
  • 修改源模板以包含所需的标记(在compile函数中),然后允许ng-repeat复制它。

如果raws集合中有1000个项目,则后一个选项可能比前一个更快。

做:

  • 操纵标记,使其作为实例(克隆)的模板。

不要

  • 附加事件处理程序。
  • 检查子元素。
  • 设置对属性的观察。
  • 在示波器上设置手表。

控制器功能

每当实例化新的相关元素时,都会调用每个指令的controller函数。

正式地说,controller功能是一个:

  • 定义可在控制器之间共享的控制器逻辑(方法)。
  • 启动范围变量。

同样,重要的是要记住,如果指令涉及隔离范围,则其中继承自父范围的任何属性都不可用。

做:

  • 定义控制器逻辑
  • 启动范围变量

不要:

  • 检查子元素(它们可能尚未呈现,受范围限制等)。


后链接功能

当调用post-link函数时,所有先前的步骤都已发生 - 绑定,转换等。

这通常是进一步操纵渲染DOM的地方。

做:

  • 操纵DOM(渲染,因此实例化)元素。
  • 附加事件处理程序。
  • 检查子元素。
  • 设置对属性的观察。
  • 在示波器上设置手表。


预链接功能

每当实例化新的相关元素时,都会调用每个指令的pre-link函数。

如前面编译顺序部分所示,pre-link函数称为parent-then-child,而post-link函数称为child-then-parent

pre-link函数很少使用,但在特殊情况下可能很有用;例如,当子控制器向父控制器注册自己时,注册必须采用parent-then-child方式(ngModelController以这种方式执行)。

不要:

  • 检查子元素(它们可能尚未呈现,受范围限制等)。