-
Notifications
You must be signed in to change notification settings - Fork 27.4k
perf(*): don't trigger digests after enter/leave of structural directives #15345
Conversation
👏 Nitpicky, but |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An alternative, more radical approach (involving a BC) would be using $$q
instead of $q
for the AnimateRunner
and let people manually call $apply()
if their callback needs it. Most of the time, the animation callbacks trigger DOM-related operations/cleanup (and not touch on business logic or data), so a digest should not be necessary.
Unfortunately, such a BC has to wait for 1.7 now 😞
@@ -115,7 +115,7 @@ var ngIfDirective = ['$animate', '$compile', function($animate, $compile) { | |||
} | |||
if (block) { | |||
previousElements = getBlockNodes(block.clone); | |||
$animate.leave(previousElements).then(function() { | |||
$animate.leave(previousElements).done(function() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not functionally the same as .then()
. We need to check for success/error.
(Not sure if it makes a difference in practice.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doh ... you are right, I'll change that.
for (i = 0, ii = previousLeaveAnimations.length; i < ii; ++i) { | ||
|
||
// Start from the end, in case the array is modified during the loop | ||
for (i = previousLeaveAnimations.length - 1; i >= 0; i--) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change should not be necessary if we call the .done()
callback on success only.
@gkalpak Using $$q and letting people do $apply manually was implemented before and was changed specifically to use digest-integrated promises. I don't know if this was actually an issue for people, but I assume that it at least makes the behavior consistent with $http, and $q issued promises. |
220df23
to
ad104a6
Compare
@gkalpak This is ready for another review :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM with the exception of one comment/question.
if (runner.end) { | ||
runner.end(); | ||
if (runner.cancel) { | ||
runner.cancel(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is a good change (i.e. the right thing to do), but isn't it a breaking change.
I couldn't find the behavior documented anywhere btw.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it's not documented that it's not a change, so also no Breaking Change :>
Seriously though, I thought this change would only align ng.$animate with ngAnimate.$animate, but it turns out cancelling an actual animation resolves the promise.
The reason why I change this code is because otherwise .done isn't called with false
, so some switch tests fail. The problem is that these ngSwitch tests are "synthetic" in the sense that they never complete the "animations" they start because the rAFs aren't flushed. I think if we did that, then we didn't have to do this change. However, we would have to insert raF flushes in about 8 ngSwitch tests where they actually don't need to be there from considering the test's scope (these are not animation tests).
I'd be okay with introducing promise rejection on cancellation as a breaking change for 1.7 on both ng + ngAnimate, but that would mean this perf improvemtn has to wait ... at least for ngSwitch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought this change would only align
ng.$animate
withngAnimate.$animate
Isn't it the exact same $animate
that is being used. Only underlying pieces (e.g. $$animateQueue
) are different with ngAnimate
. So, whether we change it or not, $animate
will be consistent with and without ngAnimate
.
The reason why I change this code is because otherwise .done isn't called with false, so some switch tests fail.
It sounds more like a test issue, not an implementation issue.
I'd be okay with introducing promise rejection on cancellation as a breaking change for 1.7 on both ng + ngAnimate, but that would mean this perf improvemtn has to wait
Sorry, I didn't get why. Apart from some (bad) tests failing, what is the problem with using .done()
(but keeping the runner.end()
call above)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If $animate.cancel doesn't reject the promise, then .done callback handlers aren't called with false, which means in the ngSwitch done handler, I cannot distinguish between failed and successful animations, which leads to "cannot read function .end of undefined" error I had at the beginning. I then changed the code in ngSwitch to read the stored runners from the end to work around this problem, but then you said, I should check for successful animations inside done, which is how I found this problem with cancel in the first place
It's probably easier to understand if you run the tests with the various changes to see the different behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still don't understand why it worked before but not now. Previously, .then(cb)
was used. This is implemented in $animate
like this (code changed for brevity):
then: resolveHandler => {
return $q((resolve, reject) => this.done(status => status === false ? reject() : resolve())).
then(resolveHandler)
}
So, afaict we should be able to safely replace .then(cb)
with .done(status => status !== false && cb())
keeping the exact same behavior minus the extra digest (and asynchronicity) of $q
.
Is the fact that the callback is now synchronously called causing the problem?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see why my fix would fail. It doesn't rely on the index but on the item itself (looking it up via previousLeaveAnimations.indexOf(...)
). So, it is not affected by structural changes to the array.
Removing in reverse order should also work. (Why did I say I didn't like that?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because the error isn't coming from the splicer but from the cancel loop: http://plnkr.co/edit/MPUiYm4mTr9nNwg8SRS3?p=preview (Apologies if I'm being dense, it's very late again :D)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right. I think we have two problems (and my suggestion only takes care of one of them). So, in addition we should also change the cancellation code as follows (demo):
// Before:
for (i = 0, ii = previousLeaveAnimations.length; i < ii; ++i) {
$animate.cancel(previousLeaveAnimations[i]);
}
previousLeaveAnimations.length = 0;
// After:
while (previousLeaveAnimations.length) {
$animate.cancel(previousLeaveAnimations.shift());
}
In any case, I think it is fine to traverse the array backwards as an alternative (if it doesn't create other problems).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But do we need the splice change? I don't see any errors without it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we need the slice change if we traverse the array backwards. Either backwards-array-traversal
or slice-change+while-loop
. Up to you 😃
2e29fc3
to
696b7a4
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A couple of minor comments. LGTM otherwise.
@@ -333,6 +333,36 @@ describe('ngSwitch', function() { | |||
})); | |||
|
|||
|
|||
it('should handle changes to the switch value in a digest loop with multiple value matches', | |||
inject(function($$rAF, $compile, $rootScope, $timeout) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are $$rAF
and $timeout
needed?
$rootScope.select = 2; | ||
$rootScope.$apply(); | ||
$$rAF.flush(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that 696b7a4 has been added, are these extra $$rAF
calls necessary?
|
||
|
||
it('should handle changes to the switch value in a digest loop with multiple value matches', | ||
inject(function($$rAF, $compile, $rootScope, $timeout) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are $$rAF
and $timeout
necessary?
Good catch, no, these flushes are not necessary. The only one necessary is in the test that ensures that no digest is triggered after the leave animation |
OK, are we happy to merge this? |
I am 😃 |
Please do: 1.5.x and master? |
…ives ngIf, ngInclude, ngSwitch, and ngView now use the `done` callback functions on animation runners returned by leave/enter animations to do internal cleanup (and $anchorScroll for ngInclude and ngView). Previously, they were using promise callbacks (`then`), which caused an unnessecary digest. Background: In angular@bf0f550, animation promises where introduced instead of animation callbacks. These promises were however not tied to the digest cycle, so you had to manually call `$apply` inside them. This was changed in angular@c8700f0, so animation promise callbacks would always trigger a `$digest`. This meant that several built-in directives would now trigger additional digests on leave (ngIf, ngSwitch) or enter/leave (ngInclude, ngView). The `done` callback, which receives a single argument indicating success / failure was introduced to allow digest-less responses, but wasn't applied to these directives. Note that this applies to all calls to $animate.enter/leave, even if ngAnimate isn't included, and no actual animations are running, because the animation runner code is in the core ng module. Fixes angular#15322 Closes angular#15345
dc57ef2
to
03ce2e7
Compare
…ives ngIf, ngInclude, ngSwitch, and ngView now use the `done` callback functions on animation runners returned by leave/enter animations to do internal cleanup (and $anchorScroll for ngInclude and ngView). Previously, they were using promise callbacks (`then`), which caused an unnessecary digest. Background: In bf0f550, animation promises where introduced instead of animation callbacks. These promises were however not tied to the digest cycle, so you had to manually call `$apply` inside them. This was changed in c8700f0, so animation promise callbacks would always trigger a `$digest`. This meant that several built-in directives would now trigger additional digests on leave (ngIf, ngSwitch) or enter/leave (ngInclude, ngView). The `done` callback, which receives a single argument indicating success / failure was introduced to allow digest-less responses, but wasn't applied to these directives. Note that this applies to all calls to $animate.enter/leave, even if ngAnimate isn't included, and no actual animations are running, because the animation runner code is in the core ng module. Fixes #15322 Closes #15345
…ives ngIf, ngInclude, ngSwitch, and ngView now use the `done` callback functions on animation runners returned by leave/enter animations to do internal cleanup (and $anchorScroll for ngInclude and ngView). Previously, they were using promise callbacks (`then`), which caused an unnessecary digest. Background: In bf0f550, animation promises where introduced instead of animation callbacks. These promises were however not tied to the digest cycle, so you had to manually call `$apply` inside them. This was changed in c8700f0, so animation promise callbacks would always trigger a `$digest`. This meant that several built-in directives would now trigger additional digests on leave (ngIf, ngSwitch) or enter/leave (ngInclude, ngView). The `done` callback, which receives a single argument indicating success / failure was introduced to allow digest-less responses, but wasn't applied to these directives. Note that this applies to all calls to $animate.enter/leave, even if ngAnimate isn't included, and no actual animations are running, because the animation runner code is in the core ng module. Fixes #15322 Closes #15345
…ives ngIf, ngInclude, ngSwitch, and ngView now use the `done` callback functions on animation runners returned by leave/enter animations to do internal cleanup (and $anchorScroll for ngInclude and ngView). Previously, they were using promise callbacks (`then`), which caused an unnessecary digest. Background: In bf0f550, animation promises where introduced instead of animation callbacks. These promises were however not tied to the digest cycle, so you had to manually call `$apply` inside them. This was changed in c8700f0, so animation promise callbacks would always trigger a `$digest`. This meant that several built-in directives would now trigger additional digests on leave (ngIf, ngSwitch) or enter/leave (ngInclude, ngView). The `done` callback, which receives a single argument indicating success / failure was introduced to allow digest-less responses, but wasn't applied to these directives. Note that this applies to all calls to $animate.enter/leave, even if ngAnimate isn't included, and no actual animations are running, because the animation runner code is in the core ng module. Fixes #15322 Closes #15345
…ives ngIf, ngInclude, ngSwitch, and ngView now use the `done` callback functions on animation runners returned by leave/enter animations to do internal cleanup (and $anchorScroll for ngInclude and ngView). Previously, they were using promise callbacks (`then`), which caused an unnessecary digest. Background: In angular@bf0f550, animation promises where introduced instead of animation callbacks. These promises were however not tied to the digest cycle, so you had to manually call `$apply` inside them. This was changed in angular@c8700f0, so animation promise callbacks would always trigger a `$digest`. This meant that several built-in directives would now trigger additional digests on leave (ngIf, ngSwitch) or enter/leave (ngInclude, ngView). The `done` callback, which receives a single argument indicating success / failure was introduced to allow digest-less responses, but wasn't applied to these directives. Note that this applies to all calls to $animate.enter/leave, even if ngAnimate isn't included, and no actual animations are running, because the animation runner code is in the core ng module. Fixes angular#15322 Closes angular#15345
What kind of change does this PR introduce? (Bug fix, feature, docs update, ...)
perf
What is the current behavior? (You can also link to an open issue here)
Structural animations such as ngIf trigger an additional digest after $animate.leave completes
Does this PR introduce a breaking change?
No
Please check if the PR fulfills these requirements
[ ] Docs have been added / updated (for bug fixes / features)