AngularJS:在调用$ scope时防止错误$ digest正在进行中.$ apply()

Lig*_*lb1 832 angularjs angularjs-scope angularjs-digest

我发现自从以角度构建应用程序后,我需要手动将页面更新到我的范围.

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

错误:$ digest已在进行中

有谁知道如何避免这个错误或以不同的方式实现相同的事情?

bet*_*ust 658

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

当按下"正确"方式时,答案是当前的

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

我最近在编写角度服务时遇到了这个问题,以包装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);
            }
        });
    });
});
Run Code Online (Sandbox Code Playgroud)

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

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

  • 我在指令中遇到过很多次.正在为redactor编写一个,结果证明它完美无缺.我和布拉德·格林(Brad Green)见过面,他说Angular 2.0将是巨大的,没有消化周期,使用JS的原生观察能力,并且使用polyfill用于缺乏这种功能的浏览器.那时我们不再需要这样做了.:) (5认同)
  • 如果使用`$ timeout`而不是native`setTimeout`,为什么不使用`$ window`而不是本机`window`? (3认同)
  • @LeeGee:在这种情况下使用$ timeout的要点是$ timeout确保正确地更新了角度范围。如果没有$ digest,则将导致运行新的$ digest。 (2认同)
  • @webicy没什么。当传递给$ timeout的函数主体运行时,promise已经解决!绝对没有理由“取消”它。来自[docs](https://docs.angularjs.org/api/ng/service/$timeout#cancel):“因此,诺言将被拒绝。” 您无法解决已解决的承诺。您的取消不会引起任何错误,但也不会做任何积极的事情。 (2认同)

Lee*_*Lee 656

不要使用这种模式 - 这最终会导致比它解决的更多错误.即使你认为它固定了一些东西,它也没有.

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

if(!$scope.$$phase) {
  //$digest or $apply
}
Run Code Online (Sandbox Code Playgroud)

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

对@ dnc253来说,如果你发现自己正在打电话$digest$apply经常打电话,你可能会做错了.我通常发现当需要更新范围的状态时,我需要消化,因为DOM事件在Angular的范围之外触发​​.例如,当twitter引导模式变为隐藏时.有时DOM事件在a $digest正在进行时触发,有时则不触发.这就是我使用这张支票的原因.

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


来自评论:@anddoutoi

angular.js反模式

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

  • 在我看来,像$ digest/$ apply应该默认执行此操作 (228认同)
  • "停止做`if(!$ scope.$$ phase)$ scope.$ apply()`",https://github.com/angular/angular.js/wiki/Anti-Patterns (106认同)
  • @anddoutoi:同意; 你的链接很清楚这不是解决方案; 但是,我不确定"你在调用堆栈中不够高"是什么意思.你知道这意味着什么吗? (33认同)
  • 请注意,在某些情况下,我必须检查当前范围和根范围.我一直在根阶段获得$$阶段的值,但不是我的范围.认为它与指令的孤立范围有关,但是...... (21认同)
  • @threed:看看aaronfrost的回答.正确的方法是使用延迟在下一个周期中触发摘要.否则事件将丢失,并且根本不更新范围. (13认同)
  • @anddoutoi你的链接的信息值是0.没有进一步的解释为什么以及程序员应该如何防止"反模式"它只是毫无价值. (7认同)
  • -1见[floribon的回答](http://stackoverflow.com/a/23102223/3116322).不仅仅是!$ scope.$$阶段不是正确的检查 - [$$变量是私有的](http://stackoverflow.com/a/19338518/3116322),但是实际上没有必要进行检查DOM事件在角度之外触发. (4认同)
  • 由于这个答案与@ betaorbust的回答中提到的最佳做法相反,并且由于这个答案的作者首先说他的提议不被遵循,所以应该删除这个答案. (4认同)
  • 我使用$ timeout(function(){....})或$ scope.$ evalAsync(function(){....}) (3认同)
  • 我会说它并不总是那么简单,因为它"在调用堆栈中不够高",例如你在第三方代码的回调中调用它**通常*在事件被触发后返回但有时会立即返回,因为问题(无法预见)然后这个问题难以解决. (3认同)
  • @theed,关于"调用堆栈中的不够高":我认为这意味着如果你不能告诉你是否需要$ apply,你可以将$ apply代码移动到调用函数的地方,它会更加明显.即,您有两种不同的路径可以访问相同的代码段,但只有其中一个需要$ apply调用.如果发生这种情况,只需使用$ apply调用该函数,而不是稍后尝试解决它!希望有所帮助. (2认同)
  • @Bogdan是的,这是一种常见的方法。不幸的是,这是一个令人作呕的解决方案,但它似乎通常是唯一有效的解决方案。那,或者让我觉得我真的不了解Angular。关于此问题,确实应该有一些更好的解释/材料,因为这对于使应用程序正常运行至关重要:( (2认同)
  • 建议将“永远不要这样做”更改为“绝对不要这样做,除非这实际上是唯一可行的方法”。我目前正在使用CKEditor,并且必须在作用域上调用$ apply才能应用编辑器的处理程序中的更改,并且我不确定当前是否正在进行摘要。因此,检查这正是将解决此问题的原因。 (2认同)

fro*_*sty 322

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

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

或者如果你有下划线:

_.defer(function(){$scope.$apply();});
Run Code Online (Sandbox Code Playgroud)

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

  • 只有在你已经使用`underscore.js`时才应该使用它.这个解决方案不值得导入整个下划线库只是为了使用它的`defer`函数.我更喜欢`$ timeout`解决方案,因为每个人都可以通过angular访问`$ timeout`,而不依赖于其他库. (9认同)
  • 真的......但如果你不使用下划线或lodash ......你需要重新评估你在做什么.这两个库改变了代码的外观. (9认同)
  • 这看起来很有趣,为什么这个有用? (4认同)
  • 这与使用$ timeout(...)相当吗?我在几种情况下使用$ timeout来推迟到下一个事件周期,它似乎工作正常 - 任何人都知道是否有理由不使用$ timeout? (2认同)
  • 我们将lodash作为Restangular的依赖(我们将尽快消除Restangular以支持ng-route).我认为这是一个很好的答案,但假设人们想要使用下划线/ lodash并不是很好.通过各种方式,这些库很好......如果你充分利用它们......这些天我使用ES5方法消除了98%的原因我_useused包括下划线. (2认同)
  • 你是对的@SgtPooki.我修改了答案,包括使用$ timeout的选项.$ timeout和_.defer将等到下一个动画循环,这将确保当前范围.$ apply已结束.谢谢你让我诚实,并让我在这里更新答案. (2认同)
  • 称之为老套,但使用名为$(timeout)的东西来实质上更新UI让我绝对*畏缩*.即使他们只是为它写了一个包装,比如"updateWhenPossible"或类似愚蠢的东西会让我冷静下来.我同意@SgtPooki - 这应该得到解决. (2认同)

flo*_*bon 265

这里的许多答案包含很好的建议,但也可能导致混淆.只需用$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);
});
Run Code Online (Sandbox Code Playgroud)

