mirror of
https://github.com/zhigang1992/angular.js.git
synced 2026-06-13 08:30:36 +08:00
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:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
12
src/ngMock/angular-mocks.js
vendored
12
src/ngMock/angular-mocks.js
vendored
@@ -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(', '));
|
||||
}
|
||||
|
||||
@@ -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']);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user