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

Commit 4ae9b8b

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.coalescedApply(true); }); OR: angular.mock.module(function($httpProvider) { $httpProvider.coalescedApply(true); });
1 parent a2dd040 commit 4ae9b8b

File tree

3 files changed

+119
-8
lines changed

3 files changed

+119
-8
lines changed

src/ng/http.js

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

146+
var coalesceApply = false;
147+
/**
148+
* @ngdoc method
149+
* @name $httpProvider#coalesceApply
150+
* @description
151+
*
152+
* Configure $http requests to coalesce calls to $apply() for requests which are loaded close
153+
* together. Defaults to false. If no value is specifed, returns the current configured value.
154+
*
155+
* @param {boolean=} value If true, when requests are loaded, they will schedule a deferred
156+
* "apply" on the next tick, giving time for subsequent requests in a roughly ~10ms window
157+
* to load and share the same digest cycle.
158+
*
159+
* @returns {boolean|Object} If a value is specified, returns the $httpProvider for chaining.
160+
* otherwise, returns the current configured value.
161+
**/
162+
this.coalesceApply = function(value) {
163+
if (isDefined(value)) {
164+
coalesceApply = !!value;
165+
return this;
166+
}
167+
return coalesceApply;
168+
}
169+
146170
/**
147171
* Are ordered by request, i.e. they are applied in the same order as the
148172
* array, on request, but reverse order, on response.
@@ -949,8 +973,12 @@ function $HttpProvider() {
949973
}
950974
}
951975

952-
resolvePromise(response, status, headersString, statusText);
953-
if (!$rootScope.$$phase) $rootScope.$apply();
976+
if (coalesceApply) {
977+
$rootScope.$$applyAsync(bind(null, resolvePromise, response, status, headersString, statusText));
978+
} else {
979+
resolvePromise(response, status, headersString, statusText);
980+
if (!$rootScope.$$phase) $rootScope.$apply();
981+
}
954982
}
955983

956984

src/ngMock/angular-mocks.js

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

1487-
if (angular.isDefined(count)) {
1487+
if (angular.isDefined(count) && count !== null) {
14881488
while (count--) {
14891489
if (!responses.length) throw new Error('No more pending request to flush !');
14901490
responses.shift()();
@@ -1494,7 +1494,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
14941494
responses.shift()();
14951495
}
14961496
}
1497-
$httpBackend.verifyNoOutstandingExpectation();
1497+
$httpBackend.verifyNoOutstandingExpectation(digest);
14981498
};
14991499

15001500

@@ -1512,8 +1512,8 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
15121512
* afterEach($httpBackend.verifyNoOutstandingExpectation);
15131513
* ```
15141514
*/
1515-
$httpBackend.verifyNoOutstandingExpectation = function() {
1516-
$rootScope.$digest();
1515+
$httpBackend.verifyNoOutstandingExpectation = function(digest) {
1516+
if (digest !== false) $rootScope.$digest();
15171517
if (expectations.length) {
15181518
throw new Error('Unsatisfied requests: ' + expectations.join(', '));
15191519
}

test/ng/httpSpec.js

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

0 commit comments

Comments
 (0)