858

我发现自从以 Angular 构建应用程序以来,我越来越需要手动将页面更新到我的范围。

我知道这样做的唯一方法是$apply()从我的控制器和指令的范围内调用。这样做的问题是它不断向控制台抛出错误,内容如下:

错误:$digest 已经在进行中

有谁知道如何避免此错误或以不同的方式实现相同的目标?

4

28 回答 28

675

从最近与 Angular 人员就这个主题进行的讨论:出于面向未来的原因,您不应该使用$$phase

当按下“正确”的方法时,答案是目前

$timeout(function() {
  // anything you want can go here and will safely be run on the next digest.
})

我最近在编写 Angular 服务来包装 facebook、google 和 twitter API 时遇到了这个问题,这些 API 在不同程度上都有回调。

这是服务中的一个示例。(为简洁起见,服务的其余部分——设置变量、注入 $timeout 等——已被取消。)

window.gapi.client.load('oauth2', 'v2', function() {
    var request = window.gapi.client.oauth2.userinfo.get();
    request.execute(function(response) {
        // This happens outside of angular land, so wrap it in a timeout 
        // with an implied apply and blammo, we're in action.
        $timeout(function() {
            if(typeof(response['error']) !== 'undefined'){
                // If the google api sent us an error, reject the promise.
                deferred.reject(response);
            }else{
                // Resolve the promise with the whole response if ok.
                deferred.resolve(response);
            }
        });
    });
});

请注意, $timeout 的延迟参数是可选的,如果未设置则默认为 0(如果未设置延迟,则$timeout调用$browser.defer默认为 0 )

有点不直观,但这是编写 Angular 的人的答案,所以对我来说已经足够了!

于 2013-09-25T04:06:19.560 回答
671

不要使用这种模式——这最终会导致比它解决的更多的错误。即使您认为它修复了某些问题,但它没有。

您可以$digest通过检查来检查 a 是否已经在进行中$scope.$$phase

if(!$scope.$$phase) {
  //$digest or $apply
}

$scope.$$phase将返回"$digest"或者"$apply"如果 a $digestor$apply正在进行中。我相信这些状态之间的区别在于$digest将处理当前范围及其子范围的手表,$apply并将处理所有范围的观察者。

就@dnc253 而言,如果您发现自己打电话$digest$apply经常打电话,您可能做错了。当我需要更新作用域的状态时,我通常发现我需要消化,因为 DOM 事件在 Angular 范围之外触发​​。例如,当 twitter 引导模式被隐藏时。有时 DOM 事件会在 a$digest进行时触发,有时则不会。这就是我使用此检查的原因。

如果有人知道,我很想知道一种更好的方法。


来自评论:@anddoutoi

angular.js 反模式

  1. 不要这样做if (!$scope.$$phase) $scope.$apply(),这意味着你$scope.$apply()在调用堆栈中不够高。
于 2012-10-12T12:28:54.100 回答
332

摘要循环是一个同步调用。在完成之前,它不会控制浏览器的事件循环。有几种方法可以解决这个问题。处理这个问题的最简单方法是使用内置的 $timeout,第二种方法是如果你使用下划线或 lodash(你应该使用),调用以下命令:

$timeout(function(){
    //any code in here will automatically have an apply run afterwards
});

或者如果你有 lodash:

_.defer(function(){$scope.$apply();});

我们尝试了几种解决方法,我们讨厌将 $rootScope 注入我们所有的控制器、指令甚至一些工厂。所以,到目前为止,$timeout 和 _.defer 是我们最喜欢的。这些方法成功地告诉 angular 等到下一个动画循环,这将保证当前的 scope.$apply 结束。

于 2013-07-30T22:51:27.353 回答
270

这里的许多答案都包含很好的建议,但也可能导致混淆。简单地使用$timeout不是最好的,也不是正确的解决方案。此外,如果您担心性能或可扩展性,请务必阅读。

