Understanding Angular’s $apply() and $digest() / 理解Angular中的$apply()以及$digest()

时间:2021-05-24 18:34:56

中文翻译转自:http://blog.csdn.net/dm_vincent/article/details/38705099

原文转自:http://www.sitepoint.com/understanding-angulars-apply-digest/

$apply() and $digest() are two core, and sometimes confusing, aspects of AngularJS. To understand how AngularJS works one needs to fully understand how$apply() and$digest() work. This article aims to explain what$apply() and$digest() really are, and how they can be useful in your day-to-day AngularJS programming.

$apply and$digest Explored

AngularJS offers an incredibly awesome feature known as two way data binding which greatly simplifies our lives. Data binding means that when you change something in the view, thescope modelautomagically updates. Similarly, whenever thescope model changes, the view updates itself with the new value. How does does AngularJS do that? When you write an expression ({{aModel}}), behind the scenes Angular sets up a watcher on the scope model, which in turn updates the view whenever the model changes. Thiswatcher is just like any watcher you set up in AngularJS:

$scope.$watch('aModel', function(newValue, oldValue) {
//update the DOM with newValue
});

The second argument passed to $watch() is known as a listener function, and is called whenever the value ofaModel changes. It is easy for us to grasp that when the value ofaModel changes this listener is called, updating the expression in HTML. But, there is still one big question! How does Angular figure out when to call this listener function? In other words, how does AngularJS know whenaModel changes so it can call the corresponding listener? Does it run a function periodically to check whether the value of thescope model has changed? Well, this is where the$digest cycle steps in.

It’s the $digest cycle where the watchers are fired. When a watcher is fired, AngularJS evaluates thescope model, and if it has changed then the corresponding listener function is called. So, our next question is when and how this$digest cycle starts.

The $digest cycle starts as a result of a call to$scope.$digest(). Assume that you change ascope model in a handler function through theng-click directive. In that case AngularJS automatically triggers a$digest cycle by calling$digest(). When the$digest cycle starts, it fires each of the watchers. These watchers check if the current value of thescope model is different from last calculated value. If yes, then the corresponding listener function executes. As a result if you have any expressions in the view they will be updated. In addition tong-click, there are several other built-in directives/services that let you change models (e.g.ng-model,$timeout, etc) and automatically trigger a$digest cycle.

So far, so good! But, there is a small gotcha. In the above cases, Angular doesn’t directly call$digest(). Instead, it calls$scope.$apply(), which in turn calls$rootScope.$digest(). As a result of this, a digest cycle starts at the$rootScope, and subsequently visits all the child scopes calling the watchers along the way.

Now, let’s assume you attach an ng-click directive to a button and pass a function name to it. When the button is clicked, AngularJS wraps the function call within$scope.$apply(). So, your function executes as usual, change models (if any), and a$digest cycle starts to ensure your changes are reflected in the view.

Note: $scope.$apply() automatically calls$rootScope.$digest(). The$apply() function comes in two flavors. The first one takes a function as an argument, evaluates it, and triggers a$digest cycle. The second version does not take any arguments and just starts a$digest cycle when called. We will see why the former one is the preferred approach shortly.

When Do You Call
$apply()
Manually?

If AngularJS usually wraps our code in $apply() and starts a$digest cycle, then when do you need to do call$apply() manually? Actually, AngularJS makes one thing pretty clear. It will account for only those model changes which are done inside AngularJS’ context (i.e. the code that changes models is wrapped inside$apply()). Angular’s built-in directives already do this so that any model changes you make are reflected in the view. However, if you change any model outside of the Angular context, then you need to inform Angular of the changes by calling$apply() manually. It’s like telling Angular that you are changing some models and it should fire thewatchers so that your changes propagate properly.

For example, if you use JavaScript’s setTimeout() function to update ascope model, Angular has no way of knowing what you might change. In this case it’s your responsibility to call$apply() manually, which triggers a$digest cycle. Similarly, if you have a directive that sets up a DOM event listener and changes some models inside the handler function, you need to call$apply() to ensure the changes take effect.

