fix($timeout/$interval): if invokeApply is false, do not use evalAsync

$evalAsync triggers a digest, and is unsuitable when it is expected that a digest should not occur.

BREAKING CHANGE

Previously, even if invokeApply was set to false, a $rootScope digest would occur during promise
resolution. This is no longer the case, as promises returned from $timeout and $interval will no
longer trigger $evalAsync (which in turn causes a $digest) if `invokeApply` is false.

Workarounds include manually triggering $scope.$apply(), or returning $q.defer().promise from a
promise callback, and resolving or rejecting it when appropriate.

    var interval = $interval(function() {
      if (someRequirementFulfilled) {
        $interval.cancel(interval);
        $scope.$apply();
      }
    }, 100, 0, false);

or:

    var interval = $interval(function (idx) {
      // make the magic happen
    }, 1000, 10, false);
    interval.then(function(idx) {
      var deferred = $q.defer();
      // do the asynchronous magic --- $evalAsync will cause a digest and cause
      // bindings to update.
      return deferred.promise;
    });

Closes #7999
Closes #7103
This commit is contained in:
Caitlin Potter
2014-06-26 11:38:04 -04:00
parent b28b5caab1
commit 19b6b3433a
6 changed files with 51 additions and 9 deletions

View File

@@ -73,6 +73,7 @@
$ParseProvider,
$RootScopeProvider,
$QProvider,
$$QProvider,
$$SanitizeUriProvider,
$SceProvider,
$SceDelegateProvider,
@@ -222,6 +223,7 @@ function publishExternalAPI(angular){
$parse: $ParseProvider,
$rootScope: $RootScopeProvider,
$q: $QProvider,
$$q: $$QProvider,
$sce: $SceProvider,
$sceDelegate: $SceDelegateProvider,
$sniffer: $SnifferProvider,

View File

@@ -2,8 +2,8 @@
function $IntervalProvider() {
this.$get = ['$rootScope', '$window', '$q',
function($rootScope, $window, $q) {
this.$get = ['$rootScope', '$window', '$q', '$$q',
function($rootScope, $window, $q, $$q) {
var intervals = {};
@@ -133,10 +133,10 @@ function $IntervalProvider() {
function interval(fn, delay, count, invokeApply) {
var setInterval = $window.setInterval,
clearInterval = $window.clearInterval,
deferred = $q.defer(),
promise = deferred.promise,
iteration = 0,
skipApply = (isDefined(invokeApply) && !invokeApply);
skipApply = (isDefined(invokeApply) && !invokeApply),
deferred = (skipApply ? $$q : $q).defer(),
promise = deferred.promise;
count = isDefined(count) ? count : 0;

View File

@@ -173,6 +173,13 @@ function $QProvider() {
}];
}
function $$QProvider() {
this.$get = ['$browser', '$exceptionHandler', function($browser, $exceptionHandler) {
return qFactory(function(callback) {
$browser.defer(callback);
}, $exceptionHandler);
}];
}
/**
* Constructs a promise manager.

View File

@@ -2,8 +2,8 @@
function $TimeoutProvider() {
this.$get = ['$rootScope', '$browser', '$q', '$exceptionHandler',
function($rootScope, $browser, $q, $exceptionHandler) {
this.$get = ['$rootScope', '$browser', '$q', '$$q', '$exceptionHandler',
function($rootScope, $browser, $q, $$q, $exceptionHandler) {
var deferreds = {};
@@ -33,9 +33,9 @@ function $TimeoutProvider() {
*
*/
function timeout(fn, delay, invokeApply) {
var deferred = $q.defer(),
var skipApply = (isDefined(invokeApply) && !invokeApply),
deferred = (skipApply ? $$q : $q).defer(),
promise = deferred.promise,
skipApply = (isDefined(invokeApply) && !invokeApply),
timeoutId;
timeoutId = $browser.defer(function() {

View File

@@ -98,6 +98,23 @@ describe('$interval', function() {
}));
it('should NOT call $evalAsync or $digest if invokeApply is set to false',
inject(function($interval, $rootScope, $window, $timeout) {
var evalAsyncSpy = spyOn($rootScope, '$evalAsync').andCallThrough();
var digestSpy = spyOn($rootScope, '$digest').andCallThrough();
var notifySpy = jasmine.createSpy('notify');
$interval(notifySpy, 1000, 1, false);
$window.flush(2000);
$timeout.flush(); // flush $browser.defer() timeout
expect(notifySpy).toHaveBeenCalledOnce();
expect(evalAsyncSpy).not.toHaveBeenCalled();
expect(digestSpy).not.toHaveBeenCalled();
}));
it('should allow you to specify the delay time', inject(function($interval, $window) {
var counter = 0;
$interval(function() { counter++; }, 123);

View File

@@ -48,6 +48,22 @@ describe('$timeout', function() {
}));
it('should NOT call $evalAsync or $digest if invokeApply is set to false',
inject(function($timeout, $rootScope) {
var evalAsyncSpy = spyOn($rootScope, '$evalAsync').andCallThrough();
var digestSpy = spyOn($rootScope, '$digest').andCallThrough();
var fulfilledSpy = jasmine.createSpy('fulfilled');
$timeout(fulfilledSpy, 1000, false);
$timeout.flush();
expect(fulfilledSpy).toHaveBeenCalledOnce();
expect(evalAsyncSpy).not.toHaveBeenCalled();
expect(digestSpy).not.toHaveBeenCalled();
}));
it('should allow you to specify the delay time', inject(function($timeout, $browser) {
var defer = spyOn($browser, 'defer');
$timeout(noop, 123);