你应该知道的事情

  • $$phase是框架私有的,这是有充分理由的。

  • $timeout(callback)将等到当前的摘要循环(如果有)完成,然后执行回调,然后在最后运行一个完整的$apply.

  • $timeout(callback, delay, false)会做同样的事情(在执行回调之前有一个可选的延迟),但不会触发$apply(第三个参数),如果你没有修改你的 Angular 模型($scope),它会节省性能。

  • $scope.$apply(callback)调用,除其他外$rootScope.$digest,这意味着它将重新消化应用程序的根范围及其所有子级,即使您在一个孤立的范围内。

  • $scope.$digest()只会将其模型同步到视图,但不会消化其父范围,这可以在使用隔离范围(主要来自指令)处理 HTML 的隔离部分时节省大量性能。$digest 不接受回调:您执行代码,然后进行摘要。

  • $scope.$evalAsync(callback)已经在 angularjs 1.2 中引入,可能会解决你的大部分麻烦。请参阅最后一段以了解更多信息。

  • 如果你得到$digest already in progress error,那么你的架构是错误的:要么你不需要重新消化你的范围,要么你不应该负责(见下文)。

如何构建你的代码

当您收到该错误时,您正在尝试消化您的示波器,而它已经在进行中:因为您不知道此时您的示波器的状态,您不负责处理它的消化。

function editModel() {
  $scope.someVar = someVal;
  /* Do not apply your scope here since we don't know if that
     function is called synchronously from Angular or from an
     asynchronous code */
}

// Processed by Angular, for instance called by a ng-click directive
$scope.applyModelSynchronously = function() {
  // No need to digest
  editModel();
}

// Any kind of asynchronous code, for instance a server request
callServer(function() {
  /* That code is not watched nor digested by Angular, thus we
     can safely $apply it */
  $scope.$apply(editModel);
});

而且,如果您知道自己在做什么,并且在大型 Angular 应用程序的一部分中处理一个孤立的小指令,那么您可能更喜欢 $digest 而不是 $apply 以节省性能。

自 Angularjs 1.2 起更新

任何 $scope: 中都添加了一个新的、强大的方法$evalAsync。基本上,如果正在发生,它将在当前摘要周期内执行其回调,否则新的摘要周期将开始执行回调。

$scope.$digest如果您真的知道只需要同步 HTML 的一个隔离部分(因为$apply如果没有正在进行的操作将触发一个新的部分),那仍然没有那么好,但是当您执行一个函数时,这是最好的解决方案您无法知道它是否会同步执行,例如在获取可能缓存的资源之后:有时这需要对服务器进行异步调用,否则资源将在本地同步获取。

在这些情况下以及您拥有的所有其他情况下,请!$scope.$$phase务必使用$scope.$evalAsync( callback )

于 2014-04-16T06:59:44.770 回答
89

方便的小助手方法来保持这个过程干燥:

function safeApply(scope, fn) {
    (scope.$$phase || scope.$root.$$phase) ? fn() : scope.$apply(fn);
}
于 2013-06-14T18:14:15.677 回答
33

我在使用第三方脚本(例如 CodeMirror 和 Krpano)时遇到了同样的问题,甚至使用此处提到的 safeApply 方法也没有为我解决错误。

但是解决它的方法是使用 $timeout 服务(不要忘记先注入它)。

因此,类似:

$timeout(function() {
  // run my code safely here
})

如果在你的代码中你正在使用

也许是因为它在工厂指令的控制器内,或者只需要某种绑定,那么你会做类似的事情:

.factory('myClass', [
  '$timeout',
  function($timeout) {

    var myClass = function() {};

    myClass.prototype.surprise = function() {
      // Do something suprising! :D
    };

    myClass.prototype.beAmazing = function() {
      // Here 'this' referes to the current instance of myClass

      $timeout(angular.bind(this, function() {
          // Run my code safely here and this is not undefined but
          // the same as outside of this anonymous function
          this.surprise();
       }));
    }

    return new myClass();

  }]
)
于 2013-09-03T00:15:39.200 回答
32

请参阅http://docs.angularjs.org/error/$rootScope:inprog

当您调用$apply有时在 Angular 代码之外异步运行(应该使用 $apply 时),有时在 Angular 代码内部同步运行(这会导致$digest already in progress错误)时,就会出现问题。

