为什么Angular不会忽略后续的$ digest调用?

时间:2013-12-27 18:02:45

标签: javascript angularjs internals

以下必须是Angular.js开发人员在某些时候进行故障排除时最常见的错误。

  

错误:$申请已在进行中

有时候,根本不需要$scope.$apply来电,删除它会解决问题。其他时候,可能需要,因此开发人员采用以下模式。

if(!$scope.$$phase) {
    $scope.$apply();
}

我的问题是,为什么当调用$scope.$$phase循环或调用return时,Angular不会仅检查$digest$scope.$apply而不执行任何操作而不是抱怨和抛出一个如此难以追查的错误

Angular是否只能在触发$$phase的任何方法的入口点检查$digest本身,并使消费者不必经历麻烦?

除了不受欢迎的手表触发之外,是否有任何影响可能导致错误的$scope.$apply来电,$$phase的检查会阻止?

为什么不$$phase检查默认值,而不是奇怪?

背景

我正在将Angular从1.2.1升级到1.2.6,并且当页面加载时会抛出异常,因此我不知道它来自何处。因此,问题是,为什么我们甚至不得不追查这种模糊不清的错误而不知道我们的代码在哪里寻找它?

1 个答案:

答案 0 :(得分:8)

如果您收到此错误,那么您在wrong place中使用$scope.$apply()。我认为这个错误背后的原因如下:如果你在错误的地方调用$apply,你可能不完全理解它存在的原因,你可能错过了它实际上的地方应该被召唤。

如果允许这样的失误是默默地进行(例如,总是首先测试$scope.$$phase),这会鼓励人们使用草率,人们可能会在他们的“$apply覆盖范围内漏洞”。这种错误(其中$scope更改未在视图中反映)比这个错误消息更难追踪,您应该能够通过检查堆栈跟踪来调试(见下文)。

为什么$apply存在?

$apply的目的是在$scope和视图之间启用自动双向数据绑定,这是Angular的主要功能之一。此功能要求所有可能包含$scope修改的代码(因此,基本上每个用户代码)都在调用堆栈中的$apply调用之下运行。正确地做这件事实际上并不复杂,但我认为这不是documented

主要的'问题'是Javascript可以有任意数量的活动调用堆栈,并且Angular不会自动通知新的。每当触发异步回调时,就会创建一个新的调用堆栈,例如,来自点击,超时,文件访问,网络响应等。想要修改这样的回调函数中的$scope是很常见的。如果相应的事件是由Angular本身提供的,那么一切都会正常工作。但有时您会想要订阅“外部活动”。 Google地图事件,例如:

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/

双击此示例的地图不会触发对视图的更新,因为坐标会从“$scope.coordinates较少”的调用堆栈添加到$apply。换句话说,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-aware,或者'内部事件',如果你愿意的话(顺便说一下,我完全按照这些条款来完成)。这使您的代码更具可读性,让您在日常编程中忘记$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控制台。在这种情况下,我们可以得出结论:“哦,这是'内部事件'订阅者,我可以删除我的手动$apply电话”。但在其他情况下,您可能会发现堆叠中较低的呼叫也未正确定位。

这正是我认为错误有用的原因。理想情况下,当您忽略完全致电$apply时,您只会收到错误消息。但是如果Angular可以做到这一点,就不需要首先手动调用该函数。