なぜ 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-ifng-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