例如,当您有一个从服务器异步获取项目并缓存它们的库时,可能会发生这种情况。第一次请求一个项目时,它将被异步检索,以免阻塞代码执行。但是,第二次,该项目已经在缓存中,因此可以同步检索。

防止此错误的方法是确保调用的代码$apply异步运行。这可以通过在调用中运行代码来完成,$timeout延迟设置为0(这是默认设置)。然而,在里面调用你的代码$timeout就不需要调用了$apply,因为 $timeout 会自己触发另一个$digest循环,这反过来会做所有必要的更新等等。

解决方案

简而言之,而不是这样做:

... your controller code...

$http.get('some/url', function(data){
    $scope.$apply(function(){
        $scope.mydate = data.mydata;
    });
});

... more of your controller code...

做这个:

... your controller code...

$http.get('some/url', function(data){
    $timeout(function(){
        $scope.mydate = data.mydata;
    });
});

... more of your controller code...

$apply当您知道运行它的代码将始终在 Angular 代码之外运行时才调用(例如,您对 $apply 的调用将发生在由您的 Angular 代码之外的代码调用的回调中)。

$timeout除非有人意识到使用over有一些不利的影响$apply,否则我不明白为什么你不能总是使用$timeout(零延迟)而不是$apply,因为它会做大致相同的事情。

于 2014-01-21T21:33:02.040 回答
28

当您收到此错误时,基本上意味着它已经在更新您的视图。你真的不需要$apply()在你的控制器中调用。如果您的视图没有按预期更新,然后在调用 后出现此错误$apply(),则很可能意味着您没有正确更新模型。如果您发布一些细节,我们可以找出核心问题。

于 2012-10-04T14:41:41.483 回答
14

安全的最短形式$apply是:

$timeout(angular.noop)
于 2014-11-13T13:00:45.663 回答
11

您也可以使用 evalAsync。它将在摘要完成后的某个时间运行!

scope.evalAsync(function(scope){
    //use the scope...
});
于 2013-09-19T01:48:42.720 回答
10

首先,不要这样修复

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

这是没有意义的,因为 $phase 只是 $digest 循环的布尔标志,因此您的 $apply() 有时不会运行。请记住,这是一种不好的做法。

相反,使用$timeout

    $timeout(function(){ 
  // Any code in here will automatically have an $scope.apply() run afterwards 
$scope.myvar = newValue; 
  // And it just works! 
});

如果你使用下划线或lodash,你可以使用defer():

_.defer(function(){ 
  $scope.$apply(); 
});
于 2018-03-05T08:25:32.023 回答
9

