From 27abfa853d0407f63537dc25695d99fd3b73d83c Mon Sep 17 00:00:00 2001 From: Shahar Talmi Date: Wed, 23 Jul 2014 00:40:22 +0300 Subject: [PATCH 1/2] feat(ngEventDirs): allow $apply expressions to prevent unnecessary digest This allows, for example to prevent unnecessary digest in ngClick handler function in case the handler does not have any immediate side effects. Closes #8286 --- src/ng/directive/ngEventDirs.js | 4 ++-- src/ng/rootScope.js | 15 +++++++++------ test/ng/directive/ngClickSpec.js | 8 ++++++++ test/ng/rootScopeSpec.js | 6 ++++++ 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/ng/directive/ngEventDirs.js b/src/ng/directive/ngEventDirs.js index c14b173a2b48..ee0967e78c8d 100644 --- a/src/ng/directive/ngEventDirs.js +++ b/src/ng/directive/ngEventDirs.js @@ -60,8 +60,8 @@ forEach( var fn = $parse(attr[directiveName], /* interceptorFn */ null, /* expensiveChecks */ true); return function ngEventHandler(scope, element) { element.on(eventName, function(event) { - var callback = function() { - fn(scope, {$event:event}); + var callback = function(scope, locals) { + fn(scope, {$event:event, $abortApply: locals && locals.$abortApply}); }; if (forceAsyncEvents[eventName] && $rootScope.$$phase) { scope.$evalAsync(callback); diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js index 1ee04ed15770..7d1f719a2143 100644 --- a/src/ng/rootScope.js +++ b/src/ng/rootScope.js @@ -1022,18 +1022,21 @@ function $RootScopeProvider() { * @returns {*} The result of evaluating the expression. */ $apply: function(expr) { + var abortApply = false; try { beginPhase('$apply'); - return this.$eval(expr); + return this.$eval(expr, {$abortApply: function() {abortApply = true;}}); } catch (e) { $exceptionHandler(e); } finally { clearPhase(); - try { - $rootScope.$digest(); - } catch (e) { - $exceptionHandler(e); - throw e; + if (!abortApply) { + try { + $rootScope.$digest(); + } catch (e) { + $exceptionHandler(e); + throw e; + } } } }, diff --git a/test/ng/directive/ngClickSpec.js b/test/ng/directive/ngClickSpec.js index 3e997c462b20..65b85ba6104f 100644 --- a/test/ng/directive/ngClickSpec.js +++ b/test/ng/directive/ngClickSpec.js @@ -23,4 +23,12 @@ describe('ngClick', function() { browserTrigger(element, 'click'); expect($rootScope.event).toBeDefined(); })); + + it('should abort apply if abort argument is invoked', inject(function($rootScope, $compile) { + element = $compile('
{{a}}
')($rootScope); + $rootScope.$digest(); + + browserTrigger(element, 'click'); + expect(element.text()).toBe(''); + })); }); diff --git a/test/ng/rootScopeSpec.js b/test/ng/rootScopeSpec.js index 6ff240d6330c..18337dc8881a 100644 --- a/test/ng/rootScopeSpec.js +++ b/test/ng/rootScopeSpec.js @@ -1333,6 +1333,12 @@ describe('Scope', function() { expect(log).toEqual('1'); })); + it('should abort apply if abort argument is invoked', inject(function ($rootScope) { + var watchSpy = jasmine.createSpy('watchSpy'); + $rootScope.$watch(watchSpy); + $rootScope.$apply('$abortApply()'); + expect(watchSpy).not.toHaveBeenCalled(); + })); it('should catch exceptions', function() { module(function($exceptionHandlerProvider) { From 027a16e64034f5a0012fac498c1f7e698b564e8c Mon Sep 17 00:00:00 2001 From: Shahar Talmi Date: Sun, 14 Dec 2014 09:29:36 +0200 Subject: [PATCH 2/2] feat(ngEventDirs): allow $apply expressions to perform partial digest This allows to prevent unnecessary watchers to be invoked in case the handler side effects are limited to a certain scope Closes #7298 --- src/ng/directive/ngEventDirs.js | 6 +++++- src/ng/rootScope.js | 14 ++++++++++++-- test/ng/directive/ngClickSpec.js | 12 ++++++++++++ test/ng/rootScopeSpec.js | 29 ++++++++++++++++++++++++++++- 4 files changed, 57 insertions(+), 4 deletions(-) diff --git a/src/ng/directive/ngEventDirs.js b/src/ng/directive/ngEventDirs.js index ee0967e78c8d..3a8f3d034a60 100644 --- a/src/ng/directive/ngEventDirs.js +++ b/src/ng/directive/ngEventDirs.js @@ -61,7 +61,11 @@ forEach( return function ngEventHandler(scope, element) { element.on(eventName, function(event) { var callback = function(scope, locals) { - fn(scope, {$event:event, $abortApply: locals && locals.$abortApply}); + fn(scope, { + $event:event, + $abortApply: locals && locals.$abortApply, + $partialDigest: locals && locals.$partialDigest + }); }; if (forceAsyncEvents[eventName] && $rootScope.$$phase) { scope.$evalAsync(callback); diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js index 7d1f719a2143..ace548cef3b3 100644 --- a/src/ng/rootScope.js +++ b/src/ng/rootScope.js @@ -1023,16 +1023,26 @@ function $RootScopeProvider() { */ $apply: function(expr) { var abortApply = false; + var scopeForDigest = $rootScope; + var currentScope = this; + try { beginPhase('$apply'); - return this.$eval(expr, {$abortApply: function() {abortApply = true;}}); + return this.$eval(expr, { + $abortApply: function() { + abortApply = true; + }, + $partialDigest: function(scope) { + scopeForDigest = scope || currentScope; + } + }); } catch (e) { $exceptionHandler(e); } finally { clearPhase(); if (!abortApply) { try { - $rootScope.$digest(); + scopeForDigest.$digest(); } catch (e) { $exceptionHandler(e); throw e; diff --git a/test/ng/directive/ngClickSpec.js b/test/ng/directive/ngClickSpec.js index 65b85ba6104f..a8f3acf163af 100644 --- a/test/ng/directive/ngClickSpec.js +++ b/test/ng/directive/ngClickSpec.js @@ -31,4 +31,16 @@ describe('ngClick', function() { browserTrigger(element, 'click'); expect(element.text()).toBe(''); })); + + it('should digest locally if $partialDigest is invoked', inject(function($rootScope, $compile) { + var scope = $rootScope.$new(); + element = $compile('
{{a}}
')(scope); + $rootScope.$digest(); + + var watchSpy = jasmine.createSpy('watchSpy'); + $rootScope.$watch(watchSpy); + browserTrigger(element, 'click'); + expect(element.text()).toBe('1'); + expect(watchSpy).not.toHaveBeenCalled(); + })); }); diff --git a/test/ng/rootScopeSpec.js b/test/ng/rootScopeSpec.js index 18337dc8881a..9a8308f60046 100644 --- a/test/ng/rootScopeSpec.js +++ b/test/ng/rootScopeSpec.js @@ -1333,13 +1333,40 @@ describe('Scope', function() { expect(log).toEqual('1'); })); - it('should abort apply if abort argument is invoked', inject(function ($rootScope) { + it('should abort apply if abort argument is invoked', inject(function($rootScope) { var watchSpy = jasmine.createSpy('watchSpy'); $rootScope.$watch(watchSpy); $rootScope.$apply('$abortApply()'); expect(watchSpy).not.toHaveBeenCalled(); })); + it('should perform digest locally if $partialDigest is invoked', inject(function($rootScope) { + var watchRootSpy = jasmine.createSpy('watchRootSpy'); + var watchSpy = jasmine.createSpy('watchSpy'); + var child = $rootScope.$new(); + + $rootScope.$watch(watchRootSpy); + child.$watch(watchSpy); + child.$apply('$partialDigest()'); + + expect(watchRootSpy).not.toHaveBeenCalled(); + expect(watchSpy).toHaveBeenCalled(); + })); + + it('should perform digest on specific scope', inject(function($rootScope) { + var watchRootSpy = jasmine.createSpy('watchRootSpy'); + var watchSpy = jasmine.createSpy('watchSpy'); + var child = $rootScope.$new(); + var grandChild = child.$new(); + + $rootScope.$watch(watchRootSpy); + child.$watch(watchSpy); + grandChild.$apply('$partialDigest($parent)'); + + expect(watchRootSpy).not.toHaveBeenCalled(); + expect(watchSpy).toHaveBeenCalled(); + })); + it('should catch exceptions', function() { module(function($exceptionHandlerProvider) { $exceptionHandlerProvider.mode('log');