Let’s look at an example. Suppose you have a page, and once the page loads you want to display a message after a two second delay. Your implementation might look something like the JavaScript and HTML shown in the following listing.

    <body ng-app="myApp">
<div ng-controller="MessageController">
Delayed Message: {{message}}
</div>
</body>

/* What happens without an $apply() */

angular.module('myApp',[]).controller('MessageController', function($scope) {

$scope.getMessage = function() {
setTimeout(function() {
$scope.message = 'Fetched after 3 seconds';
console.log('message:'+$scope.message);
}, 2000);
}


By running the example, you will see that the delayed function runs after a two second interval, and updates thescope modelmessage. Still, the view doesn’t update. The reason, as you may have guessed, is that we forgot to call$apply() manually. Therefore, we need to update ourgetMessage() function as shown below.

  <body ng-app="myApp">
<div ng-controller="MessageController">
Delayed Message: {{message}}
</div>
</body>
/* What happens with $apply */ 
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();

});

If you run this updated example, you can see the view update after two seconds. The only change is that we wrapped our code inside$scope.$apply() which automatically triggers$rootScope.$digest(). As a result the watchers are fired as usual and the view updates.

Note: By the way, you should use
$timeout
service whenever possible which is setTimeout() with automatic $apply() so that you don’t have to call$apply() manually.

Also, note that in the above code you could have done the model changes as usual and placed a call to$apply() (the no-arg version) in the end. Have a look at the following snippet:

$scope.getMessage = function() {
setTimeout(function() {
$scope.message = 'Fetched after two seconds';
console.log('message:' + $scope.message);
$scope.$apply(); //this triggers a $digest
}, 2000);
};

The above code uses the no-arg version of $apply() and works. Keep in mind that you should always use the version of$apply() that accepts a function argument. This is because when you pass a function to$apply(), the function call is wrapped inside atry...catch block, and any exceptions that occur will be passed to the$exceptionHandler service.

How Many Times Does the
$digest
Loop Run?

When a $digest cycle runs, the watchers are executed to see if thescope models have changed. If they have, then the corresponding listener functions are called. This leads to an important question. What if a listener function itself changed ascope model? How would AngularJS account for that change?

The answer is that the $digest loop doesn’t run just once. At the end of the current loop, it starts all over again to check if any of the models have changed. This is basically dirty checking, and is done to account for any model changes that might have been done by listener functions. So, the
$digest
cycle keeps looping until there are no more model changes, or it hits the max loop count of 10. It’s always good to stay idempotent and try to minimize model changes inside the listener functions.

Note: At a minimum, $digest will run twice even if your listener functions don’t change any models. As discussed above, it runs once more to make sure the models are stable and there are no changes.

Conclusion

I hope this article has clarified what $apply and$digest are all about. The most important thing to keep in mind is whether or not Angularcan detect your changes. If it cannot, then you must call $apply() manually.


$apply()$digest()AngularJS中是两个核心概念,但是有时候它们又让人困惑。而为了了解AngularJS的工作方式,首先需要了解$apply()$digest()是如何工作的。这篇文章旨在解释$apply()$digest()是什么,以及在日常的编码中如何应用它们。

 

探索$apply()$digest()

AngularJS提供了一个非常酷的特性叫做双向数据绑定(Two-way Data Binding),这个特性大大简化了我们的代码编写方式。数据绑定意味着当View中有任何数据发生了变化,那么这个变化也会自动地反馈到scope的数据上,也即意味着scope模型会自动地更新。类似地,当scope模型发生变化时,view中的数据也会更新到最新的值。那么AngularJS是如何做到这一点的呢?当你写下表达式如{{ aModel }}时,AngularJS在幕后会为你在scope模型上设置一个watcher,它用来在数据发生变化的时候更新view。这里的watcher和你会在AngularJS中设置的watcher是一样的:

 

[javascript] view plaincopyprint?
  1. $scope.$watch('aModel'function(newValue, oldValue) {  
  2.   //update the DOM with newValue  
  3. });  

 

