看O'Reilly的书看到$watch这部分,不过没看懂,网上很多资料也含糊不清,不过还是找到了几个好的,简单记录一下。
一句话说明,$watch是用来监视变量的,好了直接上代码
<html> <head> <script src='./js/angular.min.js'></script> </head> <body ng-app='watch'> <input ng-model='name' type='text'/> <div>change count: {{count}}</div> <script> var app = angular.module('watch',[]); app.run(['$rootScope',function($rootScope){ $rootScope.count = 0; $rootScope.name = 'Alfred'; $rootScope.$watch('name',function(){ $rootScope.count++; }) }]); </script> </body> </html>
上面这个代码就是用来监控name的变化的,每次当我们在input输入框中输入一个值的时候,$rootScope中的count就会对应的+1。
在angularJS的内部,每当我们对name的值进行修改的时候,angularJS内部中的$digest就会被调用一次,并在运行结束之后检查我们用$watch来监控的模型,如何和上一次执行$digest之前相比发生变化了,则执行$watch中的回调函数。
然而!!!在我们实际的开发中,仅仅实现对一个原始类型的数据监控是远远不能够满足所需的,对于原始类型的数据,如果我们使用了一次赋值操作,则这个原始类型的数据变量会真正的赋值一次,然而对于引用类型的变量,进行赋值时,仅仅是将赋值的变量指向了这个引用类型。
<html> <head> <script src='./js/angular.min.js'></script> </head> <body ng-app='watch'> <div ng-repeat='item in items'> <input ng-model='item.a'/><span>{{item.a}}</span> </div> <div>change count: {{count}}</div> <script> var app = angular.module('watch',[]) app.run(['$rootScope',function($rootScope){ $rootScope.count = 0; $rootScope.items = [ { "a": 1 }, { "a": 2 }, { "a": 3 }, { "a": 4 } ]; $rootScope.$watch('items',function(){ $rootScope.count++; }); }]); </script> </body> </html>
在这个栗子中我们就会发现,不管我们怎么改变其中的值,count都不会发生变化的。而这个就是我们上面说那样,在说明这个之前,我们在说明下$watch的第三个参数,一般$watch函数的前两个参数是必传的(监控对象,回调函数),第三个参数默认为false,这样的话我们进行的监控叫做引用监控,这个意思就是监控对象的应用没有发生变化的时候就不算对象发生了变化,具体的来说,上面的例子,就算items的属性发生了变化,只要items的引用没有发生变化,$watch就都当做没有看见,但是比如讲一个数组赋值给items时,这个时候$watch就看不下去了。
相反,如果第三个参数设置为true的时候,那么我们的监控叫做“全等监控”,此时的$watch的要求就是比较苛刻了,只要他监控的对象有一点点变化时,$watch就会跳出来,卧槽!你居然还敢动!!!
当然值得提一下的是:为什么第三个参数加个true,这么方便了我们还不加呢?!当然是牵涉到性能的问题啦!全等监控运行起来的时候是先监控到整个对象,然后在每一次把$digest跑起来之前先用angualr.copy()将整个对象先拷贝之后再调用angular.equal()方法来进行比较,所以这一监控可能会消耗大量的资源!
在angularJS 1.1.4又出来了一个$watchCollection()方法,专门来监控数组集合的,他的性能介于引用监控和全等监控之间,它不会对数组的每一项内容 进行监控,而是当数组的pop和push时候做出反应。
$apply
$apply的作用是把改变同步绑定到界面上,但是它为什么存在呢?什么时候需要用它呢?什么时候又不需要呢?
那么我们首先说一下angular是如何进行数据双向绑定的吧。
要知道一个变量变了,方法无非就两种
1、 通过固定的接口,比如set,get方法,通过set设置变量的值,set被调用时做个比较就可以,但是这个方法和复杂!
2、 脏检查,将某一个对象复制一份快照,在某个时间,比较现在对象与快照的值。很明显,这个方法要复制两份对象,而且要遍历对象,比较每一个属性。对!这样的确有性能问题!但是angular就是用这个的~
但是人家angular的脏检查不是对所有对象进行检查,只是当对象绑定到html中,该对象才复合检查对象(watcher),同理,angular对属性的脏检查也是如此。
watcher源码
watcher = { fn: listener, //监听回调函数 last: initWatchVal, //上一状态值 get: get, //取得监听的值 exp: watchExp, //监听表达式 eq: !!objectEquality //要不要比较引用 };
那么我们什么时候去进行脏检查呢?
脏检查的点是在函数执行完之后,但是不标明异步调用也执行完毕,如果我们的功能是异步的,那么我们会发现我们的改变并没有更新到DOM上。
举个栗子:
function Ctrl($scope) { $scope.message = "Waiting 2000ms for update"; setTimeout(function () { $scope.message = "Timeout called!"; // AngularJS unaware of update to $scope }, 2000); },2000); }
DOM上永远都不会显示Timeout called
当然,这个就是我们$apply的应用场景了,调用它,手动触发脏检查,举个例子:angularJs提供了$timeout,为什么咱有了setTimeout还要提供这个呢?就是应为$timeout异步完成后,angularJs会自动触发$apply。
注:应该尽可能地把要执行的代码和函数传递给$apply去执行,而不要自已执行那些函数然后再调用$apply。
例如
$scope.$apply(function() { $scope.variable1 = 'some value'; executeSomeAction(); });
尽可能别这样
$scope.variable1 = 'some value';
executeSomeAction();
$scope.$apply();
$digest
然而这个脏检查有事怎么检查的呢??
$apply被调用后最终都会触发$digest()
在调用了$scope.$digest()后,$digest循环就开始了。
假设你在一个ng-click指令对应的handler函数中更改了scope中的一条数据,此时AngularJS会自动地通过调用$digest()来触发一轮$digest循环。
当$digest循环开始后,它会触发每个watcher。这些watchers会检查scope中的当前model值是否和上一次计算得到的model值不同。
如果不同,那么对应的回调函数会执行。
调用该函数的结果,就是view中的表达式内容会被更新。
除了ng-click指令,还有一些其它的built-in指令以及服务来让你更改models(比如ng-model,$timeout 等 ) 和自动触发一次 $digest 循环。
$apply补充
HTML:
<body ng-app="myApp"> <div ng-controller="MessageController"> Delayed Message: {{message}} </div> </body>
JavaScript:
angular.module('myApp',[]).controller('MessageController', function($scope) { $scope.getMessage = function() { setTimeout(function() { $scope.message = 'Fetched after 2 seconds'; console.log('message:'+$scope.message); }, 2000); } $scope.getMessage(); });
通过运行这个例子,你会看到过了两秒钟之后,控制台确实会显示出已经更新的model,然而,view并没有更新。原因也许你已经知道了,就是我们忘了调用$apply()方法。因此,我们需要修改getMessage(),如下所示:
angular.module('myApp',[]).controller('MessageController', function($scope) { $scope.getMessage = function() { setTimeout(function() { $scope.$apply(function() { //wrapped this within $apply $scope.message = 'Fetched after 3 seconds'; console.log('message:' + $scope.message); }); }, 2000); } $scope.getMessage(); });
如果你运行了上面的例子,你会看到view在两秒钟之后也会更新。唯一的变化是我们的代码现在被wrapped到了$scope.$apply()中,它会自动触发$rootScope.$digest(),从而让watchers被触发用以更新view。
Note:顺便提一下,你应该使用$timeout service来代替setTimeout(),因为前者会帮你调用$apply(),让你不需要手动地调用它。
而且,注意在以上的代码中你也可以在修改了model之后手动调用没有参数的$apply(),就像下面这样:
$scope.getMessage = function() { setTimeout(function() { $scope.message = 'Fetched after two seconds'; console.log('message:' + $scope.message); $scope.$apply(); //this triggers a $digest }, 2000); };
以上的代码使用了$apply()的第二种形式,也就是没有参数的形式。需要记住的是你总是应该使用接受一个function作为参数的$apply()方法。这是因为当你传入一个function到$apply()中的时候,这个function会被包装到一个try…catch块中,所以一旦有异常发生,该异常会被$exceptionHandler service处理。