我发现自从以 Angular 构建应用程序以来,我越来越需要手动将页面更新到我的范围。
我知道这样做的唯一方法是从我的控制器和指令的范围内调用 $apply()
。这样做的问题是它不断向控制台抛出错误,内容如下:
错误:$digest 已经在进行中
有谁知道如何避免此错误或以不同的方式实现相同的目标?
$timeout()
ng-*
)。确保,如果您是从函数中调用它(通过 timeout/ajax/events 调用),那么它不是也最初在加载时运行。
根据最近与 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 其中 defaults to 0 if delay isn't set)
有点不直观,但这是编写 Angular 的人的答案,所以对我来说已经足够了!
不要使用这种模式——这最终会导致比它解决的更多的错误。即使您认为它修复了某些问题,但它没有。
您可以通过检查 $scope.$$phase
来检查 $digest
是否已经在进行中。
if(!$scope.$$phase) {
//$digest or $apply
}
如果 $digest
或 $apply
正在进行,$scope.$$phase
将返回 "$digest"
或 "$apply"
。我相信这些状态之间的区别在于 $digest
将处理当前范围及其子范围的监视程序,而 $apply
将处理所有范围的监视程序。
就@dnc253 而言,如果您发现自己经常调用$digest
或$apply
,那么您可能做错了。当我需要更新作用域的状态时,我通常发现我需要消化,因为 DOM 事件在 Angular 范围之外触发。例如,当 twitter 引导模式被隐藏时。有时 DOM 事件会在 $digest
正在进行时触发,有时则不会。这就是我使用此检查的原因。
如果有人知道,我很想知道一种更好的方法。
来自评论:@anddoutoi
不要这样做 if (!$scope.$$phase) $scope.$apply(),这意味着你的 $scope.$apply() 在调用堆栈中不够高。
if (!$scope.$$phase) $scope.$apply()
", github.com/angular/angular.js/wiki/Anti-Patterns
摘要循环是一个同步调用。在完成之前,它不会控制浏览器的事件循环。有几种方法可以解决这个问题。解决这个问题的最简单方法是使用内置的 $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 结束。
underscore.js
时才应该使用它。这个解决方案不值得为了使用它的 defer
函数而导入整个下划线库。我更喜欢 $timeout
解决方案,因为每个人都已经可以通过 Angular 访问 $timeout
,而不依赖于其他库。
这里的许多答案都包含很好的建议,但也可能导致混淆。简单地使用 $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 错误,那么你的架构是错误的:要么你不需要重新消化你的范围,要么你不应该负责(见下文)。
如何构建你的代码
当您收到该错误时,您正在尝试消化您的示波器,而它已经在进行中:因为您不知道此时您的示波器的状态,您不负责处理它的消化。
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
中都添加了一个新的、强大的方法。基本上,如果正在发生,它将在当前摘要周期内执行其回调,否则新的摘要周期将开始执行回调。
如果您真的知道只需要同步 HTML 的一个孤立部分(因为如果没有正在进行的操作将触发一个新的 $apply
),那仍然不如 $scope.$digest
,但这是最好的解决方案当你执行一个函数时,你不知道它是否会同步执行,例如在获取可能缓存的资源之后:有时这需要对服务器进行异步调用,否则资源将本地同步获取。
在这些情况下以及您拥有 !$scope.$$phase
的所有其他情况下,请务必使用 $scope.$evalAsync( callback )
$timeout
被批驳。你能给出更多避免 $timeout
的理由吗?
$scope.applyAsync()
怎么样 - 没有人在这个 OP 的任何答案中提到这个,但我在其他线程中看到过。当我将所有的 $scope.apply()
更改为 scope.applyAsync()
时,我所有的 $digest 循环错误都消失了……不知道我是否给自己带来了更多问题,但到目前为止还没有问题。
方便的小助手方法来保持这个过程干燥:
function safeApply(scope, fn) {
(scope.$$phase || scope.$root.$$phase) ? fn() : scope.$apply(fn);
}
scope.$apply(fn);
应该是 scope.$apply(fn());
因为 fn() 将执行函数而不是 fn。请帮我解决我错的地方
我在使用第三方脚本(例如 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();
}]
)
请参阅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...
仅当您知道运行它的代码将始终在 Angular 代码之外运行时才调用 $apply
(例如,您对 $apply 的调用将发生在由您的 Angular 代码之外的代码调用的回调中)。
除非有人意识到使用 $timeout
而不是 $apply
的某些不利影响,否则我不明白为什么您不能总是使用 $timeout
(零延迟)而不是 $apply
,因为它会做大约一样。
$apply
但仍然收到错误的情况。
$apply
是同步的(执行它的回调,然后执行 $apply 之后的代码),而 $timeout
不是:执行超时之后的当前代码,然后一个新堆栈以其回调开始,就像你正在使用 setTimeout
。如果您更新两次相同的模型,这可能会导致图形故障:$timeout
将等待视图刷新,然后再次更新它。
当您收到此错误时,基本上意味着它已经在更新您的视图。您确实不需要在控制器中调用 $apply()
。如果您的视图没有按预期更新,然后在调用 $apply()
后收到此错误,则很可能意味着您没有正确更新模型。如果您发布一些细节,我们可以找出核心问题。
you're not updating the the model correctly
到底是什么意思? $scope.err_message = 'err message';
更新不正确?
$apply()
的时间是在 Angular“外部”更新模型时(例如,来自 jQuery 插件)。很容易陷入视图不正确的陷阱,因此您到处扔一堆 $apply()
,然后最终导致 OP 中出现错误。当我说 you're not updating the the model correctly
时,我的意思是所有业务逻辑都没有正确填充范围内的任何内容,这导致视图看起来不像预期的那样。
安全 $apply
的最短形式是:
$timeout(angular.noop)
您也可以使用 evalAsync。它将在摘要完成后的某个时间运行!
scope.evalAsync(function(scope){
//use the scope...
});
首先,不要这样修复
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();
});
如果您使用这种方式(https://stackoverflow.com/a/12859093/801426),有时您仍然会收到错误。
尝试这个:
if(! $rootScope.$root.$$phase) {
...
$rootScope
和 anyScope.$root
是同一个人。 $rootScope.$root
是多余的。
您应该根据上下文使用 $evalAsync 或 $timeout。
这是一个很好的解释的链接:
http://www.bennadel.com/blog/2605-scope-evalasync-vs-timeout-in-angularjs.htm
尝试使用
$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.Scope.$applyAsync() vs. Scope.$evalAsync() in AngularJS 1.3
AngularJs 文档
我建议您使用自定义事件而不是触发摘要循环。
我发现广播自定义事件并为此事件注册侦听器是触发您希望发生的动作的一个很好的解决方案,无论您是否处于摘要周期中。
通过创建自定义事件,您还可以更高效地使用代码,因为您只触发订阅所述事件的侦听器,而不像调用 scope.$apply 那样触发绑定到范围的所有监视。
$scope.$on('customEventName', function (optionalCustomEventArguments) {
//TODO: Respond to event
});
$scope.$broadcast('customEventName', optionalCustomEventArguments);
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);
我已经能够通过在我知道 $digest
函数将运行的地方调用 $eval
而不是 $apply
来解决这个问题。
根据 docs,$apply
基本上是这样做的:
function $apply(expr) {
try {
return $eval(expr);
} catch (e) {
$exceptionHandler(e);
} finally {
$root.$digest();
}
}
在我的例子中,ng-click
会更改范围内的变量,而该变量上的 $watch 会更改必须为 $applied
的其他变量。最后一步会导致错误“正在消化”。
通过在监视表达式中将 $apply
替换为 $eval
,范围变量会按预期更新。
因此,看来,如果由于 Angular 中的一些其他更改而无论如何要运行摘要,那么您需要做的就是$eval
。
改用 $scope.$$phase || $scope.$apply();
了解 Angular 文档调用检查 $$phase
和 anti-pattern,我尝试让 $timeout
和 _.defer
工作。
timeout 和 deferred 方法会在 dom 中创建一个未解析的 {{myVar}}
内容,如 FOUT。对我来说这是不可接受的。它让我没有太多可以教条地告诉我某些东西是黑客,并且没有合适的替代方案。
唯一每次都有效的是:
if(scope.$$phase !== '$digest'){ scope.$digest() }
。
我不明白这种方法的危险性,也不明白为什么评论和角度团队中的人将其描述为黑客行为。该命令似乎精确且易于阅读:
“做摘要,除非已经发生了”
在 CoffeeScript 中它更漂亮:
scope.$digest() unless scope.$$phase is '$digest'
这有什么问题?是否有不会创建 FOUT 的替代方案? $safeApply 看起来不错,但也使用 $$phase
检查方法。
这是我的实用程序服务:
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']);
};
我一直在使用这种方法,它似乎工作得很好。这只是等待循环完成的时间,然后触发 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);
}
}
当我禁用 debugger 时,错误不再发生。就我而言,这是因为调试器停止了代码执行。
类似于上面的答案,但这对我来说非常有效......在服务中添加:
//sometimes you need to refresh scope, use this to prevent conflict
this.applyAsNeeded = function (scope) {
if (!scope.$$phase) {
scope.$apply();
}
};
问题基本上来了,我们要求有角度来运行摘要循环,即使它正在进行中,这会产生角度理解的问题。控制台中的后果异常。 1. 在 $timeout 函数内部调用 scope.$apply() 没有任何意义,因为它在内部也是如此。 2. 代码与 vanilla JavaScript 函数一起使用,因为它的本机不是角度定义的,即 setTimeout 3. 为此,您可以使用 if(!scope.$$phase){ scope.$evalAsync(function(){ }); }
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();
});
您可以使用 $timeout
来防止错误。
$timeout(function () {
var scope = angular.element($("#myController")).scope();
scope.myMethod();
scope.$scope();
}, 1);
发现: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;
}
]);
}
]);
这将解决您的问题:
if(!$scope.$$phase) {
//TODO
}
不定期副业成功案例分享
$timeout
而不是原生setTimeout
,为什么不使用$window
而不是原生window
?$timeout
的重点是$timeout
确保正确更新角度范围。如果 $digest 不在进行中,它将导致新的 $digest 运行。cancel
它。来自 docs:“因此,承诺将通过拒绝来解决。”您无法解决已解决的承诺。您的取消不会导致任何错误,但也不会产生任何积极的影响。