ホームページ >ウェブフロントエンド >jsチュートリアル >JavaScript の AngularJS フレームワークにおけるスコープとデータ バインディングの詳細な説明
AngularJS の紹介
AngularJS は、Google が開発したオープンソースのフロントエンド MVC スクリプト フレームワークで、通常の WEB アプリケーションと SPA (シングルページ アプリケーション、ユーザーのすべての操作が 1 ページで完了する) の両方に適しています。同じく MVC フレームワークである Dojo の位置付けとは異なり、AngularJS は jQuery と比較して機能が軽量であり、機械的なバインディング作業を大幅に節約できます。 AngularJS は、高い開発速度を必要とし、豊富な機能モジュールを必要としない一部の非エンタープライズ レベルの WEB アプリケーションにとって非常に良い選択肢です。 AngularJS の最も複雑かつ強力な部分は、そのデータ バインディング メカニズムです。このメカニズムにより、基礎となる DOM で低レベルの操作を実行するのではなく、モデルの確立とデータの配信に重点を置くことができます。
AngularJS スコープ
jQuery をベースとした従来の WEB アプリケーションでは、ユーザー入力などの動作を監視するために、DOM 要素ごとにリッスンメソッドを設定する、つまり DOM 上で発生するさまざまなイベントを監視する必要があり、その後、jQuery が決定を行い、応答してページに表示します。この方法はシンプルで直感的ですが、WEB アプリケーションが大きく複雑になると、監視コードが非常に機械的で冗長になります。さらに恐ろしいのは、DOM イベント監視が適切に管理されていない場合、ブラウザーが簡単に監視してしまうことです。リソースの漏洩。
上記で明らかになった問題に対応して、AngularJS は一連の命令を使用して jQuery のイベント バインディング コードを置き換えます。データの混乱を引き起こすことなくさまざまな命令間の調整を整理するために、AngularJS はモデル層のスコープの概念を拡張し、コントローラーと連携してビュー層を表示します。
スコープ
AngularJS では、スコープは式の実行環境であるアプリケーション モデルを指すオブジェクトです。スコープは階層構造になっており、対応する DOM とほぼ同じです。スコープは式を監視し、イベントを渡すことができます。
HTML コードでは、ng-app ディレクティブが定義されると、スコープが生成されます。これは、他のすべてのスコープのルート スコープであるルート スコープ ($rootScope) です。 $Scope の最上位レベル。
リスト 1. ルート スコープを生成する
<html> <head><script src="angular.min.js"></script></head> <body data-ng-app="app">...</body> </html>
ng-app ディレクティブを使用してスコープを生成することに加えて、ng-controller、ng-repeat などの他のディレクティブも 1 つ以上のスコープを生成します。さらに、AngularJS が提供するスコープ作成ファクトリ メソッドを通じてスコープを作成できます。これらの各スコープには独自の継承コンテキストがあり、ルート スコープは $rootScope です。
スコープを生成した後、AngularJS コードを記述するとき、$scope オブジェクトはこのスコープのデータ エンティティを表し、$scope でさまざまなデータ型を定義し、{{ HTML の変数名 }} メソッドを直接使用して、HTML で次のことを行うことができます。この変数にアクセスするコードは次のとおりです。
リスト 2. 単純なデータ バインディング
<script> angular.module('app', []) .controller("ctrl", function ($scope) { $scope.btns = { ibm : 'ibm' }; }); </script> </head> <body data-ng-app="app" > <div data-ng-controller="ctrl"> <button>{{btns.ibm}}</button> </div> </body>
これは、AngularJS で最も単純なデータ バインディング方法であり、最も広く使用されているデータ バインディング方法でもあります。
継承されたスコープ
AngularJS は、スコープの作成時にコンテキストを取得します。コンテキスト内に既にスコープがある場合、新しく作成されたスコープは、JavaScript プロトタイプの継承メカニズムを使用してその親スコープを継承します (分離されたスコープを除く)。スコープ、以下で説明します)。
一部の AngularJS ディレクティブは、新しい子スコープを作成し、プロトタイプの継承を実行します: ng-repeat、ng-include、ng-switch、ng-view、ng-controller、scope: true および transclude: true で作成されたディレクティブ。
次の HTML では、ng-app ディレクティブによって作成される $rootScope、parentCtrl と childCtrl によって作成される子スコープの 3 つのスコープが定義されています。このうち、childCtrl によって生成されるスコープはparentCtrl の子スコープです。
リスト 3. スコープの継承例
<body data-ng-app="app"> <div data-ng-controller="parentCtrl"> <input data-ng-model="args"> <div data-ng-controller="childCtrl"> <input data-ng-model="args"> </div> </div> </body>
継承されたスコープは JavaScript のプロトタイプ継承メカニズムに準拠しています。つまり、子スコープの親スコープで定義されたプロパティにアクセスすると、JavaScript はまずその属性を検索します。スコープが見つからない場合は、プロトタイプ チェーンの親スコープから検索します。見つからない場合は、上位のプロトタイプ チェーンの親スコープで検索します。 AngularJS では、スコープ プロトタイプ チェーンの最上位は $rootScope です。AnguarJS は、$rootScope まで検索します。それでも見つからない場合は、unknown を返します。
このメカニズムを説明するためにサンプルコードを使用します。まず、プロトタイプ データ型のスコープ継承メカニズムについて説明します。
リスト 4. スコープ継承の例 - プリミティブ型データの継承
<script type="text/javascript"> angular.module('app', []) .controller('parentCtrl', ['$scope', function($scope) { $scope.args = 'IBM DeveloperWorks'; }]) .controller('childCtrl', ['$scope', function($scope) { }]); </script> <body data-ng-app="app"> <div data-ng-controller="parentCtrl"> <input data-ng-model="args"> <div data-ng-controller="childCtrl"> <input data-ng-model="args"> </div> </div> </body>
ページを実行すると、次の結果が得られます。
図 1. ページの実行結果。
这个结果我们非常好理解,虽然在 childCtrl 中没有定义具体的 args 属性,但是因为 childCtrl 的作用域继承自 parentCtrl 的作用域,因此,AngularJS 会找到父作用域中的 args 属性并设置到输入框中。而且,如果我们在第一个输入框中改变内容,内容将会同步的反应到第二个输入框:
图 2. 改变第一个输入框的内容后页面运行结果
假如我们修改第二个输入框的内容,此时会发生什么事情呢?答案是第二个输入框的内容从此将不再和第一个输入框的内容保持同步。在改变第二个输入框的内容时,因为 HTML 代码中 model 明确绑定在 childCtrl 的作用域中,因此 AngularJS 会为 childCtrl 生成一个 args 原始类型属性。这样,根据 AngularJS 作用域继承原型机制,childCtrl 在自己的作用域找得到 args 这个属性,从而也不再会去寻找 parentCtrl 的 args 属性。从此,两个输入框的内容所绑定的属性已经是两份不同的实例,因此不会再保持同步。
图 3.改变第二个输入框的内容后页面运行结果
假如我们将代码做如下修改,结合以上两个场景,思考下会出现怎样的结果?
清单 5. 作用域继承实例-对象数据继承
<script type="text/javascript"> angular.module('app', []) .controller('parentCtrl', ['$scope', function($scope) { $scope.args = {}; $scope.args.content = 'IBM DeveloperWorks'; }]) .controller('childCtrl', ['$scope', function($scope) { }]); </script> <body data-ng-app="app"> <div data-ng-controller="parentCtrl"> <input data-ng-model="args.content"> <div data-ng-controller="childCtrl"> <input data-ng-model="args.content"> </div> </div> </body>
答案是无论改变任何一个输入框的内容,两者的内容始终同步。
根据 AngularJS 的原型继承机制,如果 ng-model 绑定的是一个对象数据,那么 AngularJS 将不会为 childCtrl 创建一个 args 的对象,自然也不会有 args.content 属性。这样,childCtrl 作用域中将始终不会存在 args.content 属性,只能从父作用域中寻找,也即是两个输入框的的变化其实只是在改变 parentCtrl 作用域中的 args.content 属性。因此,两者的内容始终保持同步。
我们再看一个例子,这次请读者自行分析结果。
清单 6. 作用域继承实例-不再访问父作用域的数据对象。
<script type="text/javascript"> angular.module('app', []) .controller('parentCtrl', ['$scope', function($scope) { $scope.args = {}; $scope.args.content = 'IBM DeveloperWorks'; }]) .controller('childCtrl', ['$scope', function($scope) { $scope.args = {}; $scope.args.content = 'IBM DeveloperWorks'; }]); </script> <body data-ng-app="app"> <div data-ng-controller="parentCtrl"> <input data-ng-model="args.content"> <div data-ng-controller="childCtrl"> <input data-ng-model="args.content"> </div> </div> </body>
答案是两个输入框的内容永远不会同步。
孤立作用域(Isolate Scope)
孤立作用域是 AngularJS 中一个非常特殊的作用域,它只在 directive 中出现。在对 directive 的定义中,我们添加上一个 scope:{} 属性,就为这个 directive 创建出了一个隔离作用域。
清单 7. directive 创建出一个孤立作用域
angular.module('isolate', []).directive("isolate", function () { return { scope : {}, }; })
孤立作用域最大的特点是不会原型继承其父作用域,对外界的父作用域保持相对的独立。因此,如果在定义了孤立作用域的 AngularJS directive 中想要访问其父作用域的属性,则得到的值为 undefined。代码如下:
清单 8. 孤立作用域的隔离性
<script type="text/javascript"> angular.module('app', []) .controller('ctrl', ['$scope', function($scope) { $scope.args = {}; }]) .directive("isolateDirective", function () { return { scope : {}, link : function($scope, $element, $attr) { console.log($scope.$args); //输出 undefined } }; }); </script> <body data-ng-app="app"> <div data-ng-controller="ctrl"> <div data-isolate-directive></div> </div> </body>
上面的代码中通过在 directive 中声明了 scope 属性从而创建了一个作用域,其父作用域为 ctrl 所属的作用域。但是,这个作用域是孤立的,因此,它访问不到父作用域的中的任何属性。存在这样设计机制的好处是:能够创建出一些列可复用的 directive,这些 directive 不会相互在拥有的属性值上产生串扰,也不会产生任何副作用。
AngularJS 孤立作用域的数据绑定
在继承作用域中,我们可以选择子作用域直接操作父作用域数据来实现父子作用域的通信,而在孤立作用域中,子作用域不能直接访问和修改父作用域的属性和值。为了能够使孤立作用域也能和外界通信,AngularJS 提供了三种方式用来打破孤立作用域“孤立”这一限制。
单向绑定(@ 或者 @attr)
这是 AngularJS 孤立作用域与外界父作用域进行数据通信中最简单的一种,绑定的对象只能是父作用域中的字符串值,并且为单向只读引用,无法对父作用域中的字符串值进行修改,此外,这个字符串还必须在父作用域的 HTML 节点中以 attr(属性)的方式声明。
使用这种绑定方式时,需要在 directive 的 scope 属性中明确指定引用父作用域中的 HTML 字符串属性,否则会抛异常。示例代码如下:
清单 9. 单向绑定示例
<script> angular.module('isolateScope', []) .directive("isolateDirective", function () { return { replace : true, template: '<button>{{isolates}}</button>', scope : { isolates : '@', }, link : function($scope, $element, $attr) { $scope.isolates = "DeveloperWorks"; } }; }) .controller("ctrl", function ($scope) { $scope.btns = 'IBM'; }); </script> <body data-ng-app="isolateScope" > <div data-ng-controller="ctrl"> <button>{{btns}}</button> <div data-isolate-directive data-isolates="{{btns}}"></div> </div> </body>
简单分析下上面的代码,通过在 directive 中声明了 scope:{isolates:'@'} 使得 directive 拥有了父作用域中 data-isolates 这个 HTML 属性所拥有的值,这个值在控制器 ctrl 中被赋值为'IBM'。所以,代码的运行结果是页面上有两个名为 IBM 的按钮。
我们还注意到 link 函数中对 isolates 进行了修改,但是最终不会在运行结果中体现。这是因为 isolates 始终绑定为父作用域中的 btns 字符串,如果父作用域中的 btns 不改变,那么在孤立作用域中无论怎么修改 isolates 都不会起作用。
引用绑定(&或者&attr)
通过这种形式的绑定,孤立作用域将有能力访问到父作用域中的函数对象,从而能够执行父作用域中的函数来获取某些结果。这种方式的绑定跟单向绑定一样,只能以只读的方式访问父作用函数,并且这个函数的定义必须写在父作用域 HTML 中的 attr(属性)节点上。
这种方式的绑定虽然无法修改父作用域的 attr 所设定的函数对象,但是却可以通过执行函数来改变父作用域中某些属性的值,来达到一些预期的效果。示例代码如下:
清单 10. 引用绑定示例
<script> angular.module('isolateScope', []) .directive("isolateDirective", function () { return { replace : true, scope : { isolates : '&', }, link : function($scope, $element, $attr) { var func = $scope.isolates(); func(); } }; }) .controller("ctrl", function ($scope) { $scope.func = function () { console.log("IBM DeveloperWorks"); } }); </script> <body data-ng-app="isolateScope" > <div data-ng-controller="ctrl"> <div data-isolate-directive data-isolates="func"></div> </div> </body>
这个例子中,浏览器的控制台将会输出一段“IBM DeveloperWorks”文字。
上面的代码中我们在父作用域中指定了一个函数对象$scope.func,在孤立作用域中通过对 HTML 属性的绑定从而引用了 func。需要注意的是 link 函数中对 func 对象的使用方法,$scope.isolates 获得的仅仅是函数对象,而不是调用这个对象,因此我们需要在调用完$scope.isolates 之后再调用这个函数,才能得到真正的执行结果。
双向绑定(=或者=attr)
双向绑定赋予 AngularJS 孤立作用域与外界最为自由的双向数据通信功能。在双向绑定模式下,孤立作用域能够直接读写父作用域中的属性和数据。和以上两种孤立作用域定义数据绑定一样,双向绑定也必须在父作用域的 HTML 中设定属性节点来绑定。
双向绑定非常适用于一些子 directive 需要频繁和父作用域进行数据交互,并且数据比较复杂的场景。不过,由于可以自由的读写父作用域中的属性和对象,所以在一些多个 directive 共享父作用域数据的场景下需要小心使用,很容易引起数据上的混乱。
示例代码如下:
清单 11. 双向绑定示例
<script> angular.module('isolateScope', []) .directive("isolateDirective", function () { return { replace : true, template: '<button>{{isolates}}</button>', scope : { isolates : '=', }, link : function($scope, $element, $attr) { $scope.isolates.ibm = "IBM"; } }; }) .controller("ctrl", function ($scope) { $scope.btns = { ibm : 'ibm', dw : 'DeveloperWorks' }; }); </script> <body data-ng-app="isolateScope" > <div data-ng-controller="ctrl"> <button>{{btns.dw}}</button> <button>{{btns.ibm}}</button> <div data-isolate-directive data-isolates="btns"></div> </div> </body>
上面的代码运行的结果是浏览器页面上出现三个按钮,其中第一个按钮标题为“DeveloperWorks”,第二和第三个按钮的标题为“IBM”。
初始时父作用域中的$scope.btns.ibm 为小写的“ibm”,通过双向绑定,孤立作用域中将父作用域的 ibm 改写成为大写的“IBM”并且直接生效,父作用域的值被更改。
总结
由于 AngularJS 框架的轻量性和其清晰的 MVC 特点使得其在推出之后就大受欢迎,实践中也很容易上手。AngularJS 比较难以掌握和理解的就是其作用域和绑定机制,本文重点将作用域和绑定机制做了分析与讨论,希望读者能够理解并熟练掌握这块内容。
更多JavaScript の AngularJS フレームワークにおけるスコープとデータ バインディングの詳細な説明相关文章请关注PHP中文网!