Angular JS (Angular.JS) is a set of frameworks, templates, data binding and rich UI components used to develop web pages. It supports the entire development process and provides the architecture of web applications without manual DOM manipulation. AngularJS is small, only 60K, compatible with mainstream browsers, and works well with jQuery. Two-way data binding may be the coolest and most practical feature of AngularJS, which fully demonstrates the principles of MVC. The working principle of AngularJS is: the HTML template will be parsed into the DOM by the browser, and the DOM structure becomes the input of the AngularJS compiler . AngularJS will traverse the DOM template to generate corresponding NG instructions. All instructions are responsible for setting data binding for the view (that is, ng-model in HTML). Therefore, the NG framework only starts to work after the DOM is loaded.
In 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>
In 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); } }]);
As above, we first define an angular in html If you specify an angular controller in your app, the controller will correspond to a scope (you can use the $scope prefix to specify properties and methods in the scope). Then the HTML tag in the scope of the ngCtl, its value is or Operations can be bound to properties and methods in js through $scope. In this way, NG's two-way data binding is realized: that is, the view presented in HTML is consistent with the data in AngularJS. Modify it 1, the corresponding other end will also change accordingly.
This method is really very convenient to use. We only care about the style of the HTML tag and its corresponding attributes bound under the angular controller scope in js and methods. That’s all, all the complex DOM operations are omitted.
This kind of thinking is actually completely different from jQuery’s DOM query and operation, so many people suggest not to mix it when using AngularJS Use jQuery. Of course, both have their own advantages and disadvantages. Which one to use depends on your choice. The app in NG is equivalent to a module. Multiple controllers can be defined in each app, and each controller will have Their respective scope spaces will not interfere with each other.
How does binding data take effect?
Beginners to AngularJS may step into such a pit. Suppose there is a command: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>
Why should we do this? Under what circumstances should we do this? We found that there is no digest in the first example, and if you write digest, it will also throw an exception saying that other digests are being made. What is going on?
Let’s think about it first, if there is no AngularJS, what should we do if we want to implement such a function ourselves?
<!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>
As you can see, in such a simple example, we have done some two-way binding. From the click of two buttons to the change of data, this is easy to understand, but we did not directly use the onclick method of the DOM. Instead, we created an ng-click, and then took out the function corresponding to the ng-click in bind. Bind to the onclick event handler function. Why has to be this way? Because although the data has changed, it has not been filled in the interface, so we need to do some additional operations here. From another aspect, when the data changes, this change needs to be applied to the interface, that is, to the three spans. But since Angular uses dirty detection, it means that after changing the data, you have to do something yourself to trigger the dirty detection, and then apply it to the DOM element corresponding to the data. The question is, how to trigger dirty detection? When is it triggered?
We know that some setter-based frameworks can reassign bound variables on DOM elements when setting values for data. The dirty detection mechanism does not have this stage. It has no way to be notified immediately after the data changes, so you can only manually call apply() in each event entry to apply the data changes to the interface. In a real Angular implementation, dirty detection is performed first to determine if the data has changed, and then the interface value is set.
So, we encapsulate the real click in ng-click. The most important function is to add an apply() later to apply the data changes to the interface.
So, why does an error appear when calling $digest in ng-click? Because of Angular's design, only one $digest is allowed to run at the same time, and the built-in instruction ng-click has already triggered $digest, and the current one has not been completed yet, so an error occurred.
$digest and $apply
In Angular, there are two functions, $apply and $digest. We just used $digest to apply this data to the interface. But at this time, you can use $apply instead of $digest. The effect is the same. So, what is the difference between them?最直接的差异是,$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,会怎样?在脏检测的机制下,这个过程毫无压力,一次做完所有数据变更,然后整体应用到界面上。这时候,基于setter的机制就惨了,除非它也是像Angular这样把批量操作延时到一次更新,否则性能会更低。
所以说,两种不同的监控方式,各有其优缺点,最好的办法是了解各自使用方式的差异,考虑出它们性能的差异所在,在不同的业务场景中,避开最容易造成性能瓶颈的用法。
更多深入学习AngularJS中数据的双向绑定机制相关文章请关注PHP中文网!