LoginSignup
52
47

More than 5 years have passed since last update.

AngularJSのCompileとLinkってなぁに?

Last updated at Posted at 2014-07-18

はじめに

AngularJSのDirectiveについては、カスタムディレクティブの作成方法含め、おおよそ理解していたつもりであったが、CompileとLinkについて、若干自信が無い部分があったので、勉強がてら少し厚めに書いてみた。

なお、公式の開発ガイドにも、Oreillyの書籍でも書かれているが、カスタムディレクティブを開発することがあっても、殆どのケースでは、Linkの処理のみを気にするだけでよいため、あんまりこのエントリは役には立ちません。

おさらい

AngularJSのDirectiveを理解する」にも書いているが、ざっくり言うと、Angularのディレクティブがテンプレートから実際のViewを描画する際は、Compile → Linkの2フェーズに分かれている。

  • Compile:ディテクティブのテンプレート(html文字列ではなく、DOMオブジェクト)から、Link関数を生成する.
  • Link:生成されたLink関数とディレクティブのScopeオブジェクトを紐付けて、View(HTML要素)を完成させる.

ディレクティブを作成する際、Compileフェーズを意識した記載方法として、↓のパターンがある.

myModule.directive('myDirective', function(){
  return {
    // templateやrestrict等は割愛.

    // Compile関数はLink関数を返却するように実装する.
    compile: function(tElement, tAttr){
      return function postLink(scope, element, attrs){
        // DOMとscopeの双方向バインディング処理.
      };
    }
  };
});

何で分けてんの?

https://docs.angularjs.org/guide/compiler#an-example-of-compile-versus-link- に、CompileとLinkの2フェーズに分割されている理由が書かれているので、ざっくり意訳。

ng-repeatの例
Hello {{user.name}}, you have these actions:
<ul>
  <li ng-repeat="action in user.actions">
    {{action.description}}
  </li>
</ul>

ngRepeatはジレンマを抱えている。

user.actionsコレクションに含まれている全てのactionオブジェクトに対して、<li>要素を複製しなくてはならない。これは、Viewの描画後にuser.actionsコレクションに対して、オブジェクトの追加/削除が発生することを考えると、少し複雑である. 描画後変更に耐えうるためには、複製の元となる(綺麗な状態の)<li>要素を保持しておく必要がある.
 (中略)
素朴な実装は、コピー元要素の<li>を複製して、<ul>へinsertし、{{action.description}}部分をコンパイルし、scopeの値の評価を行う方法である. しかし、この方法は非効率的である。複製した<li>要素の全てにおいて、<li>内要素をトラバースして、内包されるディレクティブを探索することとなる。この探索処理は、コンパイル全体を重たくする原因となる。

この問題を解決するため、コンパイルを2つのフェーズに分離する.

ディレクティブのコンパイル全体のうち、「scopeの値に依存する・依存しない」の違いがCompile/Linkの違いとも言えよう.

  • Compile:テンプレートのhtmlが読み込まれる際に1度だけ実行される
  • Link:user.actionsの要素数繰り返される. user.actionsにオブジェクトが追加された場合も実行される

これを考えると、カスタムディレクティブを作成する上で、compile関数の引数にはscopeは存在しないが、link関数の第1引数がscopeである点も得心がいくと思う。

また、開発ガイドや書籍で「殆どのケースではCompileとLinkを使い分ける必要はない」と書かれているのは、scopeに応じて、テンプレート内の要素が描画後に複製されるようなケースでない限り、Compile/Linkに分割した・しないが関係無いからである。

ngRepeat 以外で、CompileとLinkの意識が必要となるのは、思いつく範囲では、下記あたりか。

擬似的にコードで解説

折角なので、上記の「素朴な実装」版と「2フェーズ版」のrepeat風ディレクティブを書いてみた。

素朴な実装
angular.module('myApp').directive('myNaiveRepeat', ['myCompiler', function(myCompiler){
  var childTemplate = angular.element('<li>name:{{child.name}}, vendor:{{child.vendor}}</li>');
  return{
    scope:{
      myNaiveRepeat: '='
    },
    link: function(scope, element){

      //  描画関数.
      var rendering = function naiveRendering(collection){
        element.empty();
        collection.forEach(function(child){
          var cs = scope.$new();
          cs.child = child;
          //  ★子テンプレートをコピーし、コレクション内要素と結合.
          //  (myCompilerはディレクティブ探索とscope評価をする関数を仮定. 実際のangularにはそんなものない.)
          element.append(myCompiler(childTemplate.clone(), cs));
        });
      };

      // 初回描画
      rendering(scope.myNaiveRepeat);

      // コレクションに変更があれば、再描画する.
      scope.$watchCollection('myNaiveRepeat', function(newCollection){
        rendering(newCollection);
      });
    }
  };
}]);
2フェーズ版
angular.module('myApp').directive('myTwoPhaseRepeat', ['$compile', function($compile){
  var childTemplate = angular.element('<li>name:{{child.name}}, vendor:{{child.vendor}}</li>');
  return{
    scope:{
      myTwoPhaseRepeat: '='
    },
    compile: function(){
      // ☆テンプレートからリンク関数を作成しておく.
      var childLinkFunc = $compile(childTemplate);
      return function(scope, element){

        //  描画関数.
        var rendering = function linkedRendering(collection){
          element.empty();
          collection.forEach(function(child){
            var cs = scope.$new();
            cs.child = child;
            // ☆リンク関数を実行して、コレクション内要素と結合.
            childLinkFunc(cs, function(clonedElement){
              element.append(clonedElement);
            });
          });
        };

        // 初回描画
        rendering(scope.myTwoPhaseRepeat);

        // コレクションに変更があれば、再描画する.
        scope.$watchCollection('myTwoPhaseRepeat', function(newCollection){
          rendering(newCollection);
        });
      };
    }
  };
}]);
  • 素朴版: ★部分で<li>要素内の子ディレクティブ探索とscope評価を一緒くたに処理している(なお、myCompilerなんていうServiceはAngularJSには存在しない)
  • 2フェーズ版: compileフェーズでテンプレートから$compile関数でLink関数 childLinkFn を作成し(ここで<li>内探索は済ませる)、描画時(Linkフェーズからよばれる)は childLinkFn の実行のみを行う.

ちなみに $compile(template) 戻り値のLink関数 linkFn の使い方であるが、引数の与え方で結合後の要素の取り方が違う.

  1. var viewElement = linkFn(scope); の様に、scopeの値のみを引数にした場合、templateにしたDOM要素自体に、scopeの評価値がセットされたDOMエレメントが返却される.
  2. linkFn(scope, function(clonedElement){...}); の様に、第2引数に関数を仕込むと、元のテンプレートDOMを汚さずに、scopeが評価された描画用要素を clonedElement 経由で取得可能.

折角なので効果がどの程度か実験.

  • 1つ <li> 内に<input ng-model="name" />{{child.name}}を10個ずつ
  • scopeのコレクション要素数を0→1000に増幅

のボリューム条件を与えて、素朴版と2フェーズ版双方で描画に要した時間を計測してみた.

ちなみに、素朴版のmyCompilerは以下のサービスコードを作成:

素朴版用コンパイラ
angular.module('myApp').factory('myCompiler', ['$compile', function($compile){
  return function(template, scope){
    return $compile(template)(scope);
  };
}]);

Chromeのprofilerで計測した結果が下記:

  • 素朴版:描画関数部分で約4.8秒
    naive.png

  • 2フェーズ版:描画関数部分で約3.8秒
    twoPhase.png

2フェーズ版の方が、1秒くらい速い。要素数やDOMの状態でも変わるだろうが、20%程度の性能向上効果がある、ということだ。

なお、実際のngRepeatはもっと色々な工夫がされているので、上記の2フェーズ版より全然速い。

まとめ

  • Compile/Linkの分離はテンプレート要素がscopeに応じて複製されるようなディレクティブを作成するケースで有効.
  • カスタムディレクティブ内で適切に使いこなせば、AngularJSアプリの性能が上がる.
  • Compileはテンプレート解釈時に一回呼び出され、Link関数は紐付くscopeに応じて複数回呼び出される.
  • Link関数には、linkFn(scope, function(clonedElement){...}); の形式で複製済み描画要素としてscopeと結合できる.
52
47
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
52
47