如果您使用这种方式,有时您仍然会遇到错误(https://stackoverflow.com/a/12859093/801426)。

试试这个:

if(! $rootScope.$root.$$phase) {
...
于 2013-04-30T13:24:50.043 回答
5

您应该根据上下文使用 $evalAsync 或 $timeout。

这是一个很好的解释的链接:

http://www.bennadel.com/blog/2605-scope-evalasync-vs-timeout-in-angularjs.htm

于 2014-07-22T12:05:55.940 回答
5

尝试使用

$scope.applyAsync(function() {
    // your code
});

代替

if(!$scope.$$phase) {
  //$digest or $apply
}

$applyAsync 将 $apply 的调用安排在以后发生。这可用于将需要在同一摘要中评估的多个表达式排队。

注意:在 $digest 中,$applyAsync() 仅在当前范围是 $rootScope 时才会刷新。这意味着如果您在子作用域上调用 $digest,它不会隐式刷新 $applyAsync() 队列。

示例:

  $scope.$applyAsync(function () {
                if (!authService.authenticated) {
                    return;
                }

                if (vm.file !== null) {
                    loadService.setState(SignWizardStates.SIGN);
                } else {
                    loadService.setState(SignWizardStates.UPLOAD_FILE);
                }
            });

参考:

1. AngularJS 1.3 中的 Scope.$applyAsync() 与 Scope.$evalAsync()

  1. AngularJs 文档
于 2018-03-12T16:22:25.687 回答
4

我建议您使用自定义事件而不是触发摘要循环。

我发现广播自定义事件并为此事件注册侦听器是触发您希望发生的动作的一个很好的解决方案,无论您是否处于摘要周期中。

通过创建自定义事件,您还可以更高效地使用代码,因为您只触发订阅所述事件的侦听器,而不像调用 scope.$apply 那样触发绑定到范围的所有监视。

$scope.$on('customEventName', function (optionalCustomEventArguments) {
   //TODO: Respond to event
});


$scope.$broadcast('customEventName', optionalCustomEventArguments);
于 2014-01-02T15:26:46.030 回答
3

yearofmoo 在为我们创建可重用的 $safeApply 函数方面做得很好:

https://github.com/yearofmoo/AngularJS-Scope.SafeApply

用法 :

//use by itself
$scope.$safeApply();

//tell it which scope to update
$scope.$safeApply($scope);
$scope.$safeApply($anotherScope);

//pass in an update function that gets called when the digest is going on...
$scope.$safeApply(function() {

});

//pass in both a scope and a function
$scope.$safeApply($anotherScope,function() {

});

//call it on the rootScope
$rootScope.$safeApply();
$rootScope.$safeApply($rootScope);
$rootScope.$safeApply($scope);
$rootScope.$safeApply($scope, fn);
$rootScope.$safeApply(fn);
于 2013-11-21T13:33:42.697 回答
2

我已经能够通过调用$eval而不是在我知道该函数将运行$apply的地方来解决这个问题。$digest

根据文档$apply基本上是这样做的:

function $apply(expr) {
  try {
    return $eval(expr);
  } catch (e) {
    $exceptionHandler(e);
  } finally {
    $root.$digest();
  }
}

在我的情况下, ang-click更改了范围内的变量,并且该变量上的 $watch 更改了其他必须为$applied. 最后一步会导致错误“正在消化”。

通过在监视表达式中替换$apply$eval范围变量会按预期更新。

因此,如果由于 Angular 中的其他一些变化而仍然要运行摘要,那么$eval需要做的就是 'ing。

于 2013-09-01T21:20:02.743 回答
2

$scope.$$phase || $scope.$apply();改为使用

于 2015-05-21T11:27:43.330 回答
1

了解到 Angular 文档调用检查$$phase模式$timeout,我尝试开始_.defer工作。

timeout 和 deferred 方法{{myVar}}FOUT一样在 dom 中创建一串未解析的内容。对我来说这是不可接受的。它让我没有太多可以教条地告诉我某些东西是黑客,并且没有合适的替代方案。

唯一每次都有效的是:

if(scope.$$phase !== '$digest'){ scope.$digest() }.

我不明白这种方法的危险性,也不明白为什么评论和角度团队中的人将其描述为黑客行为。该命令似乎精确且易于阅读:

“做摘要,除非已经发生了”

在 CoffeeScript 中它更漂亮:

scope.$digest() unless scope.$$phase is '$digest'

这有什么问题?是否有不会创建 FOUT 的替代方案?$safeApply看起来不错,但也使用了$$phase检查方法。

于 2013-12-31T19:29:23.060 回答
1

这是我的实用程序服务:

angular.module('myApp', []).service('Utils', function Utils($timeout) {
    var Super = this;

    this.doWhenReady = function(scope, callback, args) {
        if(!scope.$$phase) {
            if (args instanceof Array)
                callback.apply(scope, Array.prototype.slice.call(args))
            else
                callback();
        }
        else {
            $timeout(function() {
                Super.doWhenReady(scope, callback, args);
            }, 250);
        }
    };
});

这是它的用法示例:

angular.module('myApp').controller('MyCtrl', function ($scope, Utils) {
    $scope.foo = function() {
        // some code here . . .
    };

    Utils.doWhenReady($scope, $scope.foo);

    $scope.fooWithParams = function(p1, p2) {
        // some code here . . .
    };

    Utils.doWhenReady($scope, $scope.fooWithParams, ['value1', 'value2']);
};
于 2015-08-24T05:59:15.053 回答
1

我一直在使用这种方法,它似乎工作得很好。这只是等待循环完成然后触发apply()。只需从您想要的任何地方调用该函数apply(<your scope>)

function apply(scope) {
  if (!scope.$$phase && !scope.$root.$$phase) {
    scope.$apply();
    console.log("Scope Apply Done !!");
  } 
  else {
    console.log("Scheduling Apply after 200ms digest cycle already in progress");
    setTimeout(function() {
        apply(scope)
    }, 200);
  }
}
于 2016-05-10T14:23:03.373 回答
1

当我禁用 debugger 时,错误不再发生。就我而言,这是因为调试器停止了代码执行。

于 2019-09-02T18:50:58.033 回答
0

类似于上面的答案,但这对我来说非常有效......在服务中添加:

    //sometimes you need to refresh scope, use this to prevent conflict
    this.applyAsNeeded = function (scope) {
        if (!scope.$$phase) {
            scope.$apply();
        }
    };
于 2016-01-12T20:39:08.413 回答
0

您可以使用$timeout来防止错误。

$timeout(function () {
    var scope = angular.element($("#myController")).scope();
    scope.myMethod(); 
    scope.$scope();
}, 1);
于 2017-09-15T10:54:26.230 回答
0

问题基本上来了,我们要求有角度来运行摘要循环,即使它正在进行中,这会产生角度理解的问题。控制台中的后果异常。
1. 在 $timeout 函数内部调用 scope.$apply() 没有任何意义,因为它在内部也是如此。
2. 代码与 vanilla JavaScript 函数一起使用,因为它的本机不是角度定义的,即 setTimeout
3. 为此,您可以使用

if(!scope.$$phase){
scope.$evalAsync(function(){

}); }

于 2019-09-22T07:05:16.837 回答
0
        let $timeoutPromise = null;
        $timeout.cancel($timeoutPromise);
        $timeoutPromise = $timeout(() => {
            $scope.$digest();
        }, 0, false);

这是避免此错误并避免 $apply的好解决方案

如果基于外部事件调用,您可以将其与 debounce(0) 结合使用。以上是我们正在使用的“去抖动”,以及完整的代码示例

.factory('debounce', [
    '$timeout',
    function ($timeout) {

        return function (func, wait, apply) {
            // apply default is true for $timeout
            if (apply !== false) {
                apply = true;
            }

            var promise;
            return function () {
                var cntx = this,
                    args = arguments;
                $timeout.cancel(promise);
                promise = $timeout(function () {
                    return func.apply(cntx, args);
                }, wait, apply);
                return promise;
            };
        };
    }
])

以及代码本身来监听一些事件并仅在您需要的 $scope 上调用 $digest

        let $timeoutPromise = null;
        let $update = debounce(function () {
            $timeout.cancel($timeoutPromise);
            $timeoutPromise = $timeout(() => {
                $scope.$digest();
            }, 0, false);
        }, 0, false);

        let $unwatchModelChanges = $scope.$root.$on('updatePropertiesInspector', function () {
            $update();
        });


        $scope.$on('$destroy', () => {
            $timeout.cancel($update);
            $timeout.cancel($timeoutPromise);
            $unwatchModelChanges();
        });
于 2020-04-10T01:39:01.367 回答
-3

发现这个:https : //coderwall.com/p/ngisma 其中 Nathan Walker(靠近页面底部)建议在 $rootScope 中使用装饰器来创建 func 'safeApply',代码:

yourAwesomeModule.config([
  '$provide', function($provide) {
    return $provide.decorator('$rootScope', [
      '$delegate', function($delegate) {
        $delegate.safeApply = function(fn) {
          var phase = $delegate.$$phase;
          if (phase === "$apply" || phase === "$digest") {
            if (fn && typeof fn === 'function') {
              fn();
            }
          } else {
            $delegate.$apply(fn);
          }
        };
        return $delegate;
      }
    ]);
  }
]);
于 2014-04-11T20:00:09.527 回答
-7

这将解决您的问题:

if(!$scope.$$phase) {
  //TODO
}
于 2015-02-11T19:10:56.153 回答