ChatGPT解决这个技术问题 Extra ChatGPT

为什么使用 if(!$scope.$$phase) $scope.$apply() 反模式?

有时我需要在我的代码中使用 $scope.$apply ,有时它会抛出“digest already in progress”错误。所以我开始想办法解决这个问题,发现了这个问题:AngularJS : Prevent error $digest already in progress when calling $scope.$apply()。然而,在评论(和角度维基)中,您可以阅读:

不要这样做 if (!$scope.$$phase) $scope.$apply(),这意味着你的 $scope.$apply() 在调用堆栈中不够高。

所以现在我有两个问题:

为什么这是一个反模式?我怎样才能安全地使用 $scope.$apply?

另一个防止“正在消化”错误的“解决方案”似乎是使用 $timeout:

$timeout(function() {
  //...
});

这是要走的路吗?它更安全吗?所以这是真正的问题:我如何才能完全消除“正在消化”错误的可能性?

PS:我只在非同步的非 angularjs 回调中使用 $scope.$apply。 (据我所知,如果您希望应用更改,则必须使用 $scope.$apply 的情况)

根据我的经验,您应该始终知道,您是从 Angular 内部还是从 Angular 外部操纵 scope。所以根据这个你总是知道,如果你需要调用 scope.$apply 与否。如果您对角度/非角度 scope 操作使用相同的代码,那么您做错了,它应该始终分开......所以基本上如果您遇到需要检查 {4 },您的代码没有以正确的方式设计,并且总有一种方法可以“以正确的方式”进行
我只在非角度回调中使用它(!)这就是我感到困惑的原因
如果它是非角度的,它不会抛出 digest already in progress 错误
我也那么认为。问题是:它并不总是抛出错误。只是偶尔一次。我的怀疑是应用偶然与另一个摘要发生冲突。那可能吗?
如果回调严格是非角度的,我认为这是不可能的

m
mzedeler

经过更多的挖掘,我能够解决使用 $scope.$apply 是否总是安全的问题。简短的回答是肯定的。

长答案:

由于您的浏览器执行 Javascript 的方式,两个摘要调用不可能偶然发生冲突。

