这里有新鲜出炉的 AngularJS Tutorial 中文版,程序狗速度看过来!
AngularJS 诞生于 Google 是一款优秀的前端 JS 框架,已经被用于 Google 的多款产品当中。AngularJS 有着诸多特性,最为核心的是:MVC、模块化、自动化双向数据绑定、语义化标签、依赖注入,等等。
这篇文章主要介绍了 Angular JS 数据的双向绑定详解及实例的相关资料, 需要的朋友可以参考下
Angular JS 数据的双向绑定
接触 AngularJS 许了,时常问自己一些问题,如果是我实现它,会在哪些方面选择跟它相同的道路,哪些方面不同。为此,记录了一些思考,给自己回顾,也供他人参考。
初步大致有以下几个方面:
数据的双向绑定
Angular 实现了双向绑定机制。所谓的双向绑定,无非是从界面的操作能实时反映到数据,数据的变更能实时展现到界面。
一个最简单的示例就是这样:
- <div ng-controller="CounterCtrl">
- <span ng-bind="counter"></span>
- <button ng-click="counter=counter+1">increase</button>
- </div>
- function CounterCtrl($scope) {
- $scope.counter = 1;
- }
这个例子很简单,毫无特别之处,每当点击一次按钮,界面上的数字就增加一。
绑定数据是怎样生效的
初学 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>
这个时候,点击按钮,界面上的数字并不会增加。很多人会感到迷惑,因为他查看调试器,发现数据确实已经增加了,Angular 不是双向绑定吗,为什么数据变化了,界面没有跟着刷新?
试试在 scope.counter++; 这句之后加一句 scope.digest(); 再看看是不是好了?
为什么要这么做呢,什么情况下要这么做呢?我们发现第一个例子中并没有 digest,而且,如果你写了 digest,它还会抛出异常,说正在做其他的 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);
- });
- }]);
- increase b
- increase a
这时候,我们就能看出差别了,在 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 这样把批量操作延时到一次更新,否则性能会更低。
所以说,两种不同的监控方式,各有其优缺点,最好的办法是了解各自使用方式的差异,考虑出它们性能的差异所在,在不同的业务场景中,避开最容易造成性能瓶颈的用法。
来源: http://www.phperz.com/article/17/0503/328970.html