$scope.$apply()或者$scope.$digest()

http://blog.csdn.net/aitangyong/article/details/48972643

如果我们使用了AngularJS中的$scope.$apply()或者$scope.$digest(),我们很可能会遇到类似下面的错误,虽然这个错误没有太大影响,但是在日志中看起来还是很不爽的,日志中记录的异常或者错误,就应该是需要关注和解决的问题,否则就没有必要出现在日志中了。

[plain] view plain copy
 
  1. Error: [$rootScope:inprog] $apply already in progress  
  2. http://errors.angularjs.org/1.3.13/$rootScope/inprog?p0=%24apply  
  3.     at angular.js:63  
  4.     at beginPhase (angular.js:14755)  
  5.     at Scope.$apply (angular.js:14499)  
  6.     at new <anonymous> (1%20-%20%E5%89%AF%E6%9C%AC.html:10)  
  7.     at Object.invoke (angular.js:4185)  
  8.     at extend.instance (angular.js:8454)  
  9.     at angular.js:7700  
  10.     at forEach (angular.js:331)  
  11.     at nodeLinkFn (angular.js:7699)  
  12.     at compositeLinkFn (angular.js:7078)  

下面这段代码就会报上面这个错误:

[javascript] view plain copy
 
  1. var myModule = angular.module('myModule', []);    
  2. myModule.controller("ctrl_1",function($scope){  
  3. $scope.value = "aty";  
  4. $scope.$apply();  
  5. //$scope.$digest();  
  6. });  

 

这个错误其实很好理解:angularJS框架本身已经在做脏数据检测了,我们没有必要再手动调用$apply或者$digest。这里自然而然出现了一个疑问:什么时候需要我们手动调用$apply或者$digest,什么时候不需要呢?这是个好问题,我现在也不知道,目前我只能列出自己在项目中遇到的2种需要手动调用$apply的情况。

 

情况1:controller中如果有异步操作,比如ajax回调,timeout延时等。可以这么理解:由于异步(延迟)的存在,当开始执行回调函数的时候,angularJS自身controller中的脏值检测已经结束,无法检测到回调函数导致数据的变化。

[html] view plain copy
 
  1. <html>  
  2.   <head>  
  3.     <script src="jquery-1.11.1.min.js"></script>  
  4.     <script src="angular.js"></script>    
  5.     <script>  
  6.           
  7.         var myModule = angular.module('myModule', []);    
  8.         myModule.controller("ctrl_1",function($scope){  
  9.             $scope.text = "place";  
  10.               
  11.             setTimeout(function(){  
  12.                 $scope.text = "value setted after time out";  
  13.                 $scope.$apply();//必需手动进行脏值检测,否则数据无法刷新到界面  
  14.             },1000);  
  15.           
  16.         });   
  17.           
  18.         $(function(){  
  19.             angular.bootstrap($("#div1")[0], ["myModule"]);   
  20.               
  21.         })  
  22.     </script>  
  23.   </head>  
  24.   <body>  
  25.     <div id="div1" ng-controller="ctrl_1">  
  26.         <div>{{text}}</div>  
  27.         <input id="btn" type="button" value="jquery-event"></input>       
  28.     </div>    
  29.   </body>  
  30. </html>  
  31.   
  32.       
  33.       

这段代码如果没有调用$scope.$apply()的话,数据是不会刷新到界面上的。

 

 

情况2:在jQuery代码中修改$scope中的数据。这种情况是在angular框架之外操作$scope中的数据,angular不能检测到数据变化是正常的。

[html] view plain copy
 
  1. <html>  
  2.   <head>  
  3.     <script src="jquery-1.11.1.min.js"></script>  
  4.     <script src="angular.js"></script>    
  5.     <script>  
  6.           
  7.         var myModule = angular.module('myModule', []);    
  8.         myModule.controller("ctrl_1",function($scope){  
  9.             $scope.text = "place";  
  10.         });   
  11.           
  12.         $(function(){  
  13.             angular.bootstrap($("#div1")[0], ["myModule"]);   
  14.   
  15.             $("#btn").click(function(){  
  16.                 var $scope = $("#btn").scope();  
  17.                 $scope.text = "value setted in jquery";  
  18.                 $scope.$apply();  
  19.             });           
  20.         })  
  21.     </script>  
  22.   </head>  
  23.   <body>  
  24.     <div id="div1" ng-controller="ctrl_1">  
  25.         <div>{{text}}</div>  
  26.         <input id="btn" type="button" value="jquery-event"></input>       
  27.     </div>    
  28.   </body>  
  29. </html>  
  30.   
  31.       

