web-dev-qa-db-ja.com

Angular ディレクティブ - コンパイル、コントローラー、プリリンク、ポストリンクの使用時期と使用方法

448
Izhaki

ディレクティブ関数はどの順序で実行されますか?

単一の指令の場合

次の plunk に基づいて、次の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を含むディレクティブなど)。これらのディレクティブは、beforeという名前の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)

ここでは、2つのフェーズ(compileフェーズと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

166
Izhaki

これらの関数呼び出しの間に他に何が起こりますか?

さまざまなディレクティブ関数は、$compile(ディレクティブのcompileが実行される場所)および内部関数nodeLinkFn(ディレクティブのcontrollerpreLink、およびpostLinkが実行される場所)という2つの角度関数の中から実行されます。ディレクティブ関数が呼び出される前後で、角度関数内ではさまざまなことが起こります。おそらく最も顕著なのは、子の再帰です。次の簡略図は、コンパイルフェーズとリンクフェーズの主な手順を示しています。

An illustration showing Angular compile and link phases

これらの手順を説明するために、次の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は、ディレクティブによって導入されたばかりのもの(テンプレート要素など)を含む、すべての子要素をトラバースします。

インスタンス作成

この例では、上記のソーステンプレートの3つのインスタンスが(ng-repeatによって)作成されます。したがって、次のシーケンスはインスタンスごとに1回、3回実行されます。

コントローラ

controller APIには以下が含まれます。

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

リンクフェーズに入ると、$compileを介して返されたリンク関数にスコープが提供されます。

最初に、リンク関数は要求されれば子スコープ(scope: true)または独立スコープ(scope: {...})を作成します。

その後、instance要素のスコープを指定して、コントローラが実行されます。

プレリンク

pre-link APIは以下のようになります。

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

ディレクティブの.controllerの呼び出しと.preLink関数の呼び出しの間に事実上何も起こりません。 Angularは、それぞれの使用方法に関する推奨事項を引き続き提供します。

.preLink呼び出しに続いて、link関数は各子要素をトラバースします - 正しいlink関数を呼び出し、それに現在のスコープ(子要素の親スコープとして機能する)をそれにアタッチします。

ポストリンク

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>
90
Izhaki

さまざまな関数を宣言する方法

コンパイル、コントローラー、プレリンクとポストリンク

4つの関数すべてを使用する場合、ディレクティブはこの形式に従います。

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 lingoでは、コンパイル関数は テンプレート関数 を返します。

コンパイル、コントローラ、ポストリンク

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)compileメソッドが定義された後に、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                 
        },          
    };  
});
43
Izhaki

ソーステンプレート インスタンステンプレート の違いは何ですか?

AngularがDOM操作を許可するという事実は、コンパイルプロセスへの入力マークアップが出力と異なることがあることを意味します。特に、いくつかの入力マークアップはDOMにレンダリングされる前に数回クローンされるかもしれません(ng-repeatのように)。

Angularの用語は少し矛盾していますが、それでも2種類のマークアップを区別します。

  • ソーステンプレート - 必要に応じて複製するマークアップ。複製した場合、このマークアップは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に表示され、関連する範囲にバインドされます。

30
Izhaki

コンパイル機能

各ディレクティブのcompile関数は、Angularブートストラップのときに一度だけ呼び出されます。

公式には、これはスコープやデータバインディングを含まない(ソース)テンプレート操作を実行する場所です。

主に、これは最適化の目的で行われます。次のマークアップを検討してください。

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

<my-raw>ディレクティブはDOMマークアップの特定のセットをレンダリングします。だから我々はどちらかできます。

  • ng-repeatにソーステンプレート(<my-raw>)の複製を許可してから、各インスタンステンプレートのマークアップを(compile関数の外側で)変更します。
  • compile関数内の)目的のマークアップを含むようにソーステンプレートを変更し、ng-repeatでそれを複製できるようにします。

rawsコレクションに1000個のアイテムがある場合、後者のオプションは前者のものより速いかもしれません。

行う:

  • マークアップを操作して、インスタンス(クローン)のテンプレートとして機能するようにします。

しない

  • イベントハンドラを添付してください。
  • 子要素を調べます。
  • 属性の観測を設定します。
  • スコープに時計を設定します。
23
Izhaki

コントローラ機能

各ディレクティブのcontroller関数は、新しい関連要素がインスタンス化されるたびに呼び出されます。

公式には、controller関数は次のとおりです。

  • コントローラ間で共有できるコントローラロジック(メソッド)を定義します。
  • スコープ変数を開始します。

繰り返しますが、ディレクティブに独立スコープが含まれている場合、その親スコープから継承されたその中のプロパティはまだ使用できないことを覚えておくことが重要です。

行う:

  • コントローラロジックを定義する
  • スコープ変数を開始する

しない:

  • 子要素を調べます(まだレンダリングされていない、スコープにバインドされているなど)。
19
Izhaki

リンク後機能

post-link関数が呼び出されると、バインド、除外など、これまでのすべてのステップが実行されています。

これは通常、レンダリングされたDOMをさらに操作するための場所です。

行う:

  • DOM(レンダリングされた、したがってインスタンス化された)要素を操作します。
  • イベントハンドラを添付してください。
  • 子要素を調べます。
  • 属性の観測を設定します。
  • スコープに時計を設定します。
19
Izhaki

プリリンク機能

各ディレクティブのpre-link関数は、新しい関連要素がインスタンス化されるたびに呼び出されます。

コンパイル順のセクションで前述したように、pre-link関数はparent-then-childと呼ばれ、post-link関数はchild-then-parentと呼ばれます。

pre-link関数はめったに使われませんが、特別なシナリオで役に立つことができます。たとえば、子コントローラが自分自身を親コントローラに登録したとしても、登録はparent-then-child方式で行われる必要があります(ngModelControllerはこのようにして処理を行います)。

しない:

  • 子要素を調べます(まだレンダリングされていない、スコープにバインドされているなど)。
15
Izhaki