-
Notifications
You must be signed in to change notification settings - Fork 24.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
afterRender
hooks should support updating state
#54074
Conversation
Errors during change detection are terminal and we do not generally attempt to continue processing or recover after one occurs. This helps clean up the `tick` implementation with respect to running `afterRender` hooks.
This change updates the `checkNoChanges` pass to only run once after both view refresh and `afterRender` hooks execute rather than both before and after the hooks. The original motivation was to specifically ensure that the application was in a "clean state" before running the `afterRender` hooks and ensure that `afterRender` hooks don't "fix" `ExpressionChanged...` errors. This, however, adds to the complexity and cost of running change detection in dev mode. Instead, the `checkNoChanges` pass runs once we have done all render-related work and want to ensure that the application state has been synced to the DOM (without additional changes).
86b434e
to
4d20b4b
Compare
5a26275
to
8282645
Compare
It was always the intent to have `afterRender` hooks allow updating state, as long as those state updates specifically mark views for (re)check. This commit delivers that behavior. Developers can now use `afterRender` hooks to perform further updates within the same change detection cycle rather than needing to defer this work to another round (i.e. `queueMicrotask(() => <<updateState>>)`). This is an important change to support migrating from the `queueMicrotask`-style deferred updates, which are not entirely compatible with zoneless or event `NgZone` run coalescing. When using a microtask to update state after a render, it doesn't work with coalescing because the render may not have happened yet (coalescing and zoneless use `requestAnimationFrame` while the default for `NgZone` is effectively a microtask-based change detection scheduler). Second, when using a `microtask` _during_ change detection to defer updates to the next cycle, this can cause updates to be split across multiple frames with run coalescing and with zoneless (again, because of `requestAnimationFrame`/`macrotask` change detection scheduling).
8282645
to
130bf2a
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.
LGTM 👍
This change updates the `checkNoChanges` pass to only run once after both view refresh and `afterRender` hooks execute rather than both before and after the hooks. The original motivation was to specifically ensure that the application was in a "clean state" before running the `afterRender` hooks and ensure that `afterRender` hooks don't "fix" `ExpressionChanged...` errors. This, however, adds to the complexity and cost of running change detection in dev mode. Instead, the `checkNoChanges` pass runs once we have done all render-related work and want to ensure that the application state has been synced to the DOM (without additional changes). PR Close #54074
It was always the intent to have `afterRender` hooks allow updating state, as long as those state updates specifically mark views for (re)check. This commit delivers that behavior. Developers can now use `afterRender` hooks to perform further updates within the same change detection cycle rather than needing to defer this work to another round (i.e. `queueMicrotask(() => <<updateState>>)`). This is an important change to support migrating from the `queueMicrotask`-style deferred updates, which are not entirely compatible with zoneless or event `NgZone` run coalescing. When using a microtask to update state after a render, it doesn't work with coalescing because the render may not have happened yet (coalescing and zoneless use `requestAnimationFrame` while the default for `NgZone` is effectively a microtask-based change detection scheduler). Second, when using a `microtask` _during_ change detection to defer updates to the next cycle, this can cause updates to be split across multiple frames with run coalescing and with zoneless (again, because of `requestAnimationFrame`/`macrotask` change detection scheduling). PR Close #54074
This change updates the `checkNoChanges` pass to only run once after both view refresh and `afterRender` hooks execute rather than both before and after the hooks. The original motivation was to specifically ensure that the application was in a "clean state" before running the `afterRender` hooks and ensure that `afterRender` hooks don't "fix" `ExpressionChanged...` errors. This, however, adds to the complexity and cost of running change detection in dev mode. Instead, the `checkNoChanges` pass runs once we have done all render-related work and want to ensure that the application state has been synced to the DOM (without additional changes). PR Close #54074
It was always the intent to have `afterRender` hooks allow updating state, as long as those state updates specifically mark views for (re)check. This commit delivers that behavior. Developers can now use `afterRender` hooks to perform further updates within the same change detection cycle rather than needing to defer this work to another round (i.e. `queueMicrotask(() => <<updateState>>)`). This is an important change to support migrating from the `queueMicrotask`-style deferred updates, which are not entirely compatible with zoneless or event `NgZone` run coalescing. When using a microtask to update state after a render, it doesn't work with coalescing because the render may not have happened yet (coalescing and zoneless use `requestAnimationFrame` while the default for `NgZone` is effectively a microtask-based change detection scheduler). Second, when using a `microtask` _during_ change detection to defer updates to the next cycle, this can cause updates to be split across multiple frames with run coalescing and with zoneless (again, because of `requestAnimationFrame`/`macrotask` change detection scheduling). PR Close #54074
This PR was merged into the repository by commit 432afd1. |
…y reverting angular#54074 In some situations, calling `markForCheck` can result in an infinite loop in seemingly valid scenarios. When a transplanted view is inserted before its declaration, it gets refreshed in the retry loop of `detectChanges`. At this point, the `Dirty` flag has been cleared from all parents. Calling `markForCheck` marks the insertion tree up to the root `Dirty`. If the declaration is checked again as a result (i.e. because it has default change detection) and is reachable because its parent was marked `Dirty`, this can cause an infinite loop. The declaration is refreshed again, so the insertion is marked for refresh (again). We enter an infinite loop if the insertion tree always calls `markForCheck` for some reason (i.e. `{{createReplayObservable() | async}}`). While the case above does fall into an infinite loop, it also truly is a problem in the application. While it's not an infinite synchronous loop, the declaration and insertion are infinitely dirty and will be refreshed on every change detection round.
…y reverting angular#54074 In some situations, calling `markForCheck` can result in an infinite loop in seemingly valid scenarios. When a transplanted view is inserted before its declaration, it gets refreshed in the retry loop of `detectChanges`. At this point, the `Dirty` flag has been cleared from all parents. Calling `markForCheck` marks the insertion tree up to the root `Dirty`. If the declaration is checked again as a result (i.e. because it has default change detection) and is reachable because its parent was marked `Dirty`, this can cause an infinite loop. The declaration is refreshed again, so the insertion is marked for refresh (again). We enter an infinite loop if the insertion tree always calls `markForCheck` for some reason (i.e. `{{createReplayObservable() | async}}`). While the case above does fall into an infinite loop, it also truly is a problem in the application. While it's not an infinite synchronous loop, the declaration and insertion are infinitely dirty and will be refreshed on every change detection round. Usually `markForCheck` does not have this problem because the `Dirty` flag is not cleared until the very end of change detection. However, if the view did not already have the `Dirty` flag set, it is never cleared because we never entered view refresh. One solution to this problem could be to clear the `Dirty` flag even after skipping view refresh but traversing to children.
…y reverting angular#54074 In some situations, calling `markForCheck` can result in an infinite loop in seemingly valid scenarios. When a transplanted view is inserted before its declaration, it gets refreshed in the retry loop of `detectChanges`. At this point, the `Dirty` flag has been cleared from all parents. Calling `markForCheck` marks the insertion tree up to the root `Dirty`. If the declaration is checked again as a result (i.e. because it has default change detection) and is reachable because its parent was marked `Dirty`, this can cause an infinite loop. The declaration is refreshed again, so the insertion is marked for refresh (again). We enter an infinite loop if the insertion tree always calls `markForCheck` for some reason (i.e. `{{createReplayObservable() | async}}`). While the case above does fall into an infinite loop, it also truly is a problem in the application. While it's not an infinite synchronous loop, the declaration and insertion are infinitely dirty and will be refreshed on every change detection round. Usually `markForCheck` does not have this problem because the `Dirty` flag is not cleared until the very end of change detection. However, if the view did not already have the `Dirty` flag set, it is never cleared because we never entered view refresh. One solution to this problem could be to clear the `Dirty` flag even after skipping view refresh but traversing to children.
…y reverting #54074 (#54329) In some situations, calling `markForCheck` can result in an infinite loop in seemingly valid scenarios. When a transplanted view is inserted before its declaration, it gets refreshed in the retry loop of `detectChanges`. At this point, the `Dirty` flag has been cleared from all parents. Calling `markForCheck` marks the insertion tree up to the root `Dirty`. If the declaration is checked again as a result (i.e. because it has default change detection) and is reachable because its parent was marked `Dirty`, this can cause an infinite loop. The declaration is refreshed again, so the insertion is marked for refresh (again). We enter an infinite loop if the insertion tree always calls `markForCheck` for some reason (i.e. `{{createReplayObservable() | async}}`). While the case above does fall into an infinite loop, it also truly is a problem in the application. While it's not an infinite synchronous loop, the declaration and insertion are infinitely dirty and will be refreshed on every change detection round. Usually `markForCheck` does not have this problem because the `Dirty` flag is not cleared until the very end of change detection. However, if the view did not already have the `Dirty` flag set, it is never cleared because we never entered view refresh. One solution to this problem could be to clear the `Dirty` flag even after skipping view refresh but traversing to children. PR Close #54329
…y reverting #54074 (#54329) In some situations, calling `markForCheck` can result in an infinite loop in seemingly valid scenarios. When a transplanted view is inserted before its declaration, it gets refreshed in the retry loop of `detectChanges`. At this point, the `Dirty` flag has been cleared from all parents. Calling `markForCheck` marks the insertion tree up to the root `Dirty`. If the declaration is checked again as a result (i.e. because it has default change detection) and is reachable because its parent was marked `Dirty`, this can cause an infinite loop. The declaration is refreshed again, so the insertion is marked for refresh (again). We enter an infinite loop if the insertion tree always calls `markForCheck` for some reason (i.e. `{{createReplayObservable() | async}}`). While the case above does fall into an infinite loop, it also truly is a problem in the application. While it's not an infinite synchronous loop, the declaration and insertion are infinitely dirty and will be refreshed on every change detection round. Usually `markForCheck` does not have this problem because the `Dirty` flag is not cleared until the very end of change detection. However, if the view did not already have the `Dirty` flag set, it is never cleared because we never entered view refresh. One solution to this problem could be to clear the `Dirty` flag even after skipping view refresh but traversing to children. PR Close #54329
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |
It was always the intent to have
afterRender
hooks allow updatingstate, as long as those state updates specifically mark views for
(re)check. This commit delivers that behavior. Developers can now use
afterRender
hooks to perform further updates within the same changedetection cycle rather than needing to defer this work to another round
(i.e.
queueMicrotask(() => <<updateState>>)
). This is an importantchange to support migrating from the
queueMicrotask
-style deferredupdates, which are not entirely compatible with zoneless or event
NgZone
runcoalescing.
When using a microtask to update state after a render, it
doesn't work with coalescing because the render may not have happened
yet (coalescing and zoneless use
requestAnimationFrame
while thedefault for
NgZone
is effectively a microtask-based change detectionscheduler). Second, when using a
microtask
during change detection to deferupdates to the next cycle, this can cause updates to be split across
multiple frames with run coalescing and with zoneless (again, because of
requestAnimationFrame
/macrotask
change detection scheduling).