Thanks to visit codestin.com
Credit goes to github.com

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

feat(*): coalesce $apply calls which occur close together WIP #8736

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 38 additions & 2 deletions src/ng/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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();
}
}


Expand Down
56 changes: 56 additions & 0 deletions src/ng/rootScope.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ function $RootScopeProvider(){
var TTL = 10;
var $rootScopeMinErr = minErr('$rootScope');
var lastDirtyWatch = null;
var applyAsyncId = null;

this.digestTtl = function(value) {
if (arguments.length) {
Expand Down Expand Up @@ -134,6 +135,7 @@ function $RootScopeProvider(){
this.$$listeners = {};
this.$$listenerCount = {};
this.$$isolateBindings = {};
this.$$applyAsyncQueue = [];
}

/**
Expand Down Expand Up @@ -688,6 +690,13 @@ function $RootScopeProvider(){

beginPhase('$digest');

if (this === $rootScope && applyAsyncId !== null) {
// If this is the root scope, and $applyAsync has scheduled a deferred $apply(), then
// cancel the scheduled $apply and flush the queue of expressions to be evaluated.
$browser.defer.cancel(applyAsyncId);
flushApplyAsync();
}

lastDirtyWatch = null;

do { // "while dirty" loop
Expand Down Expand Up @@ -997,6 +1006,33 @@ function $RootScopeProvider(){
}
},

/**
* @ngdoc method
* @name $rootScope.Scope#$applyAsync
* @kind function
*
* @description
* Schedule the invokation of $apply to occur at a later time. The actual time difference
* varies across browsers, but is typically around ~10 milliseconds.
*
* This can be used to queue up multiple expressions which need to be evaluated in the same
* digest.
*
* @param {(string|function())=} exp An angular expression to be executed.
*
* - `string`: execute using the rules as defined in {@link guide/expression expression}.
* - `function(scope)`: execute the function with current `scope` parameter.
*/
$applyAsync: function(expr) {
var scope = this;
expr && $rootScope.$$applyAsyncQueue.push($applyAsyncExpression);
scheduleApplyAsync();

function $applyAsyncExpression() {
scope.$eval(expr);
}
},

/**
* @ngdoc method
* @name $rootScope.Scope#$on
Expand Down Expand Up @@ -1229,5 +1265,25 @@ function $RootScopeProvider(){
* because it's unique we can easily tell it apart from other values
*/
function initWatchVal() {}

function flushApplyAsync() {
var queue = $rootScope.$$applyAsyncQueue;
while (queue.length) {
try {
queue.shift()();
} catch(e) {
$exceptionHandler(e);
}
}
applyAsyncId = null;
}

function scheduleApplyAsync() {
if (applyAsyncId === null) {
applyAsyncId = $browser.defer(function() {
$rootScope.$apply(flushApplyAsync);
});
}
}
}];
}
12 changes: 6 additions & 6 deletions src/ngMock/angular-mocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()();
Expand All @@ -1502,7 +1502,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
responses.shift()();
}
}
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingExpectation(digest);
};


Expand All @@ -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(', '));
}
Expand Down
77 changes: 77 additions & 0 deletions test/ng/httpSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
});
});
85 changes: 85 additions & 0 deletions test/ng/rootScopeSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1399,6 +1399,91 @@ describe('Scope', function() {
});


describe('$applyAsync', function() {
beforeEach(module(function($exceptionHandlerProvider) {
$exceptionHandlerProvider.mode('log');
}));


it('should evaluate in the context of specific $scope', inject(function($rootScope, $browser) {
var scope = $rootScope.$new();
scope.$applyAsync('x = "CODE ORANGE"');

$browser.defer.flush();
expect(scope.x).toBe('CODE ORANGE');
expect($rootScope.x).toBeUndefined();
}));


it('should evaluate queued expressions in order', inject(function($rootScope, $browser) {
$rootScope.x = [];
$rootScope.$applyAsync('x.push("expr1")');
$rootScope.$applyAsync('x.push("expr2")');

$browser.defer.flush();
expect($rootScope.x).toEqual(['expr1', 'expr2']);
}));


it('should evaluate subsequently queued items in same turn', inject(function($rootScope, $browser) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we might not really want to support this, but it doesn't hurt

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's fine

$rootScope.x = [];
$rootScope.$applyAsync(function() {
$rootScope.x.push('expr1');
$rootScope.$applyAsync('x.push("expr2")');
expect($browser.deferredFns.length).toBe(0);
});

$browser.defer.flush();
expect($rootScope.x).toEqual(['expr1', 'expr2']);
}));


it('should pass thrown exceptions to $exceptionHandler', inject(function($rootScope, $browser, $exceptionHandler) {
$rootScope.$applyAsync(function() {
throw 'OOPS';
});

$browser.defer.flush();
expect($exceptionHandler.errors).toEqual([
'OOPS'
]);
}));


it('should evaluate subsequent expressions after an exception is thrown', inject(function($rootScope, $browser) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

technically this depends on whether $exceptionHandler is rethrowing or not

$rootScope.$applyAsync(function() {
throw 'OOPS';
});
$rootScope.$applyAsync('x = "All good!"');

$browser.defer.flush();
expect($rootScope.x).toBe('All good!');
}));


it('should be cancelled if a $rootScope digest occurs before the next tick', inject(function($rootScope, $browser) {
var apply = spyOn($rootScope, '$apply').andCallThrough();
var cancel = spyOn($browser.defer, 'cancel').andCallThrough();
var expression = jasmine.createSpy('expr');

$rootScope.$applyAsync(expression);
$rootScope.$digest();
expect(expression).toHaveBeenCalledOnce();
expect(cancel).toHaveBeenCalledOnce();
expression.reset();
cancel.reset();

// assert that we no longer are waiting to execute
expect($browser.deferredFns.length).toBe(0);

// assert that another digest won't call the function again
$rootScope.$digest();
expect(expression).not.toHaveBeenCalled();
expect(cancel).not.toHaveBeenCalled();
}));
});


describe('events', function() {

describe('$on', function() {
Expand Down