传入到$watch()中的第二个参数是一个回调函数,该函数在aModel的值发生变化的时候会被调用。当aModel发生变化的时候,这个回调函数会被调用来更新view这一点不难理解,但是,还存在一个很重要的问题!AngularJS是如何知道什么时候要调用这个回调函数呢?换句话说,AngularJS是如何知晓aModel发生了变化,才调用了对应的回调函数呢?它会周期性的运行一个函数来检查scope模型中的数据是否发生了变化吗?好吧,这就是$digest循环的用武之地了。

 

$digest循环中,watchers会被触发。当一个watcher被触发时,AngularJS会检测scope模型,如何它发生了变化那么关联到该watcher的回调函数就会被调用。那么,下一个问题就是$digest循环是在什么时候以各种方式开始的?

 

在调用了$scope.$digest()后,$digest循环就开始了。假设你在一个ng-click指令对应的handler函数中更改了scope中的一条数据,此时AngularJS会自动地通过调用$digest()来触发一轮$digest循环。当$digest循环开始后,它会触发每个watcher。这些watchers会检查scope中的当前model值是否和上一次计算得到的model值不同。如果不同,那么对应的回调函数会被执行。调用该函数的结果,就是view中的表达式内容(译注:诸如{{ aModel }})会被更新。除了ng-click指令,还有一些其它的built-in指令以及服务来让你更改models(比如ng-model$timeout)和自动触发一次$digest循环。

 

目前为止还不错!但是,有一个小问题。在上面的例子中,AngularJS并不直接调用$digest(),而是调用$scope.$apply(),后者会调用$rootScope.$digest()。因此,一轮$digest循环在$rootScope开始,随后会访问到所有的children scope中的watchers

 

现在,假设你将ng-click指令关联到了一个button上,并传入了一个function名到ng-click上。当该button被点击时,AngularJS会将此function包装到一个wrapping function中,然后传入到$scope.$apply()。因此,你的function会正常被执行,修改models(如果需要的话),此时一轮$digest循环也会被触发,用来确保view也会被更新。

 

Note: $scope.$apply()会自动地调用$rootScope.$digest()$apply()方法有两种形式。第一种会接受一个function作为参数,执行该function并且触发一轮$digest循环。第二种会不接受任何参数,只是触发一轮$digest循环。我们马上会看到为什么第一种形式更好。

 

什么时候手动调用$apply()方法?

如果AngularJS总是将我们的代码wrap到一个function中并传入$apply(),以此来开始一轮$digest循环,那么什么时候才需要我们手动地调用$apply()方法呢?实际上,AngularJS对此有着非常明确的要求,就是它只负责对发生于AngularJS上下文环境中的变更会做出自动地响应(即,在$apply()方法中发生的对于models的更改)AngularJSbuilt-in指令就是这样做的,所以任何的model变更都会被反映到view中。但是,如果你在AngularJS上下文之外的任何地方修改了model,那么你就需要通过手动调用$apply()来通知AngularJS。这就像告诉AngularJS,你修改了一些models,希望AngularJS帮你触发watchers来做出正确的响应。

 

比如,如果你使用了JavaScript中的setTimeout()来更新一个scope model,那么AngularJS就没有办法知道你更改了什么。这种情况下,调用$apply()就是你的责任了,通过调用它来触发一轮$digest循环。类似地,如果你有一个指令用来设置一个DOM事件listener并且在该listener中修改了一些models,那么你也需要通过手动调用$apply()来确保变更会被正确的反映到view中。

 

让我们来看一个例子。加入你有一个页面,一旦该页面加载完毕了,你希望在两秒钟之后显示一条信息。你的实现可能是下面这个样子的:

 

HTML:

[html] view plaincopyprint?
  1. <body ng-app="myApp">  
  2.   <div ng-controller="MessageController">  
  3.     Delayed Message: {{message}}  
  4.   </div>    
  5. </body>  


