diff --git a/src/ng/http.js b/src/ng/http.js index 9017fe85..1fdc615f 100644 --- a/src/ng/http.js +++ b/src/ng/http.js @@ -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(); + } } diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js index ba979053..073dadc0 100644 --- a/src/ngMock/angular-mocks.js +++ b/src/ngMock/angular-mocks.js @@ -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(', ')); } diff --git a/test/ng/httpSpec.js b/test/ng/httpSpec.js index c1c33ffb..c2ad25f0 100644 --- a/test/ng/httpSpec.js +++ b/test/ng/httpSpec.js @@ -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, '
Body!
', {}); + + $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, 'Body!
', {}); + $httpBackend.expect('GET', '/template3.html').respond(200, 'Body!
', {}); + + $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']); + }); +});