我们编写的 JavaScript 代码并不是一次性运行,而是轮流执行。这些回合中的每一个都从头到尾不间断地运行,当一个回合正在运行时,我们的浏览器中不会发生任何其他事情。 (来自 http://jimhoskins.com/2012/12/17/angularjs-and-apply.html)

因此,错误“digest already in progress”只能在一种情况下发生:当在另一个 $apply 中发出 $apply 时,例如:

$scope.apply(function() {
  // some code...
  $scope.apply(function() { ... });
});

这种情况不会出现如果我们在纯非 angularjs 回调中使用 $scope.apply,例如 setTimeout 的回调。因此,以下代码是 100% 防弹的,并且 没有 需要执行 if (!$scope.$$phase) $scope.$apply()

setTimeout(function () {
    $scope.$apply(function () {
        $scope.message = "Timeout called!";
    });
}, 2000);

即使这个是安全的:

$scope.$apply(function () {
    setTimeout(function () {
        $scope.$apply(function () {
            $scope.message = "Timeout called!";
        });
    }, 2000);
});

什么是 安全的(因为 $timeout - 像所有 angularjs 助手一样 - 已经为您调用 $scope.$apply):

$timeout(function () {
    $scope.$apply(function () {
        $scope.message = "Timeout called!";
    });
}, 2000);

这也解释了为什么使用 if (!$scope.$$phase) $scope.$apply() 是一种反模式。如果您以正确的方式使用 $scope.$apply,则根本不需要它:例如,在像 setTimeout 这样的纯 js 回调中。

阅读 http://jimhoskins.com/2012/12/17/angularjs-and-apply.html 以获得更详细的说明。


我有一个例子,我用 $document.bind('keydown', function(e) { $rootScope.$apply(function() { // a passed through function from the controller gets executed here }); }); 创建服务我真的不知道为什么我必须在这里制作 $apply,因为我正在使用 $document.bind..
因为 $document 只是“浏览器的 window.document 对象的 jQuery 或 jqLite 包装器”。并实现如下:function $DocumentProvider(){ this.$get = ['$window', function(window){ return jqLite(window.document); }]; }那里没有申请。
$timeout 在语义上意味着延迟后运行代码。这可能是一个功能安全的事情,但它是一个黑客。当您无法知道 $digest 循环是否正在进行或您已经在 $apply 中时,应该有一种安全的方式来使用 $apply。
它不好的另一个原因:它使用不属于公共 api 的内部变量 ($$phase),它们可能会在更新版本的 angular 中更改,从而破坏您的代码。不过,您的同步事件触发问题很有趣
较新的方法是使用 $scope.$evalAsync() 如果可能的话,它会在当前摘要周期或下一个周期中安全执行。请参阅bennadel.com/blog/…
F
FlavorScape

现在它绝对是一种反模式。即使您检查 $$phase,我也看到摘要爆炸了。您只是不应该访问由 $$ 前缀表示的内部 API。

你应该使用

 $scope.$evalAsync();

因为这是 Angular ^1.4 中的首选方法,并且专门作为应用程序层的 API 公开。


Necro,但请注意提供有关“即使您检查 $$ 阶段,我也看到摘要爆炸了”的来源或进一步解释。
L
Lalit Sachdeva

在任何情况下,当您的摘要正在进行中并且您推送另一个服务进行摘要时,它只会给出一个错误,即摘要已经在进行中。所以要解决这个问题,你有两个选择。您可以检查正在进行的任何其他摘要,例如轮询。

第一

if ($scope.$root.$$phase != '$apply' && $scope.$root.$$phase != '$digest') {
    $scope.$apply();
}

如果上述条件为真,那么您可以应用您的 $scope.$apply 否则

第二种解决方案是使用 $timeout

$timeout(function() {
  //...
})

它不会让其他摘要开始,直到 $timeout 完成它的执行。


被否决;这个问题特别问为什么不做你在这里描述的事情,而不是用另一种方式来破解它。有关何时使用 $scope.$apply();,请参阅 @gaul 的出色回答。
虽然没有回答问题:$timeout 是关键!它有效,后来我发现它也被推荐。
我知道在 2 年后对此添加评论已经很晚了,但是在使用 $timeout 过多时要小心,因为如果您没有良好的应用程序结构,这可能会花费您太多的性能
T
Tamas Rev

scope.$apply 触发 $digest 循环,这是 2 路数据绑定的基础

$digest 循环检查附加到 $scope 的对象,即模型(准确地说是 $watch),以评估它们的值是否已更改,如果检测到更改,则需要采取必要的步骤来更新视图。

现在,当您使用 $scope.$apply 时,您会遇到“已经在进行中”的错误所以很明显 $digest 正在运行,但是什么触发了它?

答案-->每个 $http 调用、所有 ng-click、重复、显示、隐藏等都会触发一个 $digest 循环,并且它在每个 $SCOPE 中运行最糟糕的部分。

即说您的页面有 4 个控制器或指令 A、B、C、D

如果每个属性中有 4 个 $scope 属性,那么您的页面上总共有 16 个 $scope 属性。

如果您在控制器 D 中触发 $scope.$apply,则 $digest 循环将检查所有 16 个值!!!加上所有 $rootScope 属性。

Answer-->$scope.$digest 会在子级和相同范围内触发 $digest,因此它只会检查 4 个属性。因此,如果您确定 D 中的更改不会影响 A、B、C,则使用 $scope.$digest 而不是 $scope.$apply

因此,即使用户未触发任何事件,仅 ng-click 或 ng-show/hide 就可能触发超过 100 多个属性的 $digest 循环!


是的,不幸的是,我意识到这个项目很晚。如果我从一开始就知道这一点,就不会使用 Angular。所有标准指令都会触发 $scope.$apply,它会依次调用 $rootScope.$digest,它对所有范围执行脏检查。如果你问我,糟糕的设计决定。我应该控制应该对哪些范围进行脏检查,因为我知道数据如何链接到这些范围!
S
Sunil Garg

使用$timeout,这是推荐的方式。

我的场景是我需要根据从 WebSocket 收到的数据更改页面上的项目。而且由于它在 Angular 之外,没有 $timeout,唯一的模型将被更改,但视图不会被更改。因为 Angular 不知道那条数据已被更改。 $timeout 基本上是在告诉 Angular 在下一轮 $digest 中做出改变。

我也尝试了以下方法并且它有效。对我来说不同的是 $timeout 更清晰。

setTimeout(function(){
    $scope.$apply(function(){
        // changes
    });
},0)

将套接字代码包装在 $apply 中要干净得多(很像 Angular 的 AJAX 代码,即 $http)。否则,您必须在所有地方重复此代码。
这绝对不推荐。此外,如果 $scope 具有 $$phase,您有时会在执行此操作时遇到错误。相反,您应该使用 $scope.$evalAsync();
如果您使用 setTimeout$timeout,则不需要 $scope.$apply
b
bora89

我找到了非常酷的解决方案:

.factory('safeApply', [function($rootScope) {
    return function($scope, fn) {
        var phase = $scope.$root.$$phase;
        if (phase == '$apply' || phase == '$digest') {
            if (fn) {
                $scope.$eval(fn);
            }
        } else {
            if (fn) {
                $scope.$apply(fn);
            } else {
                $scope.$apply();
            }
        }
    }
}])

在你需要的地方注入:

.controller('MyCtrl', ['$scope', 'safeApply',
    function($scope, safeApply) {
        safeApply($scope); // no function passed in
        safeApply($scope, function() { // passing a function in
        });
    }
])