如果你知道你正在做什么,并且在一个大型Angular应用程序的一部分中处理一个孤立的小指令,你可能更喜欢$ digest而不是$ apply来保存性能.

自Angularjs 1.2以来的更新

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

$scope.$digest如果你真的知道你只需要同步一个孤立的HTML部分(因为$apply如果没有正在进行的话就会触发新的部分),那仍然不如a ,但这是执行函数时的最佳解决方案这你可以不知道它是否会同步或不执行获取潜在缓存的资源之后,例如:有时这需要一个异步调用到服务器,否则,资源将在本地获取同步.

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

  • `$ timeout`被批判传递.你能给出更多理由来避免"$ timeout"吗? (3认同)

lam*_*tor 88

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

function safeApply(scope, fn) {
    (scope.$$phase || scope.$root.$$phase) ? fn() : scope.$apply(fn);
}
Run Code Online (Sandbox Code Playgroud)

  • 您的safeApply帮助我了解了最重要的事情.感谢发布. (6认同)
  • 我即将做同样的事情,但不这样做意味着我们在fn()中做出的改变有可能不被$ digest看到?假设范围,延迟函数不是更好吗.$$阶段==='$ digest'? (4认同)

小智 33

我遇到了像CodeMirror这样的第三方脚本和Krpano同样的问题,甚至使用这里提到的safeApply方法也没有为我解决错误.

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

