🅰️

AngularJS Directiveの処理順を網羅してみた

2021/07/04に公開

こんにちは@armorik83です。

この記事はAngularJS Advent Calendar 2014の2日目として書かれたものです。AngularJSは好きと嫌いの両方を2度くらい経て、今は愛に近い。

AngularJSは現在1.3がリリースされていますが、2014年10月のng-europeにて2.0系のビジョンが示されました。リリース時期は未定ですが、AngularJS 1.x系で指摘された多くの問題点、開発チームの反省が活かされた次世代のAngularになるとされ、期待が大きいです。

その中で2.0系ではControllerが無くなるという気になる情報がありました。正確にはコントローラ自体が無くなるのではなくng-controllerが整理されるとの様子ですが、この代替として、Directiveを中心としていく方針が掲げられています。

今からDirectiveに親しんでおく

Directiveと聞くだけで苦手意識を露わにする意見をしばしば目にします。この理由を筆者なりに考えてみました。

  • DDO (Directive Definition Object) が煩雑。(restrictとかを書くオプション群のこと)
  • compile, link, ng-repeatの処理順が分からず、思い通りに実装できない。
  • Directiveをネストしたら処理順とか$scopeとか目も当てられない。
  • 複雑なことすると面倒そうだから小さなDirectiveの実装を試すに留まる。
  • だんだんイライラしてくる。

この辺りと予想しています。

AngularチームもDirectiveの煩雑さは反省点として感じているようで、2.0でのDDO廃止、AtScriptの導入などからそれが伺えます。しかし2.0のリリース時期は未定。今AngularJS 1.3と親しむためには先に上記の苦手意識を払拭すればいいと考えました。

思いつく限りを網羅してみた

苦手意識を払拭するために、自分の試したことのあるケース、まだ試していなかったケース、リスクを含んでいそうなケース、どれも引っ括めて思いつく限りのDirective処理順を試してみました。資料内ではconsole.log()にも色々吐いてますのでDev Toolsを開いた状態でご覧ください。

こちら

少々長いので、時間が無いならばいきなり結論まで飛んでもらって大丈夫です。

compileとlinkの違い

DDO APIにはcompilelinkというオプションがあります。一方で$compileというServiceもあるんですが、こういうところが分かりにくいと指摘される一因と感じます。Serviceの方の$compileは一旦忘れてください。

compile

このオプションには関数を指定します。

// 変数群の宣言は略

function Dir($rootScope) {
  this.$rootScope = $rootScope;
}

Dir.prototype.compile = function() {
  // compileの中身
};

function definition($rootScope) {
  var self = new Dir($rootScope);
  return {
    compile: self.compile.bind(self) // <-ここ!
  };
}

definition.$inject = ['$rootScope'];
angular.module(appName).directive(name, definition);

