Skip to content

Commit

Permalink
refactor(core): Add scheduler abstraction and notify when necessary (#…
Browse files Browse the repository at this point in the history
…53499)

In order to provide a reasonable experience for Angular without Zones,
we need a mechanism to run change detection when we receive a change
notification. There are several existing APIs today that serve as the
change notification: `ChangeDetectorRef.markForCheck`, signal updates,
event listeners (since they mark the view dirty), and attaching a view to
either the `ApplicationRef` or `ChangeDetectorRef`. These operations
are now paired with a notification to the change detection scheduler.

The concrete implementation for this scheduler is still being designed.
However, this gives us a starting point to partner with teams to
experiment with what that might look like.

PR Close #53499
  • Loading branch information
atscott committed Dec 20, 2023
1 parent cc74ebf commit 8d58595
Show file tree
Hide file tree
Showing 26 changed files with 420 additions and 29 deletions.
2 changes: 1 addition & 1 deletion packages/core/src/application/create_application.ts
Expand Up @@ -8,7 +8,7 @@

import {Subscription} from 'rxjs';

import {provideZoneChangeDetection} from '../change_detection/scheduling';
import {provideZoneChangeDetection} from '../change_detection/scheduling/ng_zone_scheduling';
import {EnvironmentProviders, Provider, StaticProvider} from '../di/interface/provider';
import {EnvironmentInjector} from '../di/r3_injector';
import {ErrorHandler} from '../error_handler';
Expand Down
Expand Up @@ -8,13 +8,13 @@

import {Subscription} from 'rxjs';

import {ApplicationRef} from '../application/application_ref';
import {ENVIRONMENT_INITIALIZER, EnvironmentProviders, inject, Injectable, InjectionToken, makeEnvironmentProviders, StaticProvider} from '../di';
import {ErrorHandler, INTERNAL_APPLICATION_ERROR_HANDLER} from '../error_handler';
import {RuntimeError, RuntimeErrorCode} from '../errors';
import {PendingTasks} from '../pending_tasks';
import {NgZone} from '../zone';
import {InternalNgZoneOptions} from '../zone/ng_zone';
import {ApplicationRef} from '../../application/application_ref';
import {ENVIRONMENT_INITIALIZER, EnvironmentProviders, inject, Injectable, InjectionToken, makeEnvironmentProviders, StaticProvider} from '../../di';
import {ErrorHandler, INTERNAL_APPLICATION_ERROR_HANDLER} from '../../error_handler';
import {RuntimeError, RuntimeErrorCode} from '../../errors';
import {PendingTasks} from '../../pending_tasks';
import {NgZone} from '../../zone';
import {InternalNgZoneOptions} from '../../zone/ng_zone';

