如果您遇到此错误,则说明您在 wrong place 中使用了 $scope.$apply()。我认为这个错误背后的原因如下:如果你在错误的地方调用$apply,你可能没有完全理解它存在的原因,你可能错过了它实际上应该 被调用。
如果允许这样的失误无声无息地过去(例如,始终首先测试$scope.$$phase),则会鼓励草率使用,人们可能会在他们的“$apply 覆盖范围”中找到漏洞。这种错误($scope 更改未反映在视图中)比此错误消息更难追踪,您应该能够通过检查堆栈跟踪来调试它(见下文)。
为什么$apply 存在?
$apply 的目的是启用 $scope 和视图之间的自动双向数据绑定,这是 Angular 的主要功能之一。此功能要求所有可能包含$scope 修改的代码(因此,基本上每一段用户代码)都在调用堆栈中的$apply 调用下方运行。正确地做到这一点其实并没有那么复杂,但我认为它不是documented 很好。
主要的“问题”是 Javascript 可以有任意数量的活动调用堆栈,并且 Angular 不会自动通知新的调用堆栈。每当触发异步回调时,都会创建一个新的调用堆栈,例如点击、超时、文件访问、网络响应等。在这样的回调函数中修改$scope 是很常见的。如果相应的事件是由 Angular 本身提供的,那么一切都会正常工作。但有时您会想要订阅“外部事件”。谷歌地图事件,例如:
function Controller($scope) {
$scope.coordinates = [];
//...
var map = new google.maps.Map(mapElement, mapOptions);
google.maps.event.addDomListener(map, 'dblclick', function (mouseEvent) {
$scope.coordinates.push(mouseEvent.latLng);
});
}
http://jsfiddle.net/mhelvens/XLPY9/1/
双击此示例的地图不会触发视图更新,因为坐标是从“$applyless”调用堆栈添加到$scope.coordinates。换句话说,Angular 不知道 Google 地图事件。
如何以及在何处使用$apply?
我们可以使用$scope.$apply() 通知 Angular 事件:
function Controller($scope) {
//...
google.maps.event.addDomListener(map, 'dblclick', function (mouseEvent) {
$scope.$apply(function () {
$scope.coordinates.push(mouseEvent.latLng);
});
});
}
http://jsfiddle.net/mhelvens/XLPY9/2/
规则是在外部事件的每个回调函数中首先执行此操作。 “$apply already in progress”错误表明您没有遵守此规则。如果您必须经常处理 Google 地图事件,那么将这个样板代码包装在 服务 中是有意义的:
app.factory('onGoogleMapsEvent', function ($rootScope) {
return function (element, event, callback) {
google.maps.event.addDomListener(element, event, function (e) {
$rootScope.$apply(function () { callback(e); });
});
};
});
function Controller($scope, onGoogleMapsEvent) {
//...
onGoogleMapsEvent(map, 'dblclick', function (mouseEvent) {
$scope.coordinates.push(mouseEvent.latLng);
});
}
http://jsfiddle.net/mhelvens/XLPY9/3/
onGoogleMapsEvent 事件现在是 Angular 感知的,或者“内部事件”,如果你愿意的话(顺便说一下,我完全是在编造这些术语)。这使您的代码更具可读性,并让您在日常编程中忘记$apply;只需调用包装器而不是原始事件订阅者。
对于许多事件,Angular 已经为我们完成了这项工作。以$timeout 服务为例。
调试“$apply already in progress”错误
假设我使用了包装器,但是,尽管我心不在焉,我仍然手动调用$apply:
onGoogleMapsEvent(map, 'dblclick', function (mouseEvent) {
$scope.$apply(function () { // Error: $apply already in progress
$scope.coordinates.push(mouseEvent.latLng);
});
});
http://jsfiddle.net/mhelvens/XLPY9/4/
这个$apply 调用不符合我们的放置规则(onGoogleMapsEvent 事件毕竟不是外部事件;我们授予它们“内部”)。如果您双击地图,您会在日志中看到错误以及堆栈跟踪:
Error: [$rootScope:inprog] $apply already in progress
...
at Scope.$apply (.../angular.js:11675:11)
at http://fiddle.jshell.net/mhelvens/XLPY9/4/show/:50:16
...
at Scope.$apply (.../angular.js:11676:23)
at dl.ondblclick (http://fiddle.jshell.net/mhelvens/XLPY9/4/show/:33:24)
...
我只留下了相关的行:引用Scope.$apply 的行。当您收到此错误消息时,您应该总是在堆栈上找到两个Scope.$apply 调用(或Scope.$digest 调用,它们提供类似的功能;$digest 由$apply 调用)。
最上面的调用表明在哪里我们得到了错误。另一个指出为什么。继续并在打开 Javascript 控制台的情况下运行 Fiddle。在这种情况下,我们可以得出结论:“哦,这是一个‘内部事件’订阅者,我可以删除我的手册 $apply 呼叫”。但在其他情况下,您可能会发现堆栈下方的调用也未正确定位。
这正是我认为该错误有用的原因。理想情况下,当您完全忽略调用$apply 时,您只会收到错误消息。但如果 Angular 可以做到这一点,那么一开始就不需要手动调用该函数。