多くのチュートリアルとは異なる書き方なので違和感を覚える方もおられるでしょう。compileには関数型さえ指定すればよいので、compile: function(){...とせずこのようにしています。この発想はTypeScript由来です。

compileの実行タイミング

資料a-01からa-03は「compileだけを指定したDirective」を使っています。

  • a-01は1個。
  • a-02は3個並べて。
  • a-03は1個のDirectiveにng-repeatを指示して3回繰り返し。

特徴は02と03。a-02では3回compileが走るのに対し、ng-repeatを使ったa-03では1回だけcompileが走ります。

続いてlinklinkcompileと同じように関数型を指定します。

linkの実行タイミング

資料b-01からb-03は「linkだけを指定したDirective」を使っています。

  • b-01は1個。
  • b-02は3個並べて。
  • b-03は1個のDirectiveにng-repeatを指示して3回繰り返し。

ng-repeatを使ったa-03で1回だけcompileが走りましたが、b-03ではlinkが3回走ります。linkは表示される数だけ実行されます。

compileとlinkの混合

compilelinkの両方を使うときはDDOの記述に注意しなければなりません。

bad
{
  compile: function(tElem, tAttrs, tTransclude) {},
  link: function(scope, iElement, iAttrs) {} // こうじゃない!
}

これは誤りです。compilelinkを両方指定することはできません。両方使う場合はcompileの戻り値を関数型にします。

good
Dir.prototype.compile = function() {
  // compileの中身
  return this.link.bind(this); // returnでlink関数を返す
  // return this.link(); でもない。これはここで実行されてしまう。
};

Dir.prototype.link = function() {
  // linkの中身
};

function definition($rootScope) {
  var self = new Dir($rootScope);
  return {
    compile: self.compile.bind(self) // <-compileのみ
  };
}

上記が正しい例ですが、prototypebindを使わないとき下記のようになります。ネストが増えてテストもできないので個人的には好みません。

{
  compile: function() {
    // compileの中身
    return function() {
      // linkの中身
    };
  }
};

compile, link混合の実行タイミング

今までと同じで、c-01, c-02, c-03では1個、3個、ng-repeatで3回を試しています。

a-02, a-03の検証と同じくcompileの実行回数に違いがあります。そして全ての例でcompileのあとにlinkが実行されています。

Directive controller

続いてd-01, d-02, d-03controller。これはng-controllerangular.module().controller()のことではなく、Directiveに直接コントローラを持たせることができるAPIで、Angular 2.0に目を向けたとき一番に覚えるべきAPIだと感じています。

compilelinkはブラウザ上でのレンダリングに際する処理ですが、controllerは様々なロジックを置けます。MVVMのVM (ViewModel) に相当します。

controllerを指定するときは、併せてテンプレートから名指しで参照するための変数名controllerAsも指定するようにしましょう。

controllerの実行タイミング

controllerだけを指定したDirective」を1個、3個、ng-repeatで3回と試しています。

controller関数も画面上のDirectiveの数だけ走っていることがわかります。資料ではUUIDを表示するオプションも用意していますが、これをオンにするとインスタンスが全て異なることも確認できます。

compile, link, controller混合の実行タイミング

複雑になってきました。e-01, e-02, e-03では、3つのオプションを全て指定しています。これも1個、3個、ng-repeatで3回の順です。

これは面白い結果に。3個並べたe-02はcompile -> controller -> linkの組が3回繰り返されるかと思えば違って、まずcompileが3回、その後にctrl, link, ctrl, link...でした。ng-repeatを使うe-03は、これまでの結果と同じくcompileが1度走るのみ。

これらから、常にcompileが最初に実行されると分かります。

ネストの検証

fではパターンを増やしてネストの組み合わせを検証しています。また、ここからはprelinkpostlinkの実行順も検証しています。ネスト時に子要素のリンク処理が実行される前に呼ばれるのがpre、子要素の処理後に呼ばれるのはpostです。link関数ひとつを返すときはpostのタイミングで処理されます。

Dir.prototype.compile = function() {
  return { // オブジェクトを返す
    pre:  this.pre.bind(this),
    post: this.post.bind(this)
  };
};

compilelink関数を返すかわりにprepostを持つオブジェクトを返したとき有効になります。くどくどと検証しているので、検証内容は稿末の一覧表をご覧ください。

scopeの検証

fではscopeオプションを指定しませんでした。この場合初期値としてfalseとなります。

scopeの指定方法

指定方法は3種類。

  • false: そのディレクティブの親Scopeを共有
  • true: 親Scopeから派生したScopeを生成
  • isolatedScope: booleanではなくオブジェクト・リテラルを指定、分離Scopeを生成

Scopeの概念はAngular 2.0で変わると示されているので、共有のややこしいtrue / falseに悩まされず、素直にisolateScopeにしておけばいいでしょう。

g, h, iではscopeの指定を変えています。

  • f: 初期値false、親Scopeを共有
  • g: trueにし子Scopeを生成
  • h: isolatedScopeを指定、templateは指定なし
  • i: isolatedScopeを指定、templateに指定あり

scopeはホントややこしいから気をつけような!

予想と大きく反する結果となったmixed-fghi02の資料、このややこしさがDirectiveから人を遠ざけてると言ってもいい。

f

これは分かります。g, h, iとは無縁なのでexFインスタンスのみ参照できます。

g

いきなりガッカリです。親が$rootScope、子がexGになるとばかり思っていましたが、exFインスタンスも参照できています。scope: falseのDirectiveと隣接するとき、親ScopeはそのDirectiveが持つScopeになるようです。

fを抜いたmixed-ghiも試してみたところ、隣接にfが無いので参照していません。直接の親として$rootScopeを参照しています。

h

これも予想と違う結果。isolated―分離のはずなのにexFが参照できています。おまけに自身であるはずのexHは見えていません。templateを未指定にしてHTML側で中身を書いた場合分離ができていないのかと思いました。が、iを見るとそうでもなく、ただの勘違い。

i

isolatedScopeを指定してtemplateも指定した例がi。これは望んでいた挙動だ!

分離しているのでexFを参照したりしない、そして自身exIがきちんと参照できている、これが直感的。isolatedScopeにして自身をHTML上で参照するときはtemplate内からでないとできません。言い換えるとhは参照できなかったわけではなく、カプセル化が果たせていたんですね。

ダメ押しの検証

ブラウザのConsoleで$('ex-h').data()と打ち込むとオブジェクトが参照できます。ここにいるのがインスタンス化されたDirectiveの正体。この方法、デバッグに便利。

privateなプロパティとして$isolateScopeNoTemplateというものが確認できます。この中にexHインスタンスがいますが、外からは(厳密にはexFScope側からは)参照できません。

isolatedScopeの指定も怖がらないで

前に書いた記事で、『ここらでDirective Scopeの@=&をまとめておきたいと思う』というScopeオプションの指示子について解説したものがあります。

templateの中で別のDirectiveを展開

そろそろくどい感じですが、そうやって億劫になるから分からないんだと言い聞かせ、しつこく続けました。

jではtemplateex-h(isolatedScopeあり, templateなし)を指定しています。logからはcompile時に未展開な式がlink時には展開されていることが分かります。

Scopeのツリー構造

Screen Shot 2014-12-01 at 14.43.02.png

この画面はAngularJS Batarang、Angular公式のデバッグツール。まだ使ってない? 今すぐダウンロード!

Scopesのツリーでは、Scope (3, 7, 11)がex-jの外側Scope、Scope (4, 8, 12)がex-j自身の分離Scopeと分かりました。Scope (5, 6, 9, 10, 13, 14)はネストされたex-hが持つScope。

外側Scopeと分離Scopeってのが混乱の種ですが、前節のカプセル化の話を思い出して下さい。外側Scopeとは<ex-j>に記述したng-repeatの値が参照できるScope(ng-repeatは繰り返し対象が何かは知らない)。分離Scope内にexJControllerのインスタンスがいます。

処理中にjQueryで要素を追加したらどーなるの

ここまできたら徹底的にやる。

Directiveのcompilelinkの処理中にわざわざjQueryで他のDirectiveを足したとき、どう解釈するのか。jQueryを使うこと自体アンチパターンっぽいですが、リッチなUIを作るとつい手が伸びてしまう。(jQueryの是非は今後考えたい)

検証方法は7つ。

  • k-a: compile内でjQuery#append()
  • k-b: controller内でjQuery#append()
  • k-c: pre-link内でjQuery#append()
  • k-d: post-link内でjQuery#append()
  • k-e: compile内でjQuery#append()したあとcontroller$compile()
  • k-f: compile内でjQuery#append()したあとpre-link$compile()
  • k-g: compile内でjQuery#append()したあとpost-link$compile()

結論から言うとk-cの例を採用するとよさそうです。

理由

  • k-aは、そもそも失敗する
  • k-bは、controllerにレンダリング周りの事を書くべきではないとして却下
  • k-dは、k-cと結果が似ているが、post-linkでappendするのは「子のlink前にpre、子のlink後にpost」というAngularJSの仕様に反する

という理由でk-cにすべきとしました。

なお、k-eからk-gを試した理由としては、リフレクションを用いたテンプレートの展開にaからdのどの方法も適さなかったということがあります。ただしリフレクション自体が黒魔術っぽいのであまり深入りはしません。もしやるならk-fを採用します。

$compileは未処理のDOMと親Scopeを与えて強制的にcompile -> linkと走らせるもので自由度が高過ぎるので、慣れないうちは忘れていいです。

これも3回繰り返そうかと思ったけど、もう処理順見えてきたし冗長すぎるのでやめた。

参考資料早見表

参考資料

01 02 03 内容
a- 1個 3個 ng-repeatで3回 compileのみ
b- linkのみ
c- compilelink
d- controllerのみ
e- compile, link, controller
f-a 1個 3個 ng-repeatで3回 compile, prelink,postlink, controller, inherit scope
f-b 親1子1 親1子3 親1、子ng-repeat3回
f-c 親1子1を3組 親1子3を3組 親1、子ng-repeat3回を3組
f-d 親ng-repeat3回、子1 親ng-repeat3回、子3 親ng-repeat3回、子ng-repeat3回
f-e 4段ネスト 4段ネストを3組 4段ネストのルートにng-repeat3回
g- x x x new scope
h- x x x isolated scope, templateなし
i- x x x isolated scope, templateあり
j- 1個 3個 ng-repeatで3回 isolated scope, templateあり、内部でhを2個展開
k-a compileでappend x x isolated scope, templateなし、jQuery#append()でhを1個作成
k-b controllerでappend x x
k-c pre-linkでappend x x
k-d post-linkでappend x x
k-e compileでappend、controller$compile x x
k-f compileでappend、pre-link$compile x x
k-g compileでappend、post-link$compile x x

多いので必要な時に参照してください。

最終的な結論

  • 処理順は素直なので覚えたらおしまい。
  • ng-controllerは止めてDirectiveのcontroller APIに目を向ける。
  • Scopeは親子の共有を考え始めると混乱するだけなのでisolatedScope以外使わないというルールにしてしまう。
  • AngularJSの全APIを総動員するとかなり自由度の高いことができるがオススメしない。

Directiveが複雑そうに見られ、この記事が煩雑になったことも事実なんですが、この結論を守って迷ったときに処理順を確認するぐらいにすれば、だいぶ直感的にDirectiveが書けるようになるでしょう!

Angular 2.0に備えて今から慣れてみてはいかが?


2日目は@armorik83でした。明日は@takeyamaさん「AngularJS Factoryを使った言語切り替え」です。

Discussion