在JQuery的事件处理函数中,我们能够通过dom拿到其关联的$scope对象,进而修改$scope中的数据。这种情况下,也必需要手动调用$scope.$apply()。

也就是说我们必须要清楚哪儿些情况需要手动$apply,哪儿些情况不需要手动$apply,这看起来很简单,实际上并不是这样的。看一段我们项目中的代码:

 

[javascript] view plain copy
 
  1. var myModule = angular.module('myModule', []);    
  2. myModule.controller("ctrl_1",function($scope){  
  3.     $scope.listItems = [];  
  4.       
  5.     $scope.loadListFromService = function(){  
  6.         Spl.MessageProcessor.loadData({  
  7.                     serviceId : "url",  
  8.                     data : {},  
  9.                     success : function(json) {  
  10.                         $scope.listItems = json.results;  
  11.             // 要不要$scope.$apply()?  
  12.                     },  
  13.                     error: function() {  
  14.                         console.error("invoke service["+optionsJson.serviceId+"] error.");  
  15.                     }  
  16.         });  
  17.       
  18.     }  
  19.       
  20.     $scope.loadListFromService();  
  21.       
  22. });  

loadData()这个函数很像ajax回调,确实是这样的,这个API不过是做了一点封装,大致代码如下:

 

 

[javascript] view plain copy
 
  1. function loadData(options)  
  2. {  
  3.     // 从本地缓存中读取,很快  
  4.     var dataInCache = U.loadDataFromCache(options.serviceId);  
  5.     if(dataInCache)  
  6.     {  
  7.         options.success(dataInCache);  
  8.     }  
  9.     else  
  10.     {  
  11.         //异步ajax  
  12.         U.readDataFromServer(options.serviceId, options.data, function(response){  
  13.             options.success(response);  
  14.         });  
  15.     }  
  16. }  


由于缓存的影响,$scope.loadListFromService()变得不再那么可控,如果本地已经有缓存了,那直接读取缓存是很快的,这个时候不需要手动$apply;如果是第一次,本地没有缓存,那么就变成了情况1,我们需要手动$apply。显然调用loadData()函数的地方,不需要也不应该关注到底有没有缓存的存在,这个时候判断需要不要手动$apply就没有那么容易的。简单粗暴的方式,不管怎么样都手动调用下$scope.$apply(),这样功能不会有问题,但是日志中就不可避免要出现文中开头提到的错误。

 

 

angular中的$scope中提供了一个$$phase变量,如果这个变量的值是"$digest" 或者"$apply",就代表angular自身已经在做脏值检测了,不需要我们再去调用$apply或者$digest;否则的话就需要我们手动调用$apply或者$digest了。利用这个属性,我们就可以方便解决上面的错误了,判断一下呗。下面是一个工具函数,好理解吧。

 

[plain] view plain copy
 
  1. function safeApply(scope, fn) {  
  2.     (scope.phase || scope.$root.phase) ? fn() : scope.$apply(fn);  
  3. }  

 


最后提一下,$digest、$apply、$$phase这些属性或者方法其实都是$scope中的私有的,最好不要使用。如果你用到了这些方法,基本可以断言你的代码存在问题,没有按照angular的方式来组织代码。比如情况1中的setTimeout,完全可以用angular中的$timeout代替,这才是推荐的方式,而不是通过$apply来补救。

 

 

[html] view plain copy
 
  1. <html>  
  2.   <head>  
  3.     <script src="jquery-1.11.1.min.js"></script>  
  4.     <script src="angular.js"></script>    
  5.     <script>  
  6.           
  7.         var myModule = angular.module('myModule', []);    
  8.         myModule.controller("ctrl_1",function($scope, $timeout){  
  9.             $scope.text = "place";  
  10.               
  11.             $timeout(function(){  
  12.                 $scope.text = "value setted after time out";  
  13.             },1000);  
  14.           
  15.         });   
  16.           
  17.         $(function(){  
  18.             angular.bootstrap($("#div1")[0], ["myModule"]);   
  19.               
  20.         })  
  21.     </script>  
  22.   </head>  
  23.   <body>  
  24.     <div id="div1" ng-controller="ctrl_1">  
  25.         <div>{{text}}</div>  
  26.         <input id="btn" type="button" value="jquery-event"></input>       
  27.     </div>    
  28.   </body>  
  29. </html>  

 

所以解决“$apply already in progress”最好的方式,就是不要使用$scope.$apply()或者$scope.$digest()。

posted @ 2016-11-15 14:57  五艺  阅读(592)  评论(0)    收藏  举报