Angular指令 - 何时以及如何使用compile、controller、pre-link和post-link?

454
当编写Angular指令时,可以使用以下任何函数来操作指令声明的元素的DOM行为、内容和外观:
  • compile(编译)
  • controller(控制器)
  • pre-link(前置链接)
  • post-link(后置链接)
似乎存在一些混淆,关于应该使用哪个函数。这个问题涵盖了:

指令基础

函数性质、要做和不要做的事情

相关问题:


27
这句话的意思是“什么?”或者“啥?”,有些惊讶或困惑的语气。 - haimlit
8
这篇内容很棒,但我们要求所有内容都必须符合问答格式。也许你可以将其分成多个离散的问题,然后从标签维基中链接到它们? - Flexo
2
这个问题似乎不适合讨论,因为它并不是一个实际的问题。 - George Stocker
57
虽然这篇帖子离题,并且以博客形式出现,但它为Angular指令提供了深入的解释,非常有用。请管理员不要删除这篇帖子! - Exegesis
12
老实说,我根本不会去翻原始文档。一篇Stackoverflow帖子或博客通常在几秒钟内就能让我开始工作,而理解原始文档则需要15-30分钟的时间来抓狂。 - David
显示剩余4条评论
8个回答

170

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

针对单个指令

基于以下示例,考虑以下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 的任何指令)。这些指令将在其子指令compile 被调用之前自然地调用它们的link 函数。
通常,原始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。我们可以用图形来表示它: 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

5
流程图看起来不错。能分享一下这个绘图工具的名字吗? :) - merlin
1
@merlin 我用过OmniGraffle(但也可以使用Illustrator或Inkscape - 除了速度之外,就这个插图而言,OmniGraffle没有比其他制图工具更好的地方)。 - Izhaki
2
@Anant的plunker消失了,这里有一个新的:http://plnkr.co/edit/kZZks8HN0iFIY8ZaKJkA?p=preview 打开JS控制台以查看日志语句。 - user879121
如果我们想要从自定义指令动态地向 DOM 中添加另一个指令(例如 ng-click),应该在编译阶段还是链接阶段完成呢? - floribon
@Izhaki 我知道,但是关于这些编译/链接的问题有很多,我相信我最终会得到一个重复的标志。我想在同一元素上添加该指令:我怀疑我应该删除 my-custom-dir 属性并添加 ng-click 属性,然后重新编译整个程序,但不确定在哪里或何时进行。 - floribon
显示剩余3条评论

93

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

各种指令函数是从两个叫做 $compile(执行指令的 compile 函数)和一个内部函数 nodeLinkFn(执行指令的 controllerpreLinkpostLink 函数)中执行的。在指令函数被调用之前和之后,Angular 函数内会发生各种事情。其中最显著的是子递归。下面是编译和链接阶段的主要步骤示意图:

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>'
    }
});

编译

compile API 如下:

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

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

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

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

请注意,传递的内容此时不会被重新插入。
在调用指令的.compile之后,Angular会遍历所有子元素,包括指令刚刚引入的那些(比如模板元素)。
在我们的例子中,源模板将创建三个实例(由ng-repeat创建)。因此,以下序列将执行三次,每次都是一个实例。
控制器API涉及:
controller: function( $scope, $element, $attrs, $transclude ) { ... }

进入链接阶段后,通过$compile返回的链接函数现在会提供一个作用域。

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

然后执行控制器,使用实例元素的作用域。

预链接

pre-link API 如下:

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

几乎在指令的.controller被调用和.preLink函数之间没有任何事情发生。Angular仍然提供了关于如何使用每个函数的建议。
在.preLink调用后,链接函数将遍历每个子元素 - 调用正确的链接函数并将当前作用域附加到它上面(作为子元素的父级作用域)。
后链接
.post-link API类似于.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
你是怎么创作这幅画的? - Royi Namir
6
@RoyiNamir Omnigraffle 翻译成中文是「奥尼格拉夫」。 - Izhaki

45

如何声明各种函数?

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

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

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
                }
            };
        }
    };  
});

需要注意的是,编译函数返回一个包含前链接和后链接函数的对象;在 Angular 中我们说编译函数返回一个模板函数

编译、控制器和后链接

如果不需要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方法。为此,可以使用:

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                 
        },          
    };  
});

32

什么是源模板实例模板之间的区别?

Angular允许DOM操作,这意味着输入到编译过程的标记有时与输出不同。特别是,在渲染到DOM之前,一些输入标记可能会被克隆几次(例如使用ng-repeat)。

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

  • 源模板 - 要克隆的标记(如果需要)。如果克隆,则不会将此标记呈现到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中并绑定到相关作用域。


24

编译函数

每个指令的compile函数只在Angular引导时被调用一次。

官方文档称这个函数用于执行与作用域或数据绑定无关的(源)模板操作。

主要目的是为了优化;考虑以下标记:

<tr ng-repeat="raw in raws">
    <my-raw></my-raw>
</tr>
< p > <my-raw>指令将呈现一组特定的DOM标记。因此我们可以:

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

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

操作:

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

不要:

  • 附加事件处理程序。
  • 检查子元素。
  • 设置属性的观察。
  • 在范围上设置监视。

20

控制器函数

每个指令的controller函数在实例化一个新的相关元素时被调用。

官方文档中,controller函数的作用有:

  • 定义可能在多个控制器之间共享的控制器逻辑(方法)。
  • 初始化作用域变量。

同样需要记住的是,如果指令涉及隔离作用域,在其中继承自父作用域的任何属性都还不可用。

应该做:

  • 定义控制器逻辑
  • 初始化作用域变量

不应该做:

  • 检查子元素(它们可能还没有呈现、绑定到作用域等)。

很高兴你提到在指令中使用控制器(Controller)来初始化作用域(scope),这是一个很好的方法。我之前花了很长时间才发现这个技巧。 - jsbisht
1
控制器不会“初始化作用域”,它只是访问已经独立于其之外初始化的作用域。 - Dmitri Zaitsev
@DmitriZaitsev 注意细节做得很好。我已经修改了文本。 - Izhaki

19

后链接函数

后链接函数(post-link function)被调用时,之前的所有步骤都已经完成-绑定、转置等。

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

应该做:

  • 操作DOM (已渲染和实例化的)元素。
  • 附加事件处理程序。
  • 检查子元素。
  • 设置属性的观察。
  • 在作用域上设置观察。

9
如果有人使用链接功能(没有预链接或后链接),那么了解它相当于后链接是很好的。 - Asaf David

16

预链接函数

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

如前文所述,在编译顺序部分中,pre-link函数按父-子顺序调用,而post-link函数按子-父顺序调用。

pre-link函数很少使用,但在特殊情况下可能非常有用;例如,当子控制器向父控制器注册自身时,但注册必须以父-子的形式进行(ngModelController就是这样做的)。

不要:

  • 检查子元素(它们可能尚未被渲染、绑定到作用域等)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接