原文:http://www.sitepoint.com/understanding-angulars-apply-digest/
$apply()
和 $digest()
是 AngularJS 中的两个核心概念,有时它们令人困惑。要理解 AngularJS 是如何工作的,需要完全理解 $apply()
和 $digest()
是如何工作的。本文的目的是解释 $apply()
和 $digest()
实际上是什么,以及如何在你的日常的 AngularJS 编程中应用它们。
探索 $apply()
和 $digest()
AngularJS提供了一种令人难以置信的功能,叫做双向数据绑定(Two-way Data Binding),它极大地简化了我们的代码编写方式。数据绑定意味着当你在视图(View)中更改某个内容时,scope
模型将会自动更新。类似地,当 scope
模型发生变化时,视图(View)将以新的值进行更新。那么AngularJS是怎么做到的呢?当你编写一个表达式({{aModel}})时,Angular将会在幕后在 scope
模型上设置一个 watcher
,当模型发生变化时,它会更新视图。这个 watcher
和你在AngularJS中设置的任何一个 watcher
相同:
$scope.$watch('aModel', function(newValue, oldValue) {
//update the DOM with newValue
});
传递给$watch()的第二个参数称为监听器函数,当aModel的值发生变化时,它就被调用。我们很容易理解,当aModel的值发生改变,这个监听器就会被调用来更新HTML中的表达式。但是,还有一个很重要的问题!Angular是怎么判断什么时候调用这个监听器函数的呢?换句话说,AngularJS是如何知道aModel值是何时发生改变的,从而它可以调用相应的监听器函数呢?它是否定期运行一个函数来检查 scope
模型的值是否已经改变了?好,这就是 $digest
循环的步骤。
在 $digest
周期中,watcher
会被触发。当一个 watcher
被触发时,AngularJS将评估 scope
模型,如果它发生了变化,则调用相应的监听器函数。那么,我们的下一个问题是,这个 $digest
循环是何时开始的。
调用 $scope.$digest()
后,$digest
循环就开始了。假设你通过ng-click指令在处理程序函数中更改了一个scope模型。在这种情况下,AngularJS会通过调用 $digest()
自动触发一个 $digest
循环。当 $digest
循环开始的时候,它就会触发每一个 watcher
。这些 watcher
会检查scope模型的当前值是否与上次计算得到的值不同。如果不同,则执行相应的监听器函数。因此,如果在视图中有任何表达式,它们将被更新。除了ng-click之外,还有其他一些内置的指令/服务可以让你更改模型(例如ng-model、$timeout
等),并自动触发一个 $digest
循环。
到目前为止还不错!但是,这里有一个小问题。在上面的例子中,Angular并不直接调用 $digest()
。相反,它调用 $scope.$apply()
,而 $scope.$apply()
又会调用 $rootScope.$digest()
。因此,一个 $digest
循环开始于 $rootScope
,随后会访问所有的child scopes,并在此过程中调用child scopes中的watchers。
现在,假设你将一个ng-click指令附加到一个按钮,并将一个函数名传递给它。当单击按钮时,AngularJS将函数调用包装在 $scope.$apply()
中。因此,你的函数照常执行,更改模型(如果有的话),并开始一个 $digest
循环来确保你的更改反映在视图中。
注意:$scope.$apply()
自动调用 $rootScope.$digest()
。$apply()
函数有两种形式。第一种接受一个函数作为参数,执行这个函数,并触发一个 $digest
循环。第二种则不需要任何参数,在调用时只触发一个 $digest
循环。接下来,我们将会看到为什么前一个是首选方法。
什么时候需要调用$apply()?
如果AngularJS总是用 $apply()
包装我们的代码并触发一个 $digest
循环,那么什么时候才需要我们手动调用 $apply()
呢?实际上,AngularJS对此有着非常明确的要求,就是它只考虑那些在AngularJS上下文内完成的模型更改(例如,更改模型的代码被包装在 $apply()
)中。Angular的内置指令就是这样做的,所以你所做的任何模型改变都能反映在视图中。但是,如果你更改了Angular上下文之外的任何模型,那么你需要手动调用 $apply()
来通知更改Angular。这就像是告诉Angular,你正在改变一些模型,它应该触发watchers,这样你的变化才能得到正确的传播。
例如,如果你使用JavaScript的 setTimeout()
函数来更新一个scope模型,那么Angular就无法知道你可能会改变什么。在这种情况下,手动调用 $apply()
就是你的责任,它会触发一个 $digest
循环。类似地,如果你有一个指令,它设置一个DOM事件监听器,并在处理程序函数内部修改了一些模型,那么你需要调用 $apply()
以确保更改生效。
让我们来看一个例子。假设你有一个页面,当页面加载后,你希望在延迟2秒后显示一条消息。你的实现可能类似于下面所示的JavaScript和HTML。
<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);
}
$scope.getMessage();
});
通过运行示例,你将在控制台中看到延迟函数在两个秒间隔之后运行,并更新scope模型消息。不过,视图并没有更新。你可能已经猜到了,原因是我们忘记了手动调用 $apply()
。因此,我们需要更新 getMessage()
函数,如下所示:
这个 $digest
循环运行了多少次?
当一个 $digest
循环运行时,watcher
会被执行用来查看scope模型是否已经改变。如果有,则调用相应的监听器函数。这就引出了一个重要的问题。如果监听器函数本身更改了scope模型,该怎么办?AngularJS是如何解释这种变化的呢?
答案是,$digest
循环不会只运行一次。在当前循环结束时,它将重新开始检查是否有任何模型发生了变化。这基本上是一个脏检查(Dirty Checking),并且是为了处理任何可能由监听器函数导致的模型更改。因此,$digest
循环将会一直循环,直到不再有模型更改,或者它达到了10这个最大循环数。因此为了保证功能总是好的,请尽量减少在监听器函数内对模型进行更改。
注意:即使监听器函数不改变任何模型,$digest
也会至少运行两次。正如上面所讨论的那样,它会多运行一次,来确保模型是稳定的,并且没有发生变化。
总结
我希望这篇文章解释清楚了什么是 $apply
,什么是 $digest
。要记住最重要的一点就是,Angular是否能探测到你对于模型的修改。如果不能,那么你必须手动调用 $apply()
。