Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit ea6fc6e

Browse files
committed
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
1 parent e94d454 commit ea6fc6e

File tree

3 files changed

+121
-8
lines changed

3 files changed

+121
-8
lines changed

src/ng/http.js

+38-2
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,34 @@ function $HttpProvider() {
143143
xsrfHeaderName: 'X-XSRF-TOKEN'
144144
};
145145

146+
var useApplyAsync = false;
147+
/**
148+
* @ngdoc method
149+
* @name $httpProvider#useApplyAsync
150+
* @description
151+
*
152+
* Configure $http service to combine processing of multiple http responses received at around
153+
* the same time via {@link ng.$rootScope#applyAsync $rootScope.$applyAsync}. This can result in
154+
* significant performance improvement for bigger applications that make many HTTP requests
155+
* concurrently (common during application bootstrap).
156+
*
157+
* Defaults to false. If no value is specifed, returns the current configured value.
158+
*
159+
* @param {boolean=} value If true, when requests are loaded, they will schedule a deferred
160+
* "apply" on the next tick, giving time for subsequent requests in a roughly ~10ms window
161+
* to load and share the same digest cycle.
162+
*
163+
* @returns {boolean|Object} If a value is specified, returns the $httpProvider for chaining.
164+
* otherwise, returns the current configured value.
165+
**/
166+
this.useApplyAsync = function(value) {
167+
if (isDefined(value)) {
168+
useApplyAsync = !!value;
169+
return this;
170+
}
171+
return useApplyAsync;
172+
};
173+
146174
/**
147175
* Are ordered by request, i.e. they are applied in the same order as the
148176
* array, on request, but reverse order, on response.
@@ -949,8 +977,16 @@ function $HttpProvider() {
949977
}
950978
}
951979

952-
resolvePromise(response, status, headersString, statusText);
953-
if (!$rootScope.$$phase) $rootScope.$apply();
980+
function resolveHttpPromise() {
981+
resolvePromise(response, status, headersString, statusText);
982+
}
983+
984+
if (useApplyAsync) {
985+
$rootScope.$applyAsync(resolveHttpPromise);
986+
} else {
987+
resolveHttpPromise();
988+
if (!$rootScope.$$phase) $rootScope.$apply();
989+
}
954990
}
955991

956992

src/ngMock/angular-mocks.js

+6-6
Original file line numberDiff line numberDiff line change
@@ -1488,11 +1488,11 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
14881488
* all pending requests will be flushed. If there are no pending requests when the flush method
14891489
* is called an exception is thrown (as this typically a sign of programming error).
14901490
*/
1491-
$httpBackend.flush = function(count) {
1492-
$rootScope.$digest();
1491+
$httpBackend.flush = function(count, digest) {
1492+
if (digest !== false) $rootScope.$digest();
14931493
if (!responses.length) throw new Error('No pending request to flush !');
14941494

1495-
if (angular.isDefined(count)) {
1495+
if (angular.isDefined(count) && count !== null) {
14961496
while (count--) {
14971497
if (!responses.length) throw new Error('No more pending request to flush !');
14981498
responses.shift()();
@@ -1502,7 +1502,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
15021502
responses.shift()();
15031503
}
15041504
}
1505-
$httpBackend.verifyNoOutstandingExpectation();
1505+
$httpBackend.verifyNoOutstandingExpectation(digest);
15061506
};
15071507

15081508

@@ -1520,8 +1520,8 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
15201520
* afterEach($httpBackend.verifyNoOutstandingExpectation);
15211521
* ```
15221522
*/
1523-
$httpBackend.verifyNoOutstandingExpectation = function() {
1524-
$rootScope.$digest();
1523+
$httpBackend.verifyNoOutstandingExpectation = function(digest) {
1524+
if (digest !== false) $rootScope.$digest();
15251525
if (expectations.length) {
15261526
throw new Error('Unsatisfied requests: ' + expectations.join(', '));
15271527
}

test/ng/httpSpec.js

+77
Original file line numberDiff line numberDiff line change
@@ -1526,3 +1526,80 @@ describe('$http', function() {
15261526
$httpBackend.verifyNoOutstandingExpectation = noop;
15271527
});
15281528
});
1529+
1530+
1531+
describe('$http with $applyAapply', function() {
1532+
var $http, $httpBackend, $rootScope, $browser, log;
1533+
beforeEach(module(function($httpProvider) {
1534+
$httpProvider.useApplyAsync(true);
1535+
}, provideLog));
1536+
1537+
1538+
beforeEach(inject(['$http', '$httpBackend', '$rootScope', '$browser', 'log', function(http, backend, scope, browser, logger) {
1539+
$http = http;
1540+
$httpBackend = backend;
1541+
$rootScope = scope;
1542+
$browser = browser;
1543+
spyOn($rootScope, '$apply').andCallThrough();
1544+
spyOn($rootScope, '$applyAsync').andCallThrough();
1545+
spyOn($rootScope, '$digest').andCallThrough();
1546+
spyOn($browser.defer, 'cancel').andCallThrough();
1547+
log = logger;
1548+
}]));
1549+
1550+
1551+
it('should schedule coalesced apply on response', function() {
1552+
var handler = jasmine.createSpy('handler');
1553+
$httpBackend.expect('GET', '/template1.html').respond(200, '<h1>Header!</h1>', {});
1554+
$http.get('/template1.html').then(handler);
1555+
// Ensure requests are sent
1556+
$rootScope.$digest();
1557+
1558+
$httpBackend.flush(null, false);
1559+
expect($rootScope.$applyAsync).toHaveBeenCalledOnce();
1560+
expect(handler).not.toHaveBeenCalled();
1561+
1562+
$browser.defer.flush();
1563+
expect(handler).toHaveBeenCalledOnce();
1564+
});
1565+
1566+
1567+
it('should combine multiple responses within short time frame into a single $apply', function() {
1568+
$httpBackend.expect('GET', '/template1.html').respond(200, '<h1>Header!</h1>', {});
1569+
$httpBackend.expect('GET', '/template2.html').respond(200, '<p>Body!</p>', {});
1570+
1571+
$http.get('/template1.html').then(log.fn('response 1'));
1572+
$http.get('/template2.html').then(log.fn('response 2'));
1573+
// Ensure requests are sent
1574+
$rootScope.$digest();
1575+
1576+
$httpBackend.flush(null, false);
1577+
expect(log).toEqual([]);
1578+
1579+
$browser.defer.flush();
1580+
expect(log).toEqual(['response 1', 'response 2']);
1581+
});
1582+
1583+
1584+
it('should handle pending responses immediately if a digest occurs on $rootScope', function() {
1585+
$httpBackend.expect('GET', '/template1.html').respond(200, '<h1>Header!</h1>', {});
1586+
$httpBackend.expect('GET', '/template2.html').respond(200, '<p>Body!</p>', {});
1587+
$httpBackend.expect('GET', '/template3.html').respond(200, '<p>Body!</p>', {});
1588+
1589+
$http.get('/template1.html').then(log.fn('response 1'));
1590+
$http.get('/template2.html').then(log.fn('response 2'));
1591+
$http.get('/template3.html').then(log.fn('response 3'));
1592+
// Ensure requests are sent
1593+
$rootScope.$digest();
1594+
1595+
// Intermediate $digest occurs before 3rd response is received, assert that pending responses
1596+
/// are handled
1597+
$httpBackend.flush(2);
1598+
expect(log).toEqual(['response 1', 'response 2']);
1599+
1600+
// Finally, third response is received, and a second coalesced $apply is started
1601+
$httpBackend.flush(null, false);
1602+
$browser.defer.flush();
1603+
expect(log).toEqual(['response 1', 'response 2', 'response 3']);
1604+
});
1605+
});

0 commit comments

Comments
 (0)