-
Notifications
You must be signed in to change notification settings - Fork 27.5k
fix($rootScope): avoid unstable reference in $broadcast event #7445
Conversation
The event.currentScope was a reference to the iterator of the scope traversal (`current`). It caused problems when accessing the event.currentScope asynchronously : it was the last scope of the traversal instead of the scope which listen the event.
Thanks for the PR! Please check the items below to help us merge this faster. See the contributing docs for more information.
If you need to make changes to your pull request, you can update the commit with Thanks again for your help! |
@ncuillery Thanks for the PR. This does look like a real bug, anyhow, I have a few comments
|
Right, I saw About testing, I broke the I gonna play with jsPerf. |
@ncuillery if I apply only the changes to |
} | ||
Event.prototype.preventDefault = function() { | ||
this.defaultPrevented = true; | ||
}; |
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.
What is the purpose of this 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.
It defines a javascript class which is instantiated for each listener among the tree of scopes.
By this way, each listener received a dedicated event with a stable currentScope
property.
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 why does this class need a preventDefault
? There is not action being taken there other then altering this.defaultPrevented
. This seems misleading to me.
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 retained the existing preventDefault
.
I asked myself for the purpose of this property. Perhaps to be consistent with DOM events.
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 feel it should be removed, this may have been a mistake in the angular api. Events without a native default should not have a preventDefault method. @lgalfaso thoughts?
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 they should have preventDefault
even when in the case of a $broadcast
it does nothing
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.
@lgalfaso why? I don't believe it does anything for $emit
either. Its just basically a misleading noop
.
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.
@Fresheyeball the event used to have a function called preventDefault
and that should not be removed, user may call it (even if it does nothing).
In the case of $emit
, calling preventDefault
does prevent the event from going up
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.
@lgalfaso thats even worse. Why would preventDefault
be stopPropagation
? There is no browser default, so there should be no prevent default. That is exactly why this is bad.
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.
@Fresheyeball There are two things involved there
This PR is to fix an existing issue with events current
parameter (that affects $broadcast
and $emit
). As part of this fix, it is required that this does not break what people expect from the API, this includes that there is a function called preventDefault
. How this function works is not part of this PR.
Now, preventDefault
is part of the DOM standard http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-Event-preventDefault. This patch is not about adding/removing or being consistent on the fact that there is a preventDefault
function and not stopPropagation
nor on the fact that preventDefault
works in one way when calling $broadcast
and in another way when calling $emit
. If you feel that this is a limitation you are welcome to put a PR in place
Travis build should be failed with this commit (meaning that the test case in failure is relevant for the bug seen in angular#7445). MUSTN'T BE MERGE
@lgalfaso Are you sure ? ;) I create a "trashable" PR with only the change on
The currentScope is the grandChild (003, end of the scope traversal) instead of the child (002, listening to the event). In current master, the currentScope is 002 because it's the scope listening to event and, by chance, the end of the scope traversal. But it's an "abstract" particular case (AngularJS apps rarely have only two scopes). That's why I'm a bit reluctant to add a new test case. I can add a test case with both synchronous and asynchronous expectations but it will globally look like the existing one with 3 scopes instead of 2 and a different description in |
@ncuillery you are right, the test fails with the new scope, my bad. There are two aspects to be talked about
On top, there is a real issue with |
Yes indeed, I was wrong when I said there is no bug in I'm working on a fix for |
The event.currentScope in `$emit` method was a reference to an iterator. When accessing asynchronously, the event.currentScope was the $rootScope (end of the do/while loop) instead of the scope which listen the event. The `$emit` and `$broadcast` methods use the same syntax now. Both methods are tested with : - sync/async access to `event.currentScope` and `event.targetScope`. - a test case for `preventDefault()` and `defaultPrevented`.
They are modified version of $emit and $broadcast (see angular#7445)
Now I investigated performances implications with jsperf: The test cases call the new and the original I change the number of listeners in different test cases (the tree is the same):
I think the huge lack of perfs (when there is little or no listener) is not relevant because jsperf calibrate the number of execution from the average time of execution. In the 0-listener test case, Chrome still handle 130k calls to The results are greatly influenced by the content of the listeners because their execution time is included in the measured time. Tests above have a The results are very unprofitable when listeners are empty:
At the opposite, they become better when the listener changes a variable of the scope (a common use case):
I objectively think that the updated I hope this will help. I'm not really a JS perf tester, so feel free to analyze and criticize this post. I present only time-based tests, should I do memory-based tests ? with Chrome dev tools ?. Others tests for |
why create |
event = e; | ||
}); | ||
scope.$broadcast('fooEvent'); | ||
|
||
expect(event.name).toBe('fooEvent'); | ||
// Asynchronous expectation : current & target should be the same as 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.
wat
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.
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.
it's not asynchronous, and you should not give the impression that it's asynchronous
I made this to be consistent with the existing similar test case for $emit
That's a test case you wrote yourself, it's not present in angular core! Just remove the "Asynchronous expectation..." bit
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.
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.
You understand that I'm referring to the comment, right?
That scope event handler is not being called asynchronously, there is nothing asynchronous happening here, it's all within a single turn of the event loop
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.
Oops. No, I didn't. Sorry for that.
If I understand this, you're creating a bunch of garbage (to be collected) for each scope an event bubbles over to, and this is not likely to have any kind of performance or usability benefit. What it's going to do is cause the GC to have a lot of work to do, which will cause pauses in the application What are you trying to accomplish here?
This is a non-issue, just save Further, |
During the traversal (
I never said there is no work-around. I just find that it could be painful for a developer to understand what is happening when the The way the
Maybe the docs should mention that. |
PRs welcome 😺 |
Sorry guys, but I think that this is a wrong approach. If the developer cares about the The scope event system is modeled as a simplified DOM event system, so |
When a event is finished propagating through Scope hierarchy the event's `currentScope` property should be reset to `null` to avoid accidental use of this property in asynchronous event handlers. In the previous code, the event's property would contain a reference to the last Scope instance that was visited during the traversal, which is unlikely what the code trying to grab scope reference expects. BREAKING CHANGE: $broadcast and $emit will now reset the `currentScope` property of the event to null once the event finished propagating. If any code depends on asynchronously accessing thei `currentScope` property, it should be migrated to use `targetScope` instead. All of these cases should be considered programming bugs. Closes angular#7445"
When a event is finished propagating through Scope hierarchy the event's `currentScope` property should be reset to `null` to avoid accidental use of this property in asynchronous event handlers. In the previous code, the event's property would contain a reference to the last Scope instance that was visited during the traversal, which is unlikely what the code trying to grab scope reference expects. BREAKING CHANGE: $broadcast and $emit will now reset the `currentScope` property of the event to null once the event finished propagating. If any code depends on asynchronously accessing thei `currentScope` property, it should be migrated to use `targetScope` instead. All of these cases should be considered programming bugs. Closes angular#7445
I have a fix pr here: #7523 can someone please review? |
@IgorMinar This explanation suits me well, and your fix solves the ambiguous situations I describe. |
When a event is finished propagating through Scope hierarchy the event's `currentScope` property should be reset to `null` to avoid accidental use of this property in asynchronous event handlers. In the previous code, the event's property would contain a reference to the last Scope instance that was visited during the traversal, which is unlikely what the code trying to grab scope reference expects. BREAKING CHANGE: $broadcast and $emit will now reset the `currentScope` property of the event to null once the event finished propagating. If any code depends on asynchronously accessing thei `currentScope` property, it should be migrated to use `targetScope` instead. All of these cases should be considered programming bugs. Closes angular#7445 Closes angular#7523
Request Type: bug
How to reproduce: Any case of asynchronous access to
event.currentScope
:console.log(event)
then expand the object in console (Chrome / Firefox)$timeout
Plunker
Note : The tree of scopes must have a depth greater than 2 (see commit diff for rootScopeSpec.js).
Component(s): scope
Impact: small
Complexity: small
This issue is related to:
Detailed Description:
In the $broadcast method, the
event.currentScope
is initialized withcurrent
which is the iterator used by the scope traversal's loop.It caused problems when using asynchonously the event object : the
event.currentScope
is not the scope which listen the event. It's the last scope crossed by the traversal which has nothing to do with the event (or coincidentally has).Other Comments: