【问题标题】:Why doesn't Angular ignore subsequent $digest invocations?为什么 Angular 不忽略后续的 $digest 调用?
【发布时间】:2014-01-15 07:57:11
【问题描述】:

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

错误:$apply 已经在进行中

有时,$scope.$apply 调用毕竟不是必需的,删除它可以解决问题。在其他时候,可能需要它,因此开发人员采用以下模式。

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

我的问题是,当调用 $digest 循环或调用 $scope.$apply 时,为什么 Angular 不只检查 $scope.$$phasereturn 而不做任何事情,而不是抱怨和 抛出一个非常难以追踪的错误

Angular 不能只在触发$digest 的任何方法的入口点检查$$phase 本身并避免消费者遇到麻烦吗?

留下错位的$scope.$apply 电话是否有任何影响,除了不需要的手表触发,$$phase 的检查会阻止吗?

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

背景

我正在将 Angular 从 1.2.1 升级到 1.2.6 并且在页面加载时抛出异常,所以我不知道它来自哪里。那么问题来了,为什么我们甚至必须追踪这种晦涩难懂的错误,却不知道在我们的代码中的何处寻找它?

【问题讨论】:

标签: javascript angularjs internals


【解决方案1】:

如果您遇到此错误,则说明您在 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 可以做到这一点,那么一开始就不需要手动调用该函数。

【讨论】:

  • 这是一个非常彻底有用的答案(鉴于它可以帮助解决现有问题),但我认为它不能回答实际问题。提问者可能会也可能不会理解背景和推理,但有一个更简单的问题:为什么Scope#$apply 不包含在$$phase 支票中?在if(!$scope.$$phase) { ... } 中包装该方法的主体将消除许多错误并且几乎没有开销。它可能对用户没有教育,但它肯定会像一行代码一样接近无害。
  • @eyelidlessness:感谢您的反馈!实际上,答案就在这句开头的句子中:“如果允许这样的失误默默地过去……”。但我同意它几乎没有应有的可见性。我现在把它说得更清楚了。 --- 我想,在我写这个庞然大物的时候,我有点忘记了问题的主要部分。 ;-)
  • 感谢您的编辑。现在我必须明确不同意你的推理。程序异常的目的不是鼓励最佳实践,也不是阻止有问题的实践(这实际上是警告的作用),并且当然不要通过创建错误条件来使用逆向心理学来鼓励用户在其他地方做更多相同的事情。程序异常的目的是提醒程序注意不一致的状态并允许它恢复或失败。 Angular 错误不会这样做,它会惩罚用户,最坏的情况是一个 noop。
  • @eyelidlessness:我认为$apply 应该在“外部”回调中使用,以通知 Angular 新的调用堆栈。在其他任何地方使用它不仅是不好的做法。这是错误,应该被视为错误。不幸的是,Angular(或任何 JS 库)的检测能力有限。但是,堆栈上的两个$apply 调用表明其中至少有一个放置不正确,因此这将导致错误。但是... Javascript 没有正确的 asserterror 语句。一个例外是 Angular 能做的最好的事情。至少它给了我们一个堆栈跟踪。
  • 我们应该规定“应该被视为错误”,对于异常来说,是有意义的。当一个程序将被导致进入一个不一致或不可靠的状态时,它应该抛出一个异常。 Angular 的行为并非如此。它抛出一个异常,忽略$apply 调用,然后继续其当前的$digest 阶段。这个(丑陋但简单的)示例演示了以下行为:jsfiddle.net/PT6W8 - 观察实际程序的行为与预期一致。删除对$apply 的不必要调用不会改变行为:jsfiddle.net/7uk2b(续)
猜你喜欢
  • 2013-04-24
  • 2014-09-15
  • 2016-04-04
  • 1970-01-01
  • 2014-01-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-01-29
相关资源
最近更新 更多