因此,像:

$timeout(function() {
  // run my code safely here
})
Run Code Online (Sandbox Code Playgroud)

如果在您的代码中使用

这个

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

.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();

  }]
)
Run Code Online (Sandbox Code Playgroud)


Tre*_*vor 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...
Run Code Online (Sandbox Code Playgroud)

做这个:

... your controller code...

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

... more of your controller code...
Run Code Online (Sandbox Code Playgroud)

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

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

  • 主要区别在于`$ apply`是同步的(执行回调,然后是$ apply之后的代码),而`$ timeout`则不执行:执行超时后的当前代码,然后新的堆栈以其回调开始,如如果你使用`setTimeout`.如果您更新两次相同的模型,这可能会导致图形故障:`$ timeout`将等待视图刷新,然后再次更新它. (5认同)

dnc*_*253 28

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

  • 你需要调用`$ apply()`的唯一一次是你更新角度的"外部"模型(例如从jQuery插件).很容易落入视图的陷阱,看起来不正确,所以你到处抛出一堆`$ apply(),然后以OP中看到的错误结束.当我说'你没有正确地更新模型'时,我只是说所有的业务逻辑都没有正确地填充可能在范围内的任何东西,这导致视图看起来不像预期的那样. (2认同)

War*_*ock 14

最短的安全形式$apply是:

$timeout(angular.noop)
Run Code Online (Sandbox Code Playgroud)


CMC*_*kai 11

您也可以使用evalAsync.摘要完成后它会运行一段时间!

scope.evalAsync(function(scope){
    //use the scope...
});
Run Code Online (Sandbox Code Playgroud)


Sag*_*r M 10

首先,不要这样修复它

if ( ! $scope.$$phase) { 
  $scope.$apply(); 
}
Run Code Online (Sandbox Code Playgroud)

它没有意义,因为$ 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! 
});
Run Code Online (Sandbox Code Playgroud)

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

_.defer(function(){ 
  $scope.$apply(); 
});
Run Code Online (Sandbox Code Playgroud)


bul*_*are 9

如果您使用这种方式,有时您仍会遇到错误(/sf/answers/900136541/).

试试这个:

if(! $rootScope.$root.$$phase) {
...
Run Code Online (Sandbox Code Playgroud)

  • 同时使用两个!$ scope.$$阶段和!$ scope.$ root.$$阶段(不是!$ rootScope.$ root.$$阶段)适合我.+1 (5认同)
  • `$ rootScope`和`anyScope.$ root`是同一个人.`$ rootScope.$ root`是多余的. (2认同)

小智 5

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

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

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


Edu*_*iek 5

尝试使用

$scope.applyAsync(function() {
    // your code
});
Run Code Online (Sandbox Code Playgroud)

代替

if(!$scope.$$phase) {
  //$digest or $apply
}
Run Code Online (Sandbox Code Playgroud)

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

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

例子:

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

                if (vm.file !== null) {
                    loadService.setState(SignWizardStates.SIGN);
                } else {
                    loadService.setState(SignWizardStates.UPLOAD_FILE);
                }
            });
Run Code Online (Sandbox Code Playgroud)

参考:

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

  1. AngularJs 文档