スコープ
スコープとは?
スコープは、アプリケーションのモデルを参照するオブジェクトです。 これは、式の実行コンテキストです。 スコープは、アプリケーションのDOM構造を真似た階層構造で配置されます。 スコープは、式を監視してイベント伝搬を行う事が可能です。
スコープの特徴
- スコープは、モデルの変更を見張るAPI($watch)を提供します。
- スコープは、"Angular領域"(コントローラー、サービス、Angularイベントハンドラ)の外部から、 ビューへシステムを通してモデル変更を伝搬するためのAPI($apply)を提供します。
- スコープは、共有モデルのプロパティへのアクセスを提供しながら、アプリケーションの分離コンポーネントを入れ子にすることができます。 スコープ(のプロトタイプ)は、その親スコープからプロパティを継承します。
-
スコープは、評価対象の式 に対してコンテキストを提供します。
例えば、
{{username}}
式は、usernameプロパティが定義された特定のスコープに対して評価されなければ意味がありません。
データモデルとしてのスコープ
スコープはアプリケーションのコントローラーとビューを繋げる糊の役割を果たします。 テンプレートをリンクするフェーズ中、 ディレクティブは$watch式をスコープ上に設定します。 $watchは、ディレクティブがプロパティ変更の通知を受けれるようにし、 それによってディレクティブがDOMに対して、値更新の描画が可能になります。
コントローラーとディレクティブの両方は、スコープへの参照を持ちますが、お互いへの参照は持ちません。 この取り決めは、DOMと同様にディレクティブからコントローラーを分離します。 これは、コントローラーのビューに依存しないようにし、アプリケーションでのテスト状況を大幅に向上させるため、 非常に重要なポイントです。
<!doctype html>
<html ng-app>
<head>
<script src="http://code.angularjs.org/1.2.0-rc.2/angular.min.js"></script>
<script src="script.js"></script>
</head>
<body>
<div ng-controller="MyController">
Your name:
<input type="text" ng-model="username">
<button ng-click='sayHello()'>greet</button>
<hr>
{{greeting}}
</div>
</body>
</html>
function MyController($scope) {
$scope.username = 'World';
$scope.sayHello = function() {
$scope.greeting = 'Hello ' + $scope.username + '!';
};
}
<!doctype html> <html ng-app> <head> <script src="http://code.angularjs.org/1.2.0-rc.2/angular.min.js"></script> <script>function MyController($scope) { $scope.username = 'World'; $scope.sayHello = function() { $scope.greeting = 'Hello ' + $scope.username + '!'; }; } </script> </head> <body> <div ng-controller="MyController"> Your name: <input type="text" ng-model="username"> <button ng-click='sayHello()'>greet</button> <hr> {{greeting}} </div> </body> </html>
上記の例は、MyControllerが"World"をスコープのusernameプロパティに割り当てています。 スコープはinputに対して通知をし、usernameのinputが予め入力された状態にするための描画が行われます。 このデモでは、コントローラー内でスコープに対し、データの書き込みが行えることを示しています。
同様にコントローラーは、ユーザーが'greet'ボタンをクリックした際に呼び出される、sayHelloメソッドをスコープに動作を割り当ています。 sayHelloメソッドは、usernameプロパティを読み込み、greetingプロパティを生成しています。 このデモではHTMLのinputウィジットに紐付けられていると、スコープのプロパティは自動的に更新されることを示しています。
{{greeting}}の描画で行われていることについて
- テンプレートに定義された{{greeting}}のDOMノードは、値を取得するためにスコープに関連付けられます。 この例では、これはMyControllerに渡されたスコープと同じスコープにされています。 (スコープの階層について、後述します。)
- 取得したスコープの内容でgreetingの式が評価され、DOM要素のテキストにその結果が割り当てられます。
スコープとそのプロパティを使って、どのようにビューへ描画するかを考慮することが出来ます。 スコープは、ビューに関する全ての事に対して、"データ源が単一(single source-of-truth)"のスタンスをとっています。
ビューのテストを行う場合は、コントローラーとビューを分離するのが望ましいとされています。 これは、描画の細部に対して気を取られること無く、テストをすることを可能にしてくれるためです。
it('should say hello', function() {
var scopeMock = {};
var cntl = new MyController(scopeMock);
// Assert that username is pre-filled
expect(scopeMock.username).toEqual('World');
// Assert that we read new username and greet
scopeMock.username = 'angular';
scopeMock.sayHello();
expect(scopeMock.greeting).toEqual('Hello angular!');
});
スコープの階層
各Angularアプリケーションには、必ず単一のルートスコープがあり、 ルートスコープは子スコープを持つことが出来ます。
ディレクティブによっては、新しい子スコープ(新しいスコープを作成するスコープについては、 ディレクティブのドキュメントを参照してください)を作成するため、 アプリケーションは複数のスコープを持つことが可能となっています。 新しいスコープが作成されると、それらは親スコープの子として追加されます。 これは、それらが割り当てられているDOMと同様のツリー構造を作成します。
Angularは{{username}}を評価すると、まずusernameプロパティのために指定された要素に関連付けられたスコープを探します。 そのようなプロパティが見つからない場合は、ルートスコープに達するまで親スコープを辿って探します。 JavaScriptのこの挙動は、プロトタイプ継承として知られているもので、子スコープのプロパティはその親から継承したものになっています。
下記は、アプリケーション内のスコープと、プロパティのプロトタイプ継承を用いた実例となっています。
<!doctype html>
<html ng-app>
<head>
<link rel="stylesheet" type="text/css" href="style.css">
<script src="http://code.angularjs.org/1.2.3/angular.min.js"></script>
<script src="script.js"></script>
</head>
<body>
<div class="show-scope-demo">
<div ng-controller="GreetCtrl">
Hello {{name}}!
</div>
<div ng-controller="ListCtrl">
<ol>
<li ng-repeat="name in names">{{name}} from {{department}}</li>
</ol>
</div>
</div>
</body>
</html>
.show-scope-demo.ng-scope,
.show-scope-demo .ng-scope {
border: 1px solid red;
margin: 3px;
}
function GreetCtrl($scope, $rootScope) {
$scope.name = 'World';
$rootScope.department = 'Angular';
}
function ListCtrl($scope) {
$scope.names = ['Igor', 'Misko', 'Vojta'];
}
<!doctype html> <html ng-app> <head> <style type="text/css">.show-scope-demo.ng-scope, .show-scope-demo .ng-scope { border: 1px solid red; margin: 3px; } </style> <script src="http://code.angularjs.org/1.2.3/angular.min.js"></script> <script>function GreetCtrl($scope, $rootScope) { $scope.name = 'World'; $rootScope.department = 'Angular'; } function ListCtrl($scope) { $scope.names = ['Igor', 'Misko', 'Vojta']; } </script> </head> <body> <div class="show-scope-demo"> <div ng-controller="GreetCtrl"> Hello {{name}}! </div> <div ng-controller="ListCtrl"> <ol> <li ng-repeat="name in names">{{name}} from {{department}}</li> </ol> </div> </div> </body> </html>
Angularは自動的にng-scope
クラスを、スコープが割り当てられた要素上に配置することに注目してください。
この例の<style>には、新しいスコープの範囲を赤くハイライトする定義がされています。
{{name}}の繰り返し式が評価されるため、子スコープが必要となり、その依存するスコープによって異なる評価結果が表示されています。
{{department}}は、departmentが定義されている場所は1つしかないため、ルートスコープからプロトタイプ継承が行われ、
{{department}}の評価の結果はそれと同じになります。
DOMからスコープを取得
スコープは、DOMへ$scopeのデータプロパティとして割り当てられ、デバッグ用途で値を取得することが可能です。
(この方法でアプリケーション内部からスコープを取得する必要があるようなケースは、ほとんどありません。)
ルートスコープは、ng-app
ディレクティブの場所によって割り当てられるDOMの場所が定義されます。
通常ng-app
は、<html>要素に配置されますが、他の要素にも同様に配置可能で、
例えば、Angularによって制御が必要なビューの特定箇所にだけ配置することも可能です。
下記の手順で、デバッグ中にスコープを確認します。
- ブラウザで任意の要素上で右クリックし、'要素を検証'の旨のメニューを選択します。 クリックした要素がハイライトされた状態で、ブラウザのデバッガが表示されるはずです。
-
デバッガは、コンソール内で
$0
変数とすることで選択中の要素にアクセス可能にしてくれます。 -
関連付けられたスコープを取得するためにコンソールで
angular.element($0).scope()
を実行します。
スコープのイベント伝搬
スコープは、DOMイベントと同様の方法でイベント伝搬することが可能です。 イベントは子スコープに広めたり(broadcast)、または親スコープに対して発行(emited)することが可能です。
<!doctype html>
<html ng-app>
<head>
<script src="http://code.angularjs.org/1.2.0-rc.2/angular.min.js"></script>
<script src="script.js"></script>
</head>
<body>
<div ng-controller="EventController">
Root scope <tt>MyEvent</tt> count: {{count}}
<ul>
<li ng-repeat="i in [1]" ng-controller="EventController">
<button ng-click="$emit('MyEvent')">$emit('MyEvent')</button>
<button ng-click="$broadcast('MyEvent')">$broadcast('MyEvent')</button>
<br>
Middle scope <tt>MyEvent</tt> count: {{count}}
<ul>
<li ng-repeat="item in [1, 2]" ng-controller="EventController">
Leaf scope <tt>MyEvent</tt> count: {{count}}
</li>
</ul>
</li>
</ul>
</div>
</body>
</html>
function EventController($scope) {
$scope.count = 0;
$scope.$on('MyEvent', function() {
$scope.count++;
});
}
<!doctype html> <html ng-app> <head> <script src="http://code.angularjs.org/1.2.0-rc.2/angular.min.js"></script> <script>function EventController($scope) { $scope.count = 0; $scope.$on('MyEvent', function() { $scope.count++; }); } </script> </head> <body> <div ng-controller="EventController"> Root scope <tt>MyEvent</tt> count: {{count}} <ul> <li ng-repeat="i in [1]" ng-controller="EventController"> <button ng-click="$emit('MyEvent')">$emit('MyEvent')</button> <button ng-click="$broadcast('MyEvent')">$broadcast('MyEvent')</button> <br> Middle scope <tt>MyEvent</tt> count: {{count}} <ul> <li ng-repeat="item in [1, 2]" ng-controller="EventController"> Leaf scope <tt>MyEvent</tt> count: {{count}} </li> </ul> </li> </ul> </div> </body> </html>
スコープのライフサイクル
通常のブラウザが受け取るイベントのフローでは、対応するJavaScriptのコールバックが実行されます。 ブラウザがDOMを再描画するコールバックが完了すると、次のイベントへの待機状態に戻ります。
Angular実行コンテキストの外部で、ブラウザがJavaScriptコードの実行を呼び出す場合、 Angularがモデルの変更を認識することが出来ません。 適切にモデルの変更を処理するためには、$applyメソッドを使用して、Angularの実行コンテキストに入れる必要があります。 $applyメソッド内で実行されたモデル変更のみが、Angularによって適切に処理されます。 例えば、もしディレクティブがng-clickのような、DOMイベントをリッスンしていた場合、 $applyメソッド内で式を評価しなければいけません。
式を評価した後、$applyメソッドは$digestを実行します。
$digestフェーズで、スコープは$watch式の全ての検証と、直前の値との比較を行います。
非同期で、これの"手を加えた(手つかず)"のチェックが実行されます。
これは、$scope.username="angular"
のような割り当てが、すぐに$watchへ通知されず、
代わりに$digestフェーズまで遅らされる事を意味します。
複数のモデルの更新による$watchへの通知を合併し、同様に$watch通知中に他の$watchが実行されていないことが保証されるため、
この遅延は望ましいものです。(翻訳に自信なし)
もし、$watchがモデルの値を変更すると、追加の$digestサイクルを強制します。
-
作成
アプリケーションの起動中に、$injectorによってルートスコープが作成されます。 テンプレートのリンク中に、ディレクティブは新しい子スコープを作成します。
-
監視(watch)登録
テンプレートのリンク中に、ディレクティブはスコープ上の監視を登録します。 これらの監視は、DOMへのモデル値の伝搬の際に使用されます。
-
モデルの変更
変更に対して適切に気づく事が出来るように、これらの事をscope.$apply()内で行うべきです。 (AngularのAPIはこれを暗黙的に行うため、コントローラー内での同期処理、 または$httpや$timeout等のサービスでの非同期処理の際に 余分に$applyを呼び出す必要はありません。
-
変更の監視
$applyの最後に、Angularはルートスコープ上で$digestサイクルを実行し、 全ての子スコープに伝搬します。 $digestサイクル中は、全ての$watch式または関数は、モデルの変更を確認し、 もし変更されていれば、$watchリスナーが呼び出されます。
-
スコープの破壊
子スコープが不要になった場合、scope.$destroy()のAPIを介してそれらを削除するのは、子スコープ作成者の責務です。 これは、子スコープに$digest呼び出しによる伝搬を停止し、 ガベージコレクタが、子スコープモデルに使用されていたメモリを開放出来るようにします。
スコープとディレクティブ
コンパイルフェーズ中は、コンパイラはDOMテンプレートに対応するディレクティブをマッチします。 ディレクティブは、大抵2つのカテゴリのいずれかに分類されます。
-
中括弧式である
{{式}}
のような監視ディレクティブが、 $watch()メソッドを使用してリスナーを登録します。 このタイプのディレクティブは、ビューを更新できるように、 式が変更されたときに通知する必要があります。 - ng-clickのようなリスナーディレクティブは、DOMにリスナーを登録します。 DOMリスナーがトリガされると、ディレクティブは関連する式 を実行し、 $apply()メソッドを使用してビューを更新します。
外部イベント(ユーザーのアクション、タイマー、XHR等)を受け取った際に、 関連する式は$apply()メソッドを通してスコープに適用される必要があり、 それによって全てのリスナーが正しく更新されます。
スコープを作成するディレクティブ
ほとんどのケースで、ディレクティブとスコープは互いに影響し合いますが、スコープの新しいインスタンスは作成されません。
ただし、ng-controllerとng-repeatのようなディレクティブは、
新しい子スコープを作成し、対応するDOM要素にその子スコープを割り当てます。
angular.element(aDomElement).scope()
メソッドを呼び出すことで、DOM要素のスコープを取得することが可能です。
コントローラーとスコープ
スコープとコントローラーは、下記の状況下でお互いに影響し合います。
- コントローラーは、スコープを使用して、テンプレートにコントローラのメソッドを公開します。(ng-controllerを参照)
- コントローラーは、モデル(スコープ上のプロパティ)を変更することが出来るメソッド(挙動)を定義します。
- コントローラーは、モデル上の監視を登録することがあります。 これらの監視は、コントローラーの挙動実行後に、即座に実行されます。
詳細は、ng-controllerを御覧ください。
スコープの$watchパフォーマンスに関する考慮事項
プロパティ変更のための、手つかず(または、手が加えられた)のスコープのチェックは、Angularでの共通のオペレーションであり、 そのため、このチェック関数の処理は優れている必要があります。 手つかず(または、手が加えられた)チェックの関数は、DOMにアクセスしないように配慮されるべきです。 JavaScriptオブジェクトのプロパティアクセスの中でも、DOMアクセスは非常に遅い処理です。(翻訳に自信なし)
ブラウザーのイベントループとの統合
下記の図とサンプルは、Angularがどのようにブラウザのイベントループによって作用しているかを説明したものです。
- ブラウザのイベントループは、イベントが届けられるのを待ち受けます。 イベントには、ユーザー操作によるもの、タイマーイベント、ネットワークイベント(サーバからのレスポンス)があります。
- イベントのコールバックが実行され、JavaScriptのコンテキストに入ります。 コールバックはDOM構造を変更することが可能です。
- 1度コールバックが実行されると、ブラウザはJavaScriptコンテキストを残し、 DOMの変更を元にしたビューを再描画します。
Angularは、標準のJavaScriptフローを自身のイベント処理ループを割り当てることで変更します。 これは古典的なJavaScriptと、Angularによるコンテキストの実行を切り離します。 Angular実行コンテキストで適用された操作のみが、Angularによるデータバインディング、例外のハンドリング、プロパティ監視等の恩恵を受けることが出来、 また、$apply()を使用してJavaScriptからAngularの実行コンテキストを入れることも可能です。
ほとんどの場所(controller、service)で、$applyはイベントを扱うディレクティブを介して、 常にあなたによって呼び出されている事を忘れないでください。
明確な$applyの呼び出しは、カスタムイベントのコールバック実装、 またはサードパーティ製のライブラリのコールバックが作用する時にのみ必要になります。
- scope.$apply(stimulusFn)の呼び出しによって、Angular実行コンテキストを入れます。 stimulusFnが、Angular実行コンテキスト内で行って欲しい動作になります。
- Angularは通常アプリケーションの状態を変更するstimulusFn()を実行します。
- Angularは、$digestループを登録します。 このループは$evalAsyncキューと$watchリストを処理する2つの小さなループを作ります。 $digestループはモデルの安定させるための繰り返し処理を保持し、 これは$evalAsyncキューが空、$watchリストによる変更指示が無いことを意味します。
-
$evalAsync
キューは、ブラウザのビュー描画の前に現スタックフレームの外側で発生するスケジュール作業に使用されます。 これには常にsetTimeout(0)が行われますが、setTimeout(0)によるアプローチでは遅延を気にすることになり、 また、各イベントの後のブラウザのビュー描画によって、画面のちらつきを発生させてしまうかもしれません。 - $watchリストは、最後の繰り返し以降に、変更されているかもしれない内容の式の集まりです。 もし、変更が指示されると、通常$watch関数は新しい値に基づく更新処理から呼び出されます。
- Angularの$digestループが実行にが完了すると、AngularとJavaScriptコンテンツが残されます。 これは、変更を反映するためのブラウザの再描画に続きます。 ここで、Hellow Wordlの例で、ユーザーがテキストフィールドに値を入力した際に、データバインディングによる反映の方法について説明します。
-
コンパイルフェーズ:
- ng-modelとinputディレクティブに、<input>制御上でのkeydownリスナーを設定します。
- {{name}}は、nameの変更が$watchに通知されるように設定を行います。
-
ランタイムフェーズ:
- input上で'X'キーを押すと、ブラウザがkeydownイベントを発生させます。
- inputディレクティブは、inputの値の変更を捕捉し、 Angular実行コンテキスト内部のアプリケーションモデルを変更するために$apply("name = 'X';")を呼び出します。
- Angularはname = 'X';をモデルに対し適用します。
- $digestループを開始します。
- $watchリストはnameプロパティの変更を見つけ、{{name}}にDOMを更新して変更するように伝えます。
- Angularはコンテキストの実行、keydownイベントとそのJavaScriptのコンテキストの実行、を終了します。
- テキストの更新により、ブラウザはビューを再描画します。
© 2017 Google
Licensed under the Creative Commons Attribution License 3.0.
このページは、ページトップのリンク先のAngularJS公式ドキュメント内のページを翻訳した内容を基に構成されています。 下記の項目を確認し、必要に応じて公式のドキュメントをご確認ください。 もし、誤訳などの間違いを見つけましたら、 @tomofまで教えていただければ幸いです。
- AngularJSの更新頻度が高いため、元のコンテンツと比べてドキュメントの情報が古くなっている可能性があります。
- "訳注:"などの断わりを入れた上で、日本人向けの情報やより分かり易くするための追記を行っている事があります。