JavaScript:
[javascript] view plaincopyprint?
  1. /* What happens without an $apply() */  
  2.       
  3.     angular.module('myApp',[]).controller('MessageController'function($scope) {  
  4.       
  5.       $scope.getMessage = function() {  
  6.         setTimeout(function() {  
  7.           $scope.message = 'Fetched after 3 seconds';  
  8.           console.log('message:'+$scope.message);  
  9.         }, 2000);  
  10.       }  
  11.         
  12.       $scope.getMessage();  
  13.       
  14.     });  

 

通过运行这个例子,你会看到过了两秒钟之后,控制台确实会显示出已经更新的model,然而,view并没有更新。原因也许你已经知道了,就是我们忘了调用$apply()方法。因此,我们需要修改getMessage(),如下所示:

 

[javascript] view plaincopyprint?
  1. /* What happens with $apply */   
  2. angular.module('myApp',[]).controller('MessageController'function($scope) {  
  3.       
  4.       $scope.getMessage = function() {  
  5.         setTimeout(function() {  
  6.           $scope.$apply(function() {  
  7.             //wrapped this within $apply  
  8.             $scope.message = 'Fetched after 3 seconds';   
  9.             console.log('message:' + $scope.message);  
  10.           });  
  11.         }, 2000);  
  12.       }  
  13.         
  14.       $scope.getMessage();  
  15.       
  16.     });  

 

如果你运行了上面的例子,你会看到view在两秒钟之后也会更新。唯一的变化是我们的代码现在被wrapped到了$scope.$apply()中,它会自动触发$rootScope.$digest(),从而让watchers被触发用以更新view

 

Note:顺便提一下,你应该使用$timeout service来代替setTimeout(),因为前者会帮你调用$apply(),让你不需要手动地调用它。

 

而且,注意在以上的代码中你也可以在修改了model之后手动调用没有参数的$apply(),就像下面这样:

 

[javascript] view plaincopyprint?
  1. $scope.getMessage = function() {  
  2.   setTimeout(function() {  
  3.     $scope.message = 'Fetched after two seconds';  
  4.     console.log('message:' + $scope.message);  
  5.     $scope.$apply(); //this triggers a $digest  
  6.   }, 2000);  
  7. };  

 

以上的代码使用了$apply()的第二种形式,也就是没有参数的形式。需要记住的是你总是应该使用接受一个function作为参数的$apply()方法。这是因为当你传入一个function$apply()中的时候,这个function会被包装到一个trycatch块中,所以一旦有异常发生,该异常会被$exceptionHandler service处理。

 

$digest循环会运行多少次?

当一个$digest循环运行时,watchers会被执行来检查scope中的models是否发生了变化。如果发生了变化,那么相应的listener函数就会被执行。这涉及到一个重要的问题。如果listener函数本身会修改一个scope model呢?AngularJS会怎么处理这种情况?

 

答案是$digest循环不会只运行一次。在当前的一次循环结束后,它会再执行一次循环用来检查是否有models发生了变化。这就是脏检查(Dirty Checking),它用来处理在listener函数被执行时可能引起的model变化。因此,$digest循环会持续运行直到model不再发生变化,或者$digest循环的次数达到了10次。因此,尽可能地不要在listener函数中修改model

 

Note: $digest循环最少也会运行两次,即使在listener函数中并没有改变任何model。正如上面讨论的那样,它会多运行一次来确保models没有变化。

 

结语

我希望这篇文章解释清楚了$apply$digest。需要记住的最重要的是AngularJS是否能检测到你对于model的修改。如果它不能检测到,那么你就需要手动地调用$apply()


补充:

$apply伪代码:


function $apply(expr) {
  try {
    return$eval(expr);
  } catch(e) {
    $exceptionHandler(e);
  } finally {
    $root.$digest();
  }
}

  它会捕获所有的异常并且不会再抛出来,最后都会调用$digest()方法。

  $apply()方法可以在angular框架之外执行angular JS的表达式,例如:DOM事件、setTimeout、XHR或其他第三方的库.