@Injectable({providedIn: 'root'})
export class NgZoneChangeDetectionScheduler {
Expand Down
@@ -0,0 +1,14 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

/**
* Injectable that is notified when an `LView` is made aware of changes to application state.
*/
export abstract class ChangeDetectionScheduler {
abstract notify(): void;
}
2 changes: 1 addition & 1 deletion packages/core/src/core.ts
Expand Up @@ -25,7 +25,7 @@ export * from './di';
export {BootstrapOptions, ApplicationRef, NgProbeToken, APP_BOOTSTRAP_LISTENER} from './application/application_ref';
export {PlatformRef} from './platform/platform_ref';
export {createPlatform, createPlatformFactory, assertPlatform, destroyPlatform, getPlatform} from './platform/platform';
export {provideZoneChangeDetection, NgZoneOptions} from './change_detection/scheduling';
export {provideZoneChangeDetection, NgZoneOptions} from './change_detection/scheduling/ng_zone_scheduling';
export {enableProdMode, isDevMode} from './util/is_dev_mode';
export {APP_ID, PACKAGE_ROOT_URL, PLATFORM_INITIALIZER, PLATFORM_ID, ANIMATION_MODULE_TYPE, CSP_NONCE} from './application/application_tokens';
export {APP_INITIALIZER, ApplicationInitStatus} from './application/application_init';
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/core_private_export.ts
Expand Up @@ -12,6 +12,7 @@ export {IMAGE_CONFIG as ɵIMAGE_CONFIG, IMAGE_CONFIG_DEFAULTS as ɵIMAGE_CONFIG_
export {internalCreateApplication as ɵinternalCreateApplication} from './application/create_application';
export {defaultIterableDiffers as ɵdefaultIterableDiffers, defaultKeyValueDiffers as ɵdefaultKeyValueDiffers} from './change_detection/change_detection';
export {getEnsureDirtyViewsAreAlwaysReachable as ɵgetEnsureDirtyViewsAreAlwaysReachable, setEnsureDirtyViewsAreAlwaysReachable as ɵsetEnsureDirtyViewsAreAlwaysReachable} from './change_detection/flags';
export {ChangeDetectionScheduler as ɵChangeDetectionScheduler} from './change_detection/scheduling/zoneless_scheduling';
export {Console as ɵConsole} from './console';
export {DeferBlockDetails as ɵDeferBlockDetails, getDeferBlocks as ɵgetDeferBlocks} from './defer/discovery';
export {renderDeferBlockState as ɵrenderDeferBlockState, triggerResourceLoading as ɵtriggerResourceLoading} from './defer/instructions';
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/platform/platform_ref.ts
Expand Up @@ -8,7 +8,7 @@

import {ApplicationInitStatus} from '../application/application_init';
import {_callAndReportToErrorHandler, ApplicationRef, BootstrapOptions, compileNgModuleFactory, optionsReducer, remove} from '../application/application_ref';
import {getNgZoneOptions, internalProvideZoneChangeDetection, PROVIDED_NG_ZONE} from '../change_detection/scheduling';
import {getNgZoneOptions, internalProvideZoneChangeDetection, PROVIDED_NG_ZONE} from '../change_detection/scheduling/ng_zone_scheduling';
import {Injectable, InjectionToken, Injector} from '../di';
import {ErrorHandler} from '../error_handler';
import {RuntimeError, RuntimeErrorCode} from '../errors';
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/render3/component_ref.ts
Expand Up @@ -7,6 +7,7 @@
*/

import {ChangeDetectorRef} from '../change_detection/change_detector_ref';
import {ChangeDetectionScheduler} from '../change_detection/scheduling/zoneless_scheduling';
import {Injector} from '../di/injector';
import {convertToBitFlags} from '../di/injector_compatibility';
import {InjectFlags, InjectOptions} from '../di/interface/injector';
Expand Down Expand Up @@ -201,13 +202,15 @@ export class ComponentFactory<T> extends AbstractComponentFactory<T> {
const sanitizer = rootViewInjector.get(Sanitizer, null);

const afterRenderEventManager = rootViewInjector.get(AfterRenderEventManager, null);
const changeDetectionScheduler = rootViewInjector.get(ChangeDetectionScheduler, null);

const environment: LViewEnvironment = {
rendererFactory,
sanitizer,
// We don't use inline effects (yet).
inlineEffectRunner: null,
afterRenderEventManager,
changeDetectionScheduler,
};

const hostRenderer = rendererFactory.createRenderer(null, this.componentDef);
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/render3/instructions/mark_view_dirty.ts
Expand Up @@ -7,7 +7,7 @@
*/

import {isRootView} from '../interfaces/type_checks';
import {FLAGS, LView, LViewFlags} from '../interfaces/view';
import {ENVIRONMENT, FLAGS, LView, LViewFlags} from '../interfaces/view';
import {getLViewParent} from '../util/view_traversal_utils';

/**
Expand All @@ -22,6 +22,7 @@ import {getLViewParent} from '../util/view_traversal_utils';
* @returns the root LView
*/
export function markViewDirty(lView: LView): LView|null {
lView[ENVIRONMENT].changeDetectionScheduler?.notify();
while (lView) {
lView[FLAGS] |= LViewFlags.Dirty;
const parent = getLViewParent(lView);
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/render3/interfaces/view.ts
Expand Up @@ -24,6 +24,7 @@ import {Renderer, RendererFactory} from './renderer';
import {RElement} from './renderer_dom';
import {TStylingKey, TStylingRange} from './styling';
import {TDeferBlockDetails} from '../../defer/interfaces';
import type {ChangeDetectionScheduler} from '../../change_detection/scheduling/zoneless_scheduling';



Expand Down Expand Up @@ -372,6 +373,9 @@ export interface LViewEnvironment {

/** Container for after render hooks */
afterRenderEventManager: AfterRenderEventManager|null;

/** Scheduler for change detection to notify when application state changes. */
changeDetectionScheduler: ChangeDetectionScheduler|null;
}

/** Flags associated with an LView (saved in LView[FLAGS]) */
Expand Down
25 changes: 14 additions & 11 deletions packages/core/src/render3/util/view_utils.ts
Expand Up @@ -14,7 +14,7 @@ import {LContainer, LContainerFlags, TYPE} from '../interfaces/container';
import {TConstants, TNode} from '../interfaces/node';
import {RNode} from '../interfaces/renderer_dom';
import {isLContainer, isLView} from '../interfaces/type_checks';
import {DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, LView, LViewFlags, ON_DESTROY_HOOKS, PARENT, PREORDER_HOOK_FLAGS, PreOrderHookFlags, REACTIVE_TEMPLATE_CONSUMER, TData, TView} from '../interfaces/view';
import {DECLARATION_VIEW, ENVIRONMENT, FLAGS, HEADER_OFFSET, HOST, LView, LViewFlags, ON_DESTROY_HOOKS, PARENT, PREORDER_HOOK_FLAGS, PreOrderHookFlags, REACTIVE_TEMPLATE_CONSUMER, TData, TView} from '../interfaces/view';



Expand Down Expand Up @@ -207,17 +207,19 @@ export function requiresRefreshOrTraversal(lView: LView) {
* parents above.
*/
export function updateAncestorTraversalFlagsOnAttach(lView: LView) {
// When we attach a view that's marked `Dirty`, we should ensure that it is reached during the
// next CD traversal so we add the `RefreshView` flag and mark ancestors accordingly.
if (lView[FLAGS] & LViewFlags.Dirty && getEnsureDirtyViewsAreAlwaysReachable()) {
lView[FLAGS] |= LViewFlags.RefreshView;
}

if (!requiresRefreshOrTraversal(lView)) {
return;
// TODO(atscott): Simplify if...else cases once getEnsureDirtyViewsAreAlwaysReachable is always
// `true`. When we attach a view that's marked `Dirty`, we should ensure that it is reached during
// the next CD traversal so we add the `RefreshView` flag and mark ancestors accordingly.
if (requiresRefreshOrTraversal(lView)) {
markAncestorsForTraversal(lView);
} else if (lView[FLAGS] & LViewFlags.Dirty) {
if (getEnsureDirtyViewsAreAlwaysReachable()) {
lView[FLAGS] |= LViewFlags.RefreshView;
markAncestorsForTraversal(lView);
} else {
lView[ENVIRONMENT].changeDetectionScheduler?.notify();
}
}

markAncestorsForTraversal(lView);
}

/**
Expand All @@ -228,6 +230,7 @@ export function updateAncestorTraversalFlagsOnAttach(lView: LView) {
* flag is already `true` or the `lView` is detached.
*/
export function markAncestorsForTraversal(lView: LView) {
lView[ENVIRONMENT].changeDetectionScheduler?.notify();
let parent = lView[PARENT];
while (parent !== null) {
// We stop adding markers to the ancestors once we reach one that already has the marker. This
Expand Down
5 changes: 3 additions & 2 deletions packages/core/src/render3/view_ref.ts
Expand Up @@ -17,9 +17,9 @@ import {checkNoChangesInternal, detectChangesInternal} from './instructions/chan
import {markViewDirty} from './instructions/mark_view_dirty';
import {CONTAINER_HEADER_OFFSET, VIEW_REFS} from './interfaces/container';
import {isLContainer} from './interfaces/type_checks';
import {CONTEXT, FLAGS, LView, LViewFlags, PARENT, TVIEW} from './interfaces/view';
import {CONTEXT, ENVIRONMENT, FLAGS, LView, LViewFlags, PARENT, TVIEW} from './interfaces/view';
import {destroyLView, detachView, detachViewFromDOM} from './node_manipulation';
import {storeLViewOnDestroy, updateAncestorTraversalFlagsOnAttach} from './util/view_utils';
import {markAncestorsForTraversal, storeLViewOnDestroy, updateAncestorTraversalFlagsOnAttach} from './util/view_utils';


// Needed due to tsickle downleveling where multiple `implements` with classes creates
Expand Down Expand Up @@ -326,5 +326,6 @@ export class ViewRef<T> implements EmbeddedViewRef<T>, ChangeDetectorRefInterfac
ngDevMode && 'This view is already attached to a ViewContainer!');
}
this._appRef = appRef;
updateAncestorTraversalFlagsOnAttach(this._lView);
}
}
Expand Up @@ -116,6 +116,9 @@
{
"name": "ChainedInjector"
},
{
"name": "ChangeDetectionScheduler"
},
{
"name": "ChangeDetectionStrategy"
},
Expand Down Expand Up @@ -1361,6 +1364,9 @@
{
"name": "unwrapRNode"
},
{
"name": "updateAncestorTraversalFlagsOnAttach"
},
{
"name": "updateMicroTaskStatus"
},
Expand Down
Expand Up @@ -137,6 +137,9 @@
{
"name": "ChainedInjector"
},
{
"name": "ChangeDetectionScheduler"
},
{
"name": "ChangeDetectionStrategy"
},
Expand Down Expand Up @@ -1433,6 +1436,9 @@
{
"name": "unwrapRNode"
},
{
"name": "updateAncestorTraversalFlagsOnAttach"
},
{
"name": "updateMicroTaskStatus"
},
Expand Down
Expand Up @@ -68,6 +68,9 @@
{
"name": "ChainedInjector"
},
{
"name": "ChangeDetectionScheduler"
},
{
"name": "ChangeDetectionStrategy"
},
Expand Down Expand Up @@ -1139,6 +1142,9 @@
{
"name": "unwrapRNode"
},
{
"name": "updateAncestorTraversalFlagsOnAttach"
},
{
"name": "updateMicroTaskStatus"
},
Expand Down
12 changes: 9 additions & 3 deletions packages/core/test/bundling/defer/bundle.golden_symbols.json
Expand Up @@ -71,6 +71,9 @@
{
"name": "ChainedInjector"
},
{
"name": "ChangeDetectionScheduler"
},
{
"name": "ChangeDetectionStrategy"
},
Expand Down Expand Up @@ -1577,6 +1580,9 @@
{
"name": "init_ng_zone"
},
{
"name": "init_ng_zone_scheduling"
},
{
"name": "init_node"
},
Expand Down Expand Up @@ -1712,9 +1718,6 @@
{
"name": "init_sanitizer"
},
{
"name": "init_scheduling"
},
{
"name": "init_schema"
},
Expand Down Expand Up @@ -1892,6 +1895,9 @@
{
"name": "init_zone"
},
{
"name": "init_zoneless_scheduling"
},
{
"name": "initializeDirectives"
},
Expand Down
Expand Up @@ -95,6 +95,9 @@
{
"name": "ChainedInjector"
},
{
"name": "ChangeDetectionScheduler"
},
{
"name": "ChangeDetectionStrategy"
},
Expand Down
Expand Up @@ -98,6 +98,9 @@
{
"name": "ChainedInjector"
},
{
"name": "ChangeDetectionScheduler"
},
{
"name": "ChangeDetectionStrategy"
},
Expand Down
Expand Up @@ -44,6 +44,9 @@
{
"name": "ChainedInjector"
},
{
"name": "ChangeDetectionScheduler"
},
{
"name": "ComponentFactory"
},
Expand Down Expand Up @@ -890,6 +893,9 @@
{
"name": "unwrapRNode"
},
{
"name": "updateAncestorTraversalFlagsOnAttach"
},
{
"name": "updateMicroTaskStatus"
},
Expand Down
Expand Up @@ -68,6 +68,9 @@
{
"name": "ChainedInjector"
},
{
"name": "ChangeDetectionScheduler"
},
{
"name": "ChangeDetectionStrategy"
},
Expand Down Expand Up @@ -1271,6 +1274,9 @@
{
"name": "unwrapRNode"
},
{
"name": "updateAncestorTraversalFlagsOnAttach"
},
{
"name": "updateMicroTaskStatus"
},
Expand Down
3 changes: 3 additions & 0 deletions packages/core/test/bundling/router/bundle.golden_symbols.json
Expand Up @@ -101,6 +101,9 @@
{
"name": "ChainedInjector"
},
{
"name": "ChangeDetectionScheduler"
},
{
"name": "ChangeDetectionStrategy"
},
Expand Down
Expand Up @@ -56,6 +56,9 @@
{
"name": "ChainedInjector"
},
{
"name": "ChangeDetectionScheduler"
},
{
"name": "ChangeDetectionStrategy"
},
Expand Down Expand Up @@ -992,6 +995,9 @@
{
"name": "unwrapRNode"
},
{
"name": "updateAncestorTraversalFlagsOnAttach"
},
{
"name": "updateMicroTaskStatus"
},
Expand Down
3 changes: 3 additions & 0 deletions packages/core/test/bundling/todo/bundle.golden_symbols.json
Expand Up @@ -68,6 +68,9 @@
{
"name": "ChainedInjector"
},
{
"name": "ChangeDetectionScheduler"
},
{
"name": "ChangeDetectionStrategy"
},
Expand Down

0 comments on commit 8d58595

Please sign in to comment.