feat($http): implement mechanism for coalescing calls to $apply in $http

When multiple responses are received within a short window from each other, it can be wasteful to
perform full dirty-checking cycles for each individual response. In order to prevent this, it is
now possible to coalesce calls to $apply for responses which occur close together.

This behaviour is opt-in, and the default is disabled, in order to avoid breaking tests or
applications.

In order to activate coalesced apply in tests or in an application, simply perform the following
steps during configuration.

   angular.module('myFancyApp', []).
     config(function($httpProvider) {
       $httpProvider.useApplyAsync(true);
     });

OR:

   angular.mock.module(function($httpProvider) {
     $httpProvider.useApplyAsync(true);
   });

Closes #8736
Closes #7634
Closes #5297
This commit is contained in:
Caitlin Potter
2014-08-22 18:17:55 -04:00
parent e94d454b84
commit ea6fc6e69c
3 changed files with 121 additions and 8 deletions

View File

@@ -143,6 +143,34 @@ function $HttpProvider() {
xsrfHeaderName: 'X-XSRF-TOKEN'
};
var useApplyAsync = false;
/**
* @ngdoc method
* @name $httpProvider#useApplyAsync
* @description
*
* Configure $http service to combine processing of multiple http responses received at around
* the same time via {@link ng.$rootScope#applyAsync $rootScope.$applyAsync}. This can result in
* significant performance improvement for bigger applications that make many HTTP requests
* concurrently (common during application bootstrap).
*
* Defaults to false. If no value is specifed, returns the current configured value.
*
* @param {boolean=} value If true, when requests are loaded, they will schedule a deferred
* "apply" on the next tick, giving time for subsequent requests in a roughly ~10ms window
* to load and share the same digest cycle.
*
* @returns {boolean|Object} If a value is specified, returns the $httpProvider for chaining.
* otherwise, returns the current configured value.
**/
this.useApplyAsync = function(value) {
if (isDefined(value)) {
useApplyAsync = !!value;
return this;
}
return useApplyAsync;
};
/**
* Are ordered by request, i.e. they are applied in the same order as the
* array, on request, but reverse order, on response.
@@ -949,8 +977,16 @@ function $HttpProvider() {
}
}
resolvePromise(response, status, headersString, statusText);
if (!$rootScope.$$phase) $rootScope.$apply();
function resolveHttpPromise() {
resolvePromise(response, status, headersString, statusText);
}
if (useApplyAsync) {
$rootScope.$applyAsync(resolveHttpPromise);
} else {
resolveHttpPromise();
if (!$rootScope.$$phase) $rootScope.$apply();
}
}

View File

@@ -1488,11 +1488,11 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
* all pending requests will be flushed. If there are no pending requests when the flush method
* is called an exception is thrown (as this typically a sign of programming error).
*/
$httpBackend.flush = function(count) {
$rootScope.$digest();
$httpBackend.flush = function(count, digest) {
if (digest !== false) $rootScope.$digest();
if (!responses.length) throw new Error('No pending request to flush !');
if (angular.isDefined(count)) {
if (angular.isDefined(count) && count !== null) {
while (count--) {
if (!responses.length) throw new Error('No more pending request to flush !');
responses.shift()();
@@ -1502,7 +1502,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
responses.shift()();
}
}
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingExpectation(digest);
};
@@ -1520,8 +1520,8 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
* afterEach($httpBackend.verifyNoOutstandingExpectation);
* ```
*/
$httpBackend.verifyNoOutstandingExpectation = function() {
$rootScope.$digest();
$httpBackend.verifyNoOutstandingExpectation = function(digest) {
if (digest !== false) $rootScope.$digest();
if (expectations.length) {
throw new Error('Unsatisfied requests: ' + expectations.join(', '));
}

View File

@@ -1526,3 +1526,80 @@ describe('$http', function() {
$httpBackend.verifyNoOutstandingExpectation = noop;
});
});
describe('$http with $applyAapply', function() {
var $http, $httpBackend, $rootScope, $browser, log;
beforeEach(module(function($httpProvider) {
$httpProvider.useApplyAsync(true);
}, provideLog));
beforeEach(inject(['$http', '$httpBackend', '$rootScope', '$browser', 'log', function(http, backend, scope, browser, logger) {
$http = http;
$httpBackend = backend;
$rootScope = scope;
$browser = browser;
spyOn($rootScope, '$apply').andCallThrough();
spyOn($rootScope, '$applyAsync').andCallThrough();
spyOn($rootScope, '$digest').andCallThrough();
spyOn($browser.defer, 'cancel').andCallThrough();
log = logger;
}]));
it('should schedule coalesced apply on response', function() {
var handler = jasmine.createSpy('handler');
$httpBackend.expect('GET', '/template1.html').respond(200, '<h1>Header!</h1>', {});
$http.get('/template1.html').then(handler);
// Ensure requests are sent
$rootScope.$digest();
$httpBackend.flush(null, false);
expect($rootScope.$applyAsync).toHaveBeenCalledOnce();
expect(handler).not.toHaveBeenCalled();
$browser.defer.flush();
expect(handler).toHaveBeenCalledOnce();
});
it('should combine multiple responses within short time frame into a single $apply', function() {
$httpBackend.expect('GET', '/template1.html').respond(200, '<h1>Header!</h1>', {});
$httpBackend.expect('GET', '/template2.html').respond(200, '<p>Body!</p>', {});
$http.get('/template1.html').then(log.fn('response 1'));
$http.get('/template2.html').then(log.fn('response 2'));
// Ensure requests are sent
$rootScope.$digest();
$httpBackend.flush(null, false);
expect(log).toEqual([]);
$browser.defer.flush();
expect(log).toEqual(['response 1', 'response 2']);
});
it('should handle pending responses immediately if a digest occurs on $rootScope', function() {
$httpBackend.expect('GET', '/template1.html').respond(200, '<h1>Header!</h1>', {});
$httpBackend.expect('GET', '/template2.html').respond(200, '<p>Body!</p>', {});
$httpBackend.expect('GET', '/template3.html').respond(200, '<p>Body!</p>', {});
$http.get('/template1.html').then(log.fn('response 1'));
$http.get('/template2.html').then(log.fn('response 2'));
$http.get('/template3.html').then(log.fn('response 3'));
// Ensure requests are sent
$rootScope.$digest();
// Intermediate $digest occurs before 3rd response is received, assert that pending responses
/// are handled
$httpBackend.flush(2);
expect(log).toEqual(['response 1', 'response 2']);
// Finally, third response is received, and a second coalesced $apply is started
$httpBackend.flush(null, false);
$browser.defer.flush();
expect(log).toEqual(['response 1', 'response 2', 'response 3']);
});
});