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

AngularJS : Prevent error $digest already in progress when calling $scope.$apply()

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

我知道的唯一方法是从我的控制器和指令的范围内调用$apply()。问题在于,它不断向控制台抛出一个错误,该错误为:

Error: $digest already in progress

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


从最近一次关于这个问题的讨论中可以看出:出于未来的考虑,你不应该使用$$phase

当按"正确"方式时,答案当前为

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

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

下面是一个来自服务内部的示例。(为了简洁起见,服务的其余部分——设置变量、注入$timeout等——都被省略了。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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的delay参数是可选的,如果不设置delay,则默认为0($timeout调用$browser.defer,如果不设置delay,则默认为0)

有点不直观,但这是从写角度的家伙的答案,所以这对我来说已经足够好了!


Don't use this pattern - This will end up causing more errors than it solves. Even though you think it fixed something, it didn't.

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

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

如果$digest$apply正在进行,$scope.$$phase将返回"$digest""$apply"。我认为这些国家之间的区别是,$digest将处理当前作用域及其子作用域的监视,$apply将处理所有作用域的监视者。

就@dnc253而言,如果您经常打电话给$digest$apply,可能是做错了。我通常会发现,当我需要更新作用域的状态时,我需要进行消化,因为DOM事件在角度范围之外触发。例如,当Twitter引导模式变为隐藏时。有时,当$digest正在进行时,DOM事件会激发,有时不会。所以我才用这张支票。

如果有人知道的话,我想知道更好的方法。

从评论:用"@"和"道多"

Angular.js反模式

  • Don't do if (!$scope.$$phase) $scope.$apply(), it means your $scope.$apply() isn't high enough in the call stack.

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

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

    或者如果您有下划线:

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

    我们尝试了几个解决方法,但我们不喜欢将$rootscope注入到我们的所有控制器、指令甚至一些工厂中。所以,到目前为止,$timeout和uu.defer一直是我们的最爱。这些方法成功地告诉Angular等待下一个动画循环,这将保证当前的作用域。$apply结束。


    我在这里包含的意见和答案,但可以导致混乱。简单的使用是最好的$timeoutNOR合适的解决方案。因此,请务必阅读,如果你在表演或者scalability忧思。

    你应该知道的事情

    • $$phase是私人的框架和有好的那个原因。

    • 摘要$timeout(callback)想等到当前周期(房间)是完成,然后执行的回调,然后运行一个完整的$apply尽头。

    • $timeout(callback, delay, false)(想做同样的一个可选的回调函数执行前的延迟),但不到三$apply火灾性能参数)是特殊的,如果你没有在你的角模型($范围)。

    • $scope.$apply(callback)invokes,在其他的事情,$rootScope.$digest,这意味着它会redigest根的应用范围的信息和所有的儿童,即使你在考虑的范围之内。

    • $scope.$digest()想简单的视图模型来同步其父母,但不消化它的范围,这可以节省很多的表演,当工作在你的HTML的一个孤立的部分隔离的范围(从一个指令…)。摘要不花一美元:你的代码执行的回调,然后消化。

    • 介绍$scope.$evalAsync(callback)已与angularjs 1.2,最可能会解决你的烦恼。请参阅第靠近负载更多的了解它。

    • 如果你把你的$digest already in progress error架构,然后是错误的:你不需要redigest或你的范围,你应该不收费(湖是在下面)。

    如何组织你的代码

    当你把那个错误,你试图消化你的范围,它已经在进步:因为你不知道你在那州范围的点,你需要在批处理与它的消解。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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);
    });

    如果你知道你所做的工作和在小指令集的一部分应用在大角,你可以选择将美元美元而不是救过消化性能。

    angularjs 1.2更新。

    一个新的,强大的方法已被添加到任何范围:$evalAsync美元。基本上,它会执行它的回调在消化周期电流IF是新出现的一个,否则在启动消化周期要执行回调。

    这是不是很安静$scope.$digestas a如果你真的知道,你只需要在你的HTML的孤立的部分访问(因为新$apply将是触发如果不进行),但这是最好的解决方案是一个函数执行的,当你知道你将无法或不执行,如果synchronously例如,后A:有时fetching资源潜在的缓存,这将需要在异步调用服务器资源,否则将fetched synchronously局部。

    在案件的所有论文和别人的,你有一个!$scope.$$phase,请务必使用$scope.$evalAsync( callback )


    使用简便的小助手方法保持此过程干燥:

    1
    2
    3
    function safeApply(scope, fn) {
        (scope.$$phase || scope.$root.$$phase) ? fn() : scope.$apply(fn);
    }


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

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

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

    防止此错误的方法是确保调用$apply的代码异步运行。这可以通过在对$timeout的调用中运行代码来实现,延迟设置为0(这是默认设置)。但是,在$timeout中调用代码会消除调用$apply的必要性,因为$timeout会自行触发另一个$digest周期,而该周期又会进行所有必要的更新等。

    解决方案

    简而言之,不要这样做:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    ... your controller code...

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

    ... more of your controller code...

    这样做:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    ... your controller code...

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

    ... more of your controller code...

    只有当您知道运行的代码总是在角度代码之外运行时,才调用$apply(例如,您对$apply的调用将发生在由角度代码之外的代码调用的回调内)。

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


    我对第三方脚本也有同样的问题,比如codemirror和krpano,即使使用这里提到的安全应用方法也没有为我解决错误。

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

    因此,类似于:

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

    如果在代码中使用

    this

    可能是因为它在工厂指令的控制器中,或者只是需要某种绑定,所以您可以执行如下操作:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    .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();

      }]
    )

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


    在最短$apply平安形式是:

    1
    $timeout(angular.noop)


    您也可以使用evalasync。它将在摘要完成后运行!

    1
    2
    3
    scope.evalAsync(function(scope){
        //use the scope...
    });

    首先,不要这样修

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

    它没有意义,因为$phase只是$digest循环的一个布尔标志,所以有时您的$apply()不会运行。记住这是个坏习惯。

    相反,使用EDOCX1[0]

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

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

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

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

    试试这个:

    1
    2
    if(! $rootScope.$root.$$phase) {
    ...


    你应该使用美元或美元的evalasync超时根据上下文。

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

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


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

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

    通过创建自定义事件,您对代码的效率也会更高,因为您只触发订阅了所述事件的侦听器,而不会像调用scope时那样触发绑定到作用域的所有监视。$apply。

    1
    2
    3
    4
    5
    6
    $scope.$on('customEventName', function (optionalCustomEventArguments) {
       //TODO: Respond to event
    });


    $scope.$broadcast('customEventName', optionalCustomEventArguments);


    试用使用

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

    而不是

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

    $applyasync计划稍后调用$apply。这可用于将需要在同一摘要中计算的多个表达式排队。

    注意:在$digest中,只有当当前作用域是$rootscope时,$applyasync()才会刷新。这意味着,如果在子作用域上调用$digest,它将不会隐式刷新$applyasync()队列。

    Exmaple:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
      $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()。

  • 安格洛斯JS文档

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

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

    用途:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    //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);

    而不是使用$scope.$$phase || $scope.$apply();


    在我知道$digest函数将运行的地方,我可以通过调用$eval而不是$apply来解决这个问题。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function $apply(expr) {
      try {
        return $eval(expr);
      } catch (e) {
        $exceptionHandler(e);
      } finally {
        $root.$digest();
      }
    }

    在我的例子中,一个ng-click在一个范围内改变一个变量,而一个$watch在这个变量上改变其他必须是$applied的变量。最后一步会导致错误"Digest already in progress"。

    通过在watch表达式中用$eval替换$apply,范围变量将按预期进行更新。

    因此,如果Digest由于角度上的其他变化而以任何方式运行,那么您只需要执行$eval


    由于角文件称检查$$phase为反模式,我试图让$timeout_.defer工作。

    超时和延迟方法在DOM中创建一个未分析的{{myVar}}内容的闪存,就像一个fout。对我来说,这是不可接受的。教条主义地告诉我什么是黑客行为,没有合适的选择,这让我无从谈起。

    每次都有效的唯一方法是:

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

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

    "Do the digest unless one is already happening"

    在咖啡里,它甚至更漂亮:

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

    这有什么问题?有没有其他办法不犯规?$safeapply看起来不错,但也使用了$$phase检查方法。


    服务:这是我的工具

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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);
            }
        };
    });

    这是一个例子:"它是使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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']);
    };

    我已经使用这个方法似乎工作非常精细。这只是在等待的时间周期已经完成,然后apply()触发器。简单的调用函数apply()从任何你想去的地方。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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);
      }
    }

    但这类似于上面的答案之前,我将忠实地……服务地址:A

    1
    2
    3
    4
    5
    6
        //sometimes you need to refresh scope, use this to prevent conflict
        this.applyAsNeeded = function (scope) {
            if (!scope.$$phase) {
                scope.$apply();
            }
        };

    你可以使用

    $timeout

    预防的错误。

    1
    2
    3
    4
    5
     $timeout(function () {
                            var scope = angular.element($("#myController")).scope();
                            scope.myMethod();
                            scope.$scope();
                        },1);

    发现这coderwall.com:http:///P/ngisma沃克在弥敦道(近底部页)和一rootscope鱼鳞美元safeapply创建功能,代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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;
          }
        ]);
      }
    ]);


    这将解决你的问题:

    1
    2
    3
    if(!$scope.$$phase) {
      //TODO
    }