Angular指令-何时以及如何使用编译,控制器,预链接和后链接[关闭]


451

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

  • 编译
  • 控制者
  • 预链接
  • 链接后

关于应该使用哪个功能似乎有些混乱。这个问题包括:

指令基础

功能性质,做与不做

相关问题:


27
什么什么?
haimlit 2014年

2
@Ian请参阅:运算符重载。本质上,这是针对社区Wiki的。有关相关问题的答案太多是局部的,没有提供完整的图片。
Izhaki 2014年

8
这是很棒的内容,但是我们要求这里的所有内容都保持在问答格式内。也许您想将其分解为多个离散的问题,然后从标签Wiki链接到它们?
柔印

57
即使这篇文章是题外话并且是博客形式,它对于提供Angular指令的深入解释也是最有用的。管理员,请不要删除此帖子!
训ege

12
老实说,我什至不理会原始文档。一个stackoverflow帖子或一个博客通常可以让我在几秒钟内完成工作,而不是花15到30分钟才能理解原始文档。
大卫

Answers:


168

指令功能按什么顺序执行?

对于单个指令

基于以下普拉克,请考虑以下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

对于嵌套指令

注意:以下内容不适用于在链接函数中呈现其子级的指令。不少Angular指令都这样做(例如ngIf,ngRepeat或带有的任何指令transclude)。这些指令将在link调用其子指令之前,先调用其函数compile

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

<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所有指令。在图形上,我们可以这样表示:

该图说明了儿童的编译循环

值得一提的是,在此阶段,编译功能获得的模板是源模板(而不是实例模板)。

链接阶段

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

每当带有指令的元素的新实例呈现到DOM时,链接阶段就会开始。

在此阶段,Angular调用controllerpre-link迭代子级,然后调用post-link所有指令,如下所示:

演示链接阶段步骤的图示


5
@lzhaki流程图看起来不错。介意分享图表工具的名称吗?:)
merlin 2014年

1
@merlin我使用过OmniGraffle(但可以使用illustrator或inkscape-除了速度,就此插图而言,OmniGraffle没有比其他图表工具更好的功能了)。
伊扎基

2
@Anant的矮人消失了,所以这是一个新的矮人plnkr.co/edit/kZZks8HN0iFIY8ZaKJkA?p=preview打开JS控制台以查看日志语句

为什么当ng-repeat用于子代指令时这不是真的???参见plunk
Luckylooke

@Luckylooke你普拉克已根据NG-重复没有得到孩子的指令(即,什么东西被重复是一个指令一个模板,如果会,你会看到他们的编译是NG-重复的链接后,才调用。
Izhaki 2015年

90

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

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

该图显示了Angular编译和链接阶段

为了演示这些步骤,让我们使用以下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>'
    }
});

编译

compileAPI如下所示:

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

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

在调用被compile包含内容(如果有的话)之前,将其删除,并将模板应用于标记。因此,提供给该compile函数的元素将如下所示:

<my-element>
    <div>
        "{{label}}"
        <div ng-transclude></div>
    </div>
</my-element>

请注意,此时已插入的内容尚未重新插入。

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

实例创建

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

控制者

controllerAPI涉及:

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

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

首先,如果需要,链接功能会创建一个子作用域(scope: true)或一个隔离作用域(scope: {...})。

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

预先连结

pre-linkAPI如下所示:

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

在指令.controller.preLink函数的调用之间几乎没有任何反应。Angular仍然提供有关如何使用它们的建议。

.preLink呼叫,链接功能会遍历每个子元素-调用正确的链接功能,并连接到它的电流范围(其作为子元素的父范围)。

链接后

post-linkAPI与该pre-link功能相似:

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

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

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

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

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

<my-element>
    <div class="ng-binding">
        "{{label}}"
        <div ng-transclude>                
            <div class="ng-scope">Inner content</div>
        </div>
    </div>
</my-element>

3
您是如何创建此图形的?
罗伊·纳米尔

6
@RoyiNamir Omnigraffle。
Izhaki

43

如何声明各种功能?

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

如果要使用所有四个功能,则指令将遵循以下形式:

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返回一个同时包含前链接和后链接功能的对象;在Angular语言中,我们说compile函数返回一个模板函数

编译,控制器和后链接

如果pre-link没有必要,则compile函数可以简单地返回post-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                 
            };
        }
    };  
});

有时,一个人希望在定义compile(post)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 this.link;
        },
        link: function( scope, element, attributes, controller, transcludeFn ) {
            // Post-link code goes here
        }

    };  
});

控制器和后链接

如果不需要编译函数,则可以完全跳过其声明,并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需要功能,则可以使用:

myApp.directive( 'myDirective', function () {
    return {
        restrict: 'EA',
        link: function postLink( scope, element, attributes, controller, transcludeFn ) {
                // Post-link code goes here                 
        },          
    };  
});

31

源模板实例模板有什么区别?

Angular允许DOM操作的事实意味着编译过程中的输入标记有时与输出有所不同。特别是,某些输入标记ng-repeat在呈现给DOM之前可能会被克隆几次(如)。

角度术语有点不一致,但是仍然可以区分两种标记:

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

以下标记说明了这一点:

<div ng-repeat="i in [0,1,2]">
    <my-directive>{{i}}</my-directive>
</div>

源HTML定义

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

作为源模板。

但是,由于将其包装在ng-repeat指令中,因此该源模板将被克隆(在本例中为3次)。这些克隆是实例模板,每个都会出现在DOM中并绑定到相关范围。


23

编译功能

compileAngular引导时,每个指令的函数仅被调用一次。

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

首先,这样做是出于优化目的;考虑以下标记:

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

<my-raw>指令将呈现一组特定的DOM标记。因此,我们可以:

  • 允许ng-repeat复制源模板(<my-raw>),然后修改每个实例模板的标记(在compile函数外部)。
  • 修改源模板以使其包含所需的标记(在compile函数中),然后允许其ng-repeat进行复制。

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

做:

  • 操作标记,以便将其用作实例(克隆)的模板。

不要

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

20

控制器功能

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

正式地,该controller功能是其中之一:

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

同样,重要的是要记住,如果该指令涉及一个隔离的作用域,则它中从父作用域继承的任何属性都将不可用。

做:

  • 定义控制器逻辑
  • 初始化范围变量

不要:

  • 检查子元素(它们可能尚未呈现,绑定到范围等)。

很高兴您在指令中提到Controller是初始化范围的好地方。我很难发现这一点。
jsbisht 2015年

1
控制器不会“初始化范围”,它仅访问已经独立启动的范围。
Dmitri Zaitsev 2015年

@DmitriZaitsev注重细节。我修改了文本。
伊扎基

19

链接后功能

post-link调用该函数时,所有先前的步骤都已执行-绑定,包含等。

通常,这是进一步处理渲​​染的DOM的地方。

做:

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

9
如果有人使用链接功能(没有前链接或后链接),则很高兴知道它等同于后链接。
2015年

15

预链接功能

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

如之前在编译顺序部分中所见,pre-link函数称为父-子-孩子,而post-link函数称为child-then-parent

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

不要:

  • 检查子元素(它们可能尚未呈现,绑定到范围等)。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.