原文地址:https://www.sitepoint.com/understanding-angulars-apply-digest/
原文作者:Sandeep Panda
$apply()和$digest()是AngularJs的两个核心概念,但同时也是容易令我们感到困惑。为了理解AngularJs的工作原理,我们需要深入理解$apply()和$digest()是如何运作的。这篇文章的主要目的就是解释$apply()和$digest()的概念,而这将在我们的日常AngularJs编程中是非常有用的。
$apply与$digest探险
AngularJS 提供了一个难以置信的、很棒的特性叫做——双向数据绑定,这很大程度上简化了我们的工作。数据绑定的意思是指,当你在页面上作了一些修改时(译者:如你通过输入框输入、下拉框选择等),scope中的一些数据会自动的修改。同样地,当scope中的值修改时,页面视图会自动更新为新的值。AngularJS是如何实现这些的呢?当你写了一个表达式({{aModel}})时,Angular会在幕后启动一个scope模型的观察者,当模型发生变化时依次更新视图。这个观察者跟你在AngularJS中启动的观察者是一样的:
$scope.$watch('aModel', function(newValue, oldValue) {
// 用newValue的值来更新页面DOM结构
});
传给$watch()方法的第二个参数被称为监听函数,它会在aModel发生修改时被调用。这一点我们比较好理解,当aModel的值变化时,监听函数被调用,这时修改HTML中的表达式({{aModel}})。但是,还有一个比较大的问题——AngularJS怎么知道什么时候去调用这个监听函数的呢?换句话说,AngularJs怎么知道aModel何时变化了,并去调用相应的监听函数?它有没有运行了一个方法周期性地去检查scope的值是否发生了变化?其实,这就是$digest循环所在。
观察者启动的就是$digest循环。当一个观察者被启动时,AngularJS会去评估scope模型,并在当它发生变化时调用相应的监听函数。所以,我们接下来的问题就是,$digest循环是合适启动的,如何启动的?
$digest循环的开始是来自于$scope.$digest()方法的调用。假设你通过ng-click指令在处理函数中修改了一个scope中的值,此时,AngularJS通过调用$digest()方法自动触发一个$digest循环。当$digest循环开始时,它会触发每一个观察者,这些观察者回去检查scope当前的值与最后一次计算的值是否不一样。如果是,其对应的监听函数就会执行。除了ng-click以外,还有几个其他的内置directive或service可以让你去修改scope中的model(例如:ng-model,$timeout等)并触发一个$digest循环。
到目前为止还挺好的(原文:So far, so good!)。但是,还有一个小的疑难杂症。在上述例子中,AngularJs并不直接地调用$digest()方法。而是调用$scope.$apply()方法,这会调用$rootScope.$digest()。这样的结果就是,一个$digest循环在$rootScope中开始,随后访问所有的子scope并调用其中的观察者(检查值是否变化,如果变化调用监听函数)。
此时,让我们假设你给一个按钮绑定了一个ng-click指令并传递了一个函数名给它。当这个按钮被点击时,AngularJS用$scope.$apply()包裹这个函数调用。这样,你的函数照常执行,修改model(如果有的话),同时一个$digest循环开启,确保你的改变能够被反映到视图。
注意:$scope.$apply()会自动地调用$rootScope.$digest()方法。$apply()函数有两种形式。第一种传递一个函数作为参数,评估这个函数,并触发一个$digest循环。第二种方式不传递任何参数,被调用时只是开始一个$digest循环。我们在后面很快会发现为什么前者是首选的方法。
该何时手动调用$apply()?
AngularJS通常把我们的代码包裹在$apply()方法里,并开启一个$digest循环,那我们什么时候需要手动去调用$apply()方法吗?事实上,AngularJS中这很清晰。它只负责那些在AngularJS的上下文环境里面发生的model变化(换言之,包裹在$apply()里面的model变化)。AngularJS的内置指令已经实现了这些,所以你做的任何model变化都将被反映到视图。然而,你在AngularJS上下文环境外修改model的话,你就需要通过手动调用$apply()方法来告知Angular这些model变化。就好像是告诉Angular,你在修改某些model,并且这应该要触发那些观察者,这样你的修改就可以正确的传播。
举个例子,如果你用JavaScript原生的setTimeout()函数来修改一个scope中的model,Angular没有办法知道你已经做了修改。在这里你就需要手动地调用能够触发一个$digest循环的$apply()方法。同样,如果你有一个自定义指令设置了一个DOM事件监听,并在处理函数中修改了一些model的值,你需要调用$apply()方法来确保这些修改能够起作用。
让我们来看一个例子。假设你有一个页面,你想在页面加载完成之后延迟两秒显示一个信息。你的实现代码就跟下面的JavaScript和HTML相似。 HTML:
<body ng-app="myApp">
<div ng-controller="MessageController">
Delayed Message: {{message}}
</div>
</body>
JavaScript:
/* 这是没有用$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();
});
结果:
(译者:console控制台有打印出信息,但页面上没有显示出信息)
通过运行这个例子,你会发现,延迟函数两秒执行,并修改scope中的模型message($scope.message),而视图却没有修改。这个原因你应该能猜到,那就是我们完了手动地调用$apply()方法。所以,我们需要向下面一样修改我们的getMessage()方法。
JavaScript:
/* 这是用$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();
});
结果:
(译者:console控制台有打印信息,视图也有显示信息)
如果你运行这个修改后的例子,你会发现视图在两秒后发生了修改。唯一的变化就是我们的代码被包裹在能够自动触发$rootScope.$digest()方法的$scope.$apply()方法里面。结果观察者照常被触发并且视图发生变化。
注意:顺便提一下,你应该尽可能用$timeout服务,这是自动带有$apply()的倒计时方法(setTimeout()),这样你就不必手动去调用$apply()方法。
还有,在上述的代码中你可照常完成model修改,然后在最后加一个$apply()(没有参数的形式)调用。看一下下面的代码片段:
$scope.getMessage = function() {
setTimeout(function() {
$scope.message = 'Fetched after two seconds';
console.log('message:' + $scope.message);
$scope.$apply(); // 这会触发一个$digest循环
}, 2000);
};
上面的代码用了没有参数形式的$apply()方法,并起了作用。记住,你应该尽量用接收一个函数作为的参数的$apply()方法形式。这是因为,当你传递一个函数给$apply(),这个函数的调用时包裹在一个try...catch块中的,有任何异常出现都会被传递给$exceptionHandler服务。
$digest循环运行几次?
当一个$digest循环运行时,观察者会被执行去检查scope的model是否已经发生了变化。一旦发生变化,相应的监听器函数就会被调用。这带来了一个重要的问题,如果一个监听器函数自身修改了scope的model值呢?AngularJS将会对这个变回作何反应?
答案是,$digest循环并不仅仅运行一次。在当前循环的最后,它会从头再一次启动来检查是否有任何的model被改变了。这是基本的脏值监测,这么做是为了对任何发生在监听器函数处理中的model变化起作用。这样,$digest循环会一直循环,直到没有model发生变化,或者达到了最大循环次数10。最好保持幂等(stay idempotent),尽量减少在监听器函数中进行model变化。
注意:$digest会至少循环两次,即时你的监听器函数没有修改任何model。如上所述,它会多运行一次来确认model是稳定的、没发生变化的。
结论
我希望这篇文章能够解释清楚关于$apply和$digest的全部。最重要的是要清楚,Angular能否侦查到你做的修改。如果不能,你必须要手动调用$apply()方法。