AngularJS Directiveの処理順を網羅してみた
こんにちは@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にはcompile
とlink
というオプションがあります。一方で$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」を使っています。
特徴は02と03。a-02では3回compile
が走るのに対し、ng-repeat
を使ったa-03では1回だけcompile
が走ります。
link
続いてlink
。link
もcompile
と同じように関数型を指定します。
linkの実行タイミング
資料b-01からb-03は「link
だけを指定したDirective」を使っています。
ng-repeat
を使ったa-03で1回だけcompile
が走りましたが、b-03ではlink
が3回走ります。link
は表示される数だけ実行されます。
compileとlinkの混合
compile
とlink
の両方を使うときはDDOの記述に注意しなければなりません。
{
compile: function(tElem, tAttrs, tTransclude) {},
link: function(scope, iElement, iAttrs) {} // こうじゃない!
}
これは誤りです。compile
とlink
を両方指定することはできません。両方使う場合はcompile
の戻り値を関数型にします。
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のみ
};
}
上記が正しい例ですが、prototype
やbind
を使わないとき下記のようになります。ネストが増えてテストもできないので個人的には好みません。
{
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-03はcontroller
。これはng-controller
やangular.module().controller()
のことではなく、Directiveに直接コントローラを持たせることができるAPIで、Angular 2.0に目を向けたとき一番に覚えるべきAPIだと感じています。
compile
やlink
はブラウザ上でのレンダリングに際する処理ですが、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ではパターンを増やしてネストの組み合わせを検証しています。また、ここからはprelink
とpostlink
の実行順も検証しています。ネスト時に子要素のリンク処理が実行される前に呼ばれるのがpre
、子要素の処理後に呼ばれるのはpost
です。link
関数ひとつを返すときはpost
のタイミングで処理されます。
Dir.prototype.compile = function() {
return { // オブジェクトを返す
pre: this.pre.bind(this),
post: this.post.bind(this)
};
};
compile
がlink
関数を返すかわりにpre
とpost
を持つオブジェクトを返したとき有効になります。くどくどと検証しているので、検証内容は稿末の一覧表をご覧ください。
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
インスタンスがいますが、外からは(厳密にはexF
Scope側からは)参照できません。
isolatedScopeの指定も怖がらないで
前に書いた記事で、『ここらでDirective Scopeの@=&をまとめておきたいと思う』というScopeオプションの指示子について解説したものがあります。
templateの中で別のDirectiveを展開
そろそろくどい感じですが、そうやって億劫になるから分からないんだと言い聞かせ、しつこく続けました。
jではtemplate
にex-h
(isolatedScopeあり, templateなし)を指定しています。logからはcompile
時に未展開な式がlink
時には展開されていることが分かります。
Scopeのツリー構造
この画面は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内にexJ
Controllerのインスタンスがいます。
処理中にjQueryで要素を追加したらどーなるの
ここまできたら徹底的にやる。
Directiveのcompile
やlink
の処理中にわざわざ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- | 〃 | 〃 | 〃 |
compile とlink
|
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