なぜ angularで as syntax を使いscopeの所有関係を明示的に書いた方が良いのかという話
はじめに
よく、「巷に溢れているangularのチュートリアルは良くない」だとか。「せめてcontrollerを使うときには as sytanxを使って」という話を聞きます。 それがなぜなのかということを説明してみることにしました。
$scopeの挙動について
controllerが云々というよりもangularの$scopeの挙動について説明したほうが良いかもしれません。
$scopeはアクセス時に親方向にchainしている。
まず、$scopeは特殊なオブジェクトでプロパティアクセス時に親方向にchainして探しに行きます。
var injector = angular.injector(["ng"]); var $rootScope = injector.get("$rootScope"); var childScope = $rootScope.$new(); var grandChildScope = childScope.$new(); $rootScope.x = "xxxxx"; console.log("root: %s", $rootScope.x); console.log("root -> child: %s", childScope.x); console.log("root -> child -> grand-child: %s", grandChildScope.x);
これは以下のような結果になります。
root: xxxxx root -> child: xxxxx root -> child -> grand-child: xxxxx
そのため、grandChildScope.x
も$parent
を順々にたどっていき最終的に$rootScope.x
にたどり着くので同じ結果を返すということです。
もちろん以下はtrueです。
console.log(grandChildScope.$parent.$parent === $rootScope);
ng-modelなどは現在のthisに対する変更をbindingしている。
参照に関しては常に$parent
のchainを辿ってくるということになっていましたが。値の設定については少し違った振る舞いをします。
例えば、 ng-model="x"
などと書いた場合には、親のことなどは関係なく現在のthis(現在の$scope)の$scope.x
を見ます。
ここで現在の$scope
が$rootScope
であることを想定したつもりで変更したとましょう。ところが実際に変更されていたのはchildScopeであったということが起き得ます。
その時の挙動は以下の様になります。
childScope.x = "yyyyy"; console.log("root: %s", $rootScope.x); console.log("root -> child: %s", childScope.x); console.log("root -> child -> grand-child: %s", grandChildScope.x);
結果はこうなります。
root: xxxxx root -> child: yyyyy root -> child -> grand-child: yyyyy
これは、childScope以下に対しては正しいですが、$rootScope
に関しては元の値のままです。
他のcomponentが$rootScope.x
を見るという形になっていたらどうなるでしょう?
あるcomponentに関しては新しい値であるyyyyyが表示され、一方で別のcomponentではxxxxのままということが起きてしまいます。
そしてその原因を調べるためにはhtml template側のbindingについて、それぞれどのscopeがbindされているか確認していく必要があります。
ng-if, ng-repeatなどがscopeを作る
先程の例で $rootScope
の代わりに childScopeを変更してしまっていたと書いていましたが。実際にそのようなことは起き得るのでしょうか?
scopeの生成タイミングを適切に把握していれば、 ng-model="$parent.x"
などと適切にbindingを走らせる事ができると思うかもしれません。
そもそもなぜ $parent.x
などと $parent
を経由しなければいけないのでしょう?
それは ng-if
や ng-repeat
などの directive
が新たなscopeを作るからです。
なので以下のbindingはそれぞれscopeが異なります。
<div ng-if="visible()"><input ng-model="x"><p>{{ x }}</div> <div><input ng-model="x"><p>{{ x }}</p></div>
directive定義でのcontrollAsやas syntaxの意味
では、先程の問題についての現在での解決策である as syntax を使うというのはどういうことなのでしょう? すごく雑にまとめると以下の様な形になります。
- 参照系に関しては$parentのchainで上手く取得できる
- 更新系に関しては現在のscopeに依存した位置に値を設定してしまう
つまり全て参照系ということにしておけば解決ということになります。やっている事も単純で、例えば以下の様に変えたとします。
<input ng-model="c.x">
これが何故大丈夫なのかというと以下の様なコードとほぼ等価だからです。
function Controller(){ this.x = "xxxxx"; } $rootScope.c = new Controller();
そして、一度 $rootScope.c
を取得してから値のコンテナーとしてのcontrollerの状態を変更しようとしている。ただそれだけです。
console.log("root: %s", $rootScope.c.x); console.log("root -> child: %s", childScope.c.x); console.log("root -> child -> grand-child: %s", grandChildScope.c.x); // settings console.log("settings: childScope.c.x <- 'yyyyyy'"); childScope.c.x = "yyyyy"; console.log("root: %s", $rootScope.c.x); console.log("root -> child: %s", childScope.c.x); console.log("root -> child -> grand-child: %s", grandChildScope.c.x);
全て c.x
にbindingされているので今度はrootの値も問題なく変更されるということになります。
root: xxxxx root -> child: xxxxx root -> child -> grand-child: xxxxx settings: childScope.c.x <- 'yyyyyy' root: yyyyy root -> child: yyyyy root -> child -> grand-child: yyyyy
gist
console-angularを使うと以下のようにnode.js上で angularのコードが試せます。 https://gist.github.com/podhmo/200da13416a7ed5fa673