AngularJSのCompileとLinkってなぁに？

## はじめに

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

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


## おさらい
「[AngularJSのDirectiveを理解する](http://qiita.com/Quramy/items/dd4e7d2693c32d92048c)」にも書いているが、ざっくり言うと、Angularのディレクティブがテンプレートから実際のViewを描画する際は、Compile → Linkの2フェーズに分かれている。

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

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

```js
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フェーズに分割されている理由が書かれているので、ざっくり意訳。

>```html: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の意識が必要となるのは、思いつく範囲では、下記あたりか。

 * ツリー構造(フォルダ, 掲示板のスレッド)を保持するscopeに対して、これを再帰的に処理するディレクティブ  
(「[AngularJSで再帰的なディレクティブを作成して木構造を表現する](http://qiita.com/Quramy/items/9e7a45e9333451ebff07)」に記載があります.)

## 擬似的にコードで解説

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

```js:素朴な実装
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);
      });
    }
  };
}]);
```

```js: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`は以下のサービスコードを作成：

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

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

 * 素朴版：描画関数部分で約4.8秒
![naive.png](https://qiita-image-store.s3.amazonaws.com/0/8808/217f2c5b-5bf6-2834-472a-a2c6bbd6d5ad.png)

 * 2フェーズ版：描画関数部分で約3.8秒
![twoPhase.png](https://qiita-image-store.s3.amazonaws.com/0/8808/9942c389-91d3-38e6-723a-f8c388918bee.png)

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

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

## まとめ

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