ホームページ > 記事 > ウェブフロントエンド > AngularJS_AngularJS におけるデータの双方向バインディング メカニズムの詳細な研究
Angular JS (Angular.JS) は、Web ページの開発に使用されるフレームワーク、テンプレート、データ バインディング、およびリッチ UI コンポーネントのセットです。開発プロセス全体をサポートし、手動による DOM 操作を必要としない Web アプリケーションのアーキテクチャを提供します。 AngularJS は小さく、わずか 60K で、主流のブラウザと互換性があり、jQuery とうまく連携します。双方向のデータ バインディングは、AngularJS の最もクールで最も実用的な機能であり、MVC の原則を明確に示しています。
AngularJS の動作原理は次のとおりです。HTML テンプレートはブラウザーによって DOM に解析され、DOM 構造は AngularJS コンパイラーの入力になります。 AngularJS は DOM テンプレートを走査して、対応する NG 命令を生成します。すべての命令は、ビュー (つまり、HTML の ng-model) のデータ バインディングを設定します。したがって、NG フレームワークは DOM がロードされた後にのみ機能し始めます。
HTML 内:
<body ng-app="ngApp"> <div ng-controller="ngCtl"> <label ng-model="myLabel"></label> <input type="text" ng-model="myInput" /> <button ng-model="myButton" ng-click="btnClicked"></button> </div> </body>
js 内:
// angular app var app = angular.module("ngApp", [], function(){ console.log("ng-app : ngApp"); }); // angular controller app.controller("ngCtl", [ '$scope', function($scope){ console.log("ng-controller : ngCtl"); $scope.myLabel = "text for label"; $scope.myInput = "text for input"; $scope.btnClicked = function() { console.log("Label is " + $scope.myLabel); } }]);
上記のように、まず HTML で Angular アプリを定義し、Angular コントローラーを指定します。その後、コントローラーはスコープに対応します ($scope プレフィックスを使用してスコープ内のプロパティとメソッドを指定できます)。または、ngCtl のスコープ内での HTML タグの操作は、$scope.
を通じて js のプロパティとメソッドにバインドできます。このようにして、NG の双方向データ バインディングが実現されます。つまり、HTML で表示されるビューは AngularJS のデータと一致し、一方が変更されると、対応する他方もそれに応じて変更されます。
このメソッドは非常に使いやすく、HTML タグのスタイルと、js の angular コントローラー スコープにバインドされた対応するプロパティとメソッドのみを考慮しており、多くの複雑な DOM 操作はすべて省略されています。 🎜>
この種の考え方は、実際には jQuery の DOM クエリと操作とはまったく異なります。そのため、AngularJS を使用する場合は、jQuery を混合しないことを推奨する人もいます。もちろん、どちらにも利点と欠点があるため、どちらを使用するかによって異なります。それはあなたの選択です。NG のアプリはモジュールに相当します。各アプリには複数のコントローラーを定義でき、各コントローラーは独自のスコープ空間を持ち、相互に干渉しません。
バインドされたデータはどのように有効になります
AngularJS の初心者はこの罠に陥る可能性があります: というコマンドがあるとします。
var app = angular.module("test", []); app.directive("myclick", function() { return function (scope, element, attr) { element.on("click", function() { scope.counter++; }); }; }); app.controller("CounterCtrl", function($scope) { $scope.counter = 0; }); <body ng-app="test"> <div ng-controller="CounterCtrl"> <button myclick>increase</button> <span ng-bind="counter"></span> </div> </body>
scope.counter の後にscope.digest(); を追加して、動作するかどうかを確認してください。
なぜこれを行う必要があるのですか? どのような状況でこれを行う必要がありますか?最初の例にはダイジェストが存在しないことがわかり、ダイジェストを記述すると、他のダイジェストが作成中であることを示す例外もスローされます。
まず考えてみましょう。AngularJS を使用せずにそのような関数を自分で実装したい場合はどうなるでしょうか?
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>two-way binding</title> </head> <body onload="init()"> <button ng-click="inc"> increase 1 </button> <button ng-click="inc2"> increase 2 </button> <span style="color:red" ng-bind="counter"></span> <span style="color:blue" ng-bind="counter"></span> <span style="color:green" ng-bind="counter"></span> <script type="text/javascript"> /* 数据模型区开始 */ var counter = 0; function inc() { counter++; } function inc2() { counter+=2; } /* 数据模型区结束 */ /* 绑定关系区开始 */ function init() { bind(); } function bind() { var list = document.querySelectorAll("[ng-click]"); for (var i=0; i<list.length; i++) { list[i].onclick = (function(index) { return function() { window[list[index].getAttribute("ng-click")](); apply(); }; })(i); } } function apply() { var list = document.querySelectorAll("[ng-bind='counter']"); for (var i=0; i<list.length; i++) { list[i].innerHTML = counter; } } /* 绑定关系区结束 */ </script> </body> </html>
可以看到,在这么一个简单的例子中,我们做了一些双向绑定的事情。从两个按钮的点击到数据的变更,这个很好理解,但我们没有直接使用DOM的onclick方法,而是搞了一个ng-click,然后在bind里面把这个ng-click对应的函数拿出来,绑定到onclick的事件处理函数中。为什么要这样呢?因为数据虽然变更了,但是还没有往界面上填充,我们需要在此做一些附加操作。
从另外一个方面看,当数据变更的时候,需要把这个变更应用到界面上,也就是那三个span里。但由于Angular使用的是脏检测,意味着当改变数据之后,你自己要做一些事情来触发脏检测,然后再应用到这个数据对应的DOM元素上。问题就在于,怎样触发脏检测?什么时候触发?
我们知道,一些基于setter的框架,它可以在给数据设值的时候,对DOM元素上的绑定变量作重新赋值。脏检测的机制没有这个阶段,它没有任何途径在数据变更之后立即得到通知,所以只能在每个事件入口中手动调用apply(),把数据的变更应用到界面上。在真正的Angular实现中,这里先进行脏检测,确定数据有变化了,然后才对界面设值。
所以,我们在ng-click里面封装真正的click,最重要的作用是为了在之后追加一次apply(),把数据的变更应用到界面上去。
那么,为什么在ng-click里面调用$digest的话,会报错呢?因为Angular的设计,同一时间只允许一个$digest运行,而ng-click这种内置指令已经触发了$digest,当前的还没有走完,所以就出错了。
$digest和$apply
在Angular中,有$apply和$digest两个函数,我们刚才是通过$digest来让这个数据应用到界面上。但这个时候,也可以不用$digest,而是使用$apply,效果是一样的,那么,它们的差异是什么呢?
最直接的差异是,$apply可以带参数,它可以接受一个函数,然后在应用数据之后,调用这个函数。所以,一般在集成非Angular框架的代码时,可以把代码写在这个里面调用。
var app = angular.module("test", []); app.directive("myclick", function() { return function (scope, element, attr) { element.on("click", function() { scope.counter++; scope.$apply(function() { scope.counter++; }); }); }; }); app.controller("CounterCtrl", function($scope) { $scope.counter = 0; });
除此之外,还有别的区别吗?
在简单的数据模型中,这两者没有本质差别,但是当有层次结构的时候,就不一样了。考虑到有两层作用域,我们可以在父作用域上调用这两个函数,也可以在子作用域上调用,这个时候就能看到差别了。
对于$digest来说,在父作用域和子作用域上调用是有差别的,但是,对于$apply来说,这两者一样。我们来构造一个特殊的示例:
var app = angular.module("test", []); app.directive("increasea", function() { return function (scope, element, attr) { element.on("click", function() { scope.a++; scope.$digest(); }); }; }); app.directive("increaseb", function() { return function (scope, element, attr) { element.on("click", function() { scope.b++; scope.$digest(); //这个换成$apply即可 }); }; }); app.controller("OuterCtrl", ["$scope", function($scope) { $scope.a = 1; $scope.$watch("a", function(newVal) { console.log("a:" + newVal); }); $scope.$on("test", function(evt) { $scope.a++; }); }]); app.controller("InnerCtrl", ["$scope", function($scope) { $scope.b = 2; $scope.$watch("b", function(newVal) { console.log("b:" + newVal); $scope.$emit("test", newVal); }); }]); <div ng-app="test"> <div ng-controller="OuterCtrl"> <div ng-controller="InnerCtrl"> <button increaseb>increase b</button> <span ng-bind="b"></span> </div> <button increasea>increase a</button> <span ng-bind="a"></span> </div> </div>
这时候,我们就能看出差别了,在increase b按钮上点击,这时候,a跟b的值其实都已经变化了,但是界面上的a没有更新,直到点击一次increase a,这时候刚才对a的累加才会一次更新上来。怎么解决这个问题呢?只需在increaseb这个指令的实现中,把$digest换成$apply即可。
当调用$digest的时候,只触发当前作用域和它的子作用域上的监控,但是当调用$apply的时候,会触发作用域树上的所有监控。
因此,从性能上讲,如果能确定自己作的这个数据变更所造成的影响范围,应当尽量调用$digest,只有当无法精确知道数据变更造成的影响范围时,才去用$apply,很暴力地遍历整个作用域树,调用其中所有的监控。
从另外一个角度,我们也可以看到,为什么调用外部框架的时候,是推荐放在$apply中,因为只有这个地方才是对所有数据变更都应用的地方,如果用$digest,有可能临时丢失数据变更。
脏检测的利弊
很多人对Angular的脏检测机制感到不屑,推崇基于setter,getter的观测机制,在我看来,这只是同一个事情的不同实现方式,并没有谁完全胜过谁,两者是各有优劣的。
大家都知道,在循环中批量添加DOM元素的时候,会推荐使用DocumentFragment,为什么呢,因为如果每次都对DOM产生变更,它都要修改DOM树的结构,性能影响大,如果我们能先在文档碎片中把DOM结构创建好,然后整体添加到主文档中,这个DOM树的变更就会一次完成,性能会提高很多。
同理,在Angular框架里,考虑到这样的场景:
function TestCtrl($scope) { $scope.numOfCheckedItems = 0; var list = []; for (var i=0; i<10000; i++) { list.push({ index: i, checked: false }); } $scope.list = list; $scope.toggleChecked = function(flag) { for (var i=0; i<list.length; i++) { list[i].checked = flag; $scope.numOfCheckedItems++; } }; }
インターフェース上の特定のテキストがこの numOfCheckedItems にバインドされている場合はどうなりますか?ダーティ検出メカニズムでは、このプロセスはストレスなくすべてのデータ変更が完了し、インターフェイス全体に適用されます。現時点では、Angular のようにバッチ処理を 1 回の更新に遅延させない限り、セッターベースのメカニズムは悲惨であり、パフォーマンスはさらに低下します。
したがって、2 つの異なる監視方法にはそれぞれ利点と欠点があるため、それぞれの使用方法の違いを理解し、さまざまなビジネス シナリオでのパフォーマンスの違いを考慮することが最も簡単です。パフォーマンスのボトルネック。