mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
refactor(core): decouple effects from change detection (#51049)
Previously effects were queued as they became dirty, and this queue was flushed at various checkpoints during the change detection cycle. The result was that change detection _was_ the effect runner, and without executing CD, effects would not execute. This leads a particular tradeoff: * effects are subject to unidirectional data flow (bad for dx) * effects don't cause a new round of CD (good/bad depending on use case) * effects can be used to implement control flow efficiently (desirable) This commit changes the scheduling mechanism. Effects are now scheduled via the microtask queue. This changes the tradeoffs: * effects are no longer limited by unidirectional data flow (easy dx) * effects registered in the Angular zone will trigger CD after they run (same as `Promise.resolve` really) * the public `effect()` type of effect probably isn't a good building block for our built-in control flow, and we'll need a new internal abstraction. As `effect()` is in developer preview, changing the execution timing is not considered breaking even though it may impact current users. PR Close #51049
This commit is contained in:
parent
e86d6dba27
commit
38c9f08c8d
27 changed files with 363 additions and 407 deletions
|
|
@ -20,6 +20,7 @@ import { PlatformRef } from '@angular/core';
|
|||
import { ProviderToken } from '@angular/core';
|
||||
import { SchemaMetadata } from '@angular/core';
|
||||
import { Type } from '@angular/core';
|
||||
import { ɵFlushableEffectRunner } from '@angular/core';
|
||||
|
||||
// @public
|
||||
export const __core_private_testing_placeholder__ = "";
|
||||
|
|
@ -29,7 +30,7 @@ export function async(fn: Function): (done: any) => any;
|
|||
|
||||
// @public
|
||||
export class ComponentFixture<T> {
|
||||
constructor(componentRef: ComponentRef<T>, ngZone: NgZone | null, _autoDetect: boolean);
|
||||
constructor(componentRef: ComponentRef<T>, ngZone: NgZone | null, effectRunner: ɵFlushableEffectRunner | null, _autoDetect: boolean);
|
||||
autoDetectChanges(autoDetect?: boolean): void;
|
||||
changeDetectorRef: ChangeDetectorRef;
|
||||
checkNoChanges(): void;
|
||||
|
|
@ -110,6 +111,7 @@ export interface TestBed {
|
|||
createComponent<T>(component: Type<T>): ComponentFixture<T>;
|
||||
// (undocumented)
|
||||
execute(tokens: any[], fn: Function, context?: any): any;
|
||||
flushEffects(): void;
|
||||
// @deprecated (undocumented)
|
||||
get<T>(token: ProviderToken<T>, notFoundValue?: T, flags?: InjectFlags): any;
|
||||
// @deprecated (undocumented)
|
||||
|
|
|
|||
|
|
@ -23,5 +23,8 @@ export {
|
|||
effect,
|
||||
EffectRef,
|
||||
EffectCleanupFn,
|
||||
EffectScheduler as ɵEffectScheduler,
|
||||
ZoneAwareQueueingScheduler as ɵZoneAwareQueueingScheduler,
|
||||
FlushableEffectRunner as ɵFlushableEffectRunner,
|
||||
} from './render3/reactivity/effect';
|
||||
// clang-format on
|
||||
// clang-format on
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ import {CONTEXT, HEADER_OFFSET, INJECTOR, LView, LViewEnvironment, LViewFlags, T
|
|||
import {MATH_ML_NAMESPACE, SVG_NAMESPACE} from './namespaces';
|
||||
import {createElementNode, setupStaticAttributes, writeDirectClass} from './node_manipulation';
|
||||
import {extractAttrsAndClassesFromSelector, stringifyCSSSelectorList} from './node_selector_matcher';
|
||||
import {EffectManager} from './reactivity/effect';
|
||||
import {EffectScheduler} from './reactivity/effect';
|
||||
import {enterView, getCurrentTNode, getLView, leaveView} from './state';
|
||||
import {computeStaticStyling} from './styling/static_styling';
|
||||
import {mergeHostAttrs, setUpAttributes} from './util/attrs_utils';
|
||||
|
|
@ -188,14 +188,13 @@ export class ComponentFactory<T> extends AbstractComponentFactory<T> {
|
|||
}
|
||||
const sanitizer = rootViewInjector.get(Sanitizer, null);
|
||||
|
||||
const effectManager = rootViewInjector.get(EffectManager, null);
|
||||
|
||||
const afterRenderEventManager = rootViewInjector.get(AfterRenderEventManager, null);
|
||||
|
||||
const environment: LViewEnvironment = {
|
||||
rendererFactory,
|
||||
sanitizer,
|
||||
effectManager,
|
||||
// We don't use inline effects (yet).
|
||||
inlineEffectRunner: null,
|
||||
afterRenderEventManager,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ export function detectChangesInternal<T>(
|
|||
|
||||
// One final flush of the effects queue to catch any effects created in `ngAfterViewInit` or
|
||||
// other post-order hooks.
|
||||
environment.effectManager?.flush();
|
||||
environment.inlineEffectRunner?.flush();
|
||||
|
||||
// Invoke all callbacks registered via `after*Render`, if needed.
|
||||
afterRenderEventManager?.end();
|
||||
|
|
@ -117,7 +117,7 @@ export function refreshView<T>(
|
|||
// since they were assigned. We do not want to execute lifecycle hooks in that mode.
|
||||
const isInCheckNoChangesPass = ngDevMode && isInCheckNoChangesMode();
|
||||
|
||||
!isInCheckNoChangesPass && lView[ENVIRONMENT].effectManager?.flush();
|
||||
!isInCheckNoChangesPass && lView[ENVIRONMENT].inlineEffectRunner?.flush();
|
||||
|
||||
enterView(lView);
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {DehydratedView} from '../../hydration/interfaces';
|
|||
import {SchemaMetadata} from '../../metadata/schema';
|
||||
import {Sanitizer} from '../../sanitization/sanitizer';
|
||||
import type {ReactiveLViewConsumer} from '../reactive_lview_consumer';
|
||||
import type {EffectManager} from '../reactivity/effect';
|
||||
import type {FlushableEffectRunner} from '../reactivity/effect';
|
||||
import type {AfterRenderEventManager} from '../after_render_hooks';
|
||||
|
||||
import {LContainer} from './container';
|
||||
|
|
@ -372,7 +372,7 @@ export interface LViewEnvironment {
|
|||
sanitizer: Sanitizer|null;
|
||||
|
||||
/** Container for reactivity system `effect`s. */
|
||||
effectManager: EffectManager|null;
|
||||
inlineEffectRunner: FlushableEffectRunner|null;
|
||||
|
||||
/** Container for after render hooks */
|
||||
afterRenderEventManager: AfterRenderEventManager|null;
|
||||
|
|
|
|||
|
|
@ -7,11 +7,14 @@
|
|||
*/
|
||||
|
||||
import {assertInInjectionContext} from '../../di/contextual';
|
||||
import {InjectionToken} from '../../di/injection_token';
|
||||
import {Injector} from '../../di/injector';
|
||||
import {inject} from '../../di/injector_compatibility';
|
||||
import {ɵɵdefineInjectable} from '../../di/interface/defs';
|
||||
import {ErrorHandler} from '../../error_handler';
|
||||
import {DestroyRef} from '../../linker/destroy_ref';
|
||||
import {Watch, watch} from '../../signals';
|
||||
import {isInNotificationPhase, watch, Watch, WatchCleanupFn, WatchCleanupRegisterFn} from '../../signals';
|
||||
|
||||
|
||||
/**
|
||||
* An effect can, optionally, register a cleanup function. If registered, the cleanup is executed
|
||||
|
|
@ -27,73 +30,201 @@ export type EffectCleanupFn = () => void;
|
|||
*/
|
||||
export type EffectCleanupRegisterFn = (cleanupFn: EffectCleanupFn) => void;
|
||||
|
||||
export interface SchedulableEffect {
|
||||
run(): void;
|
||||
creationZone: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks all effects registered within a given application and runs them via `flush`.
|
||||
* Not public API, which guarantees `EffectScheduler` only ever comes from the application root
|
||||
* injector.
|
||||
*/
|
||||
export class EffectManager {
|
||||
private all = new Set<Watch>();
|
||||
private queue = new Map<Watch, Zone|null>();
|
||||
export const APP_EFFECT_SCHEDULER = new InjectionToken('', {
|
||||
providedIn: 'root',
|
||||
factory: () => inject(EffectScheduler),
|
||||
});
|
||||
|
||||
create(
|
||||
effectFn: (onCleanup: (cleanupFn: EffectCleanupFn) => void) => void,
|
||||
destroyRef: DestroyRef|null, allowSignalWrites: boolean): EffectRef {
|
||||
const zone = (typeof Zone === 'undefined') ? null : Zone.current;
|
||||
const w = watch(effectFn, (watch) => {
|
||||
if (!this.all.has(watch)) {
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* A scheduler which manages the execution of effects.
|
||||
*/
|
||||
export abstract class EffectScheduler {
|
||||
/**
|
||||
* Schedule the given effect to be executed at a later time.
|
||||
*
|
||||
* It is an error to attempt to execute any effects synchronously during a scheduling operation.
|
||||
*/
|
||||
abstract scheduleEffect(e: SchedulableEffect): void;
|
||||
|
||||
this.queue.set(watch, zone);
|
||||
}, allowSignalWrites);
|
||||
/** @nocollapse */
|
||||
static ɵprov = /** @pureOrBreakMyCode */ ɵɵdefineInjectable({
|
||||
token: EffectScheduler,
|
||||
providedIn: 'root',
|
||||
factory: () => new ZoneAwareMicrotaskScheduler(),
|
||||
});
|
||||
}
|
||||
|
||||
this.all.add(w);
|
||||
/**
|
||||
* Interface to an `EffectScheduler` capable of running scheduled effects synchronously.
|
||||
*/
|
||||
export interface FlushableEffectRunner {
|
||||
/**
|
||||
* Run any scheduled effects.
|
||||
*/
|
||||
flush(): void;
|
||||
}
|
||||
|
||||
// Effects start dirty.
|
||||
w.notify();
|
||||
/**
|
||||
* An `EffectScheduler` which is capable of queueing scheduled effects per-zone, and flushing them
|
||||
* as an explicit operation.
|
||||
*/
|
||||
export class ZoneAwareQueueingScheduler implements EffectScheduler, FlushableEffectRunner {
|
||||
private queuedEffectCount = 0;
|
||||
private queues = new Map<Zone|null, Set<SchedulableEffect>>();
|
||||
|
||||
let unregisterOnDestroy: (() => void)|undefined;
|
||||
scheduleEffect(handle: SchedulableEffect): void {
|
||||
const zone = handle.creationZone as Zone | null;
|
||||
if (!this.queues.has(zone)) {
|
||||
this.queues.set(zone, new Set());
|
||||
}
|
||||
|
||||
const destroy = () => {
|
||||
w.cleanup();
|
||||
unregisterOnDestroy?.();
|
||||
this.all.delete(w);
|
||||
this.queue.delete(w);
|
||||
};
|
||||
|
||||
unregisterOnDestroy = destroyRef?.onDestroy(destroy);
|
||||
|
||||
return {
|
||||
destroy,
|
||||
};
|
||||
}
|
||||
|
||||
flush(): void {
|
||||
if (this.queue.size === 0) {
|
||||
const queue = this.queues.get(zone)!;
|
||||
if (queue.has(handle)) {
|
||||
return;
|
||||
}
|
||||
this.queuedEffectCount++;
|
||||
queue.add(handle);
|
||||
}
|
||||
|
||||
for (const [watch, zone] of this.queue) {
|
||||
this.queue.delete(watch);
|
||||
if (zone) {
|
||||
zone.run(() => watch.run());
|
||||
} else {
|
||||
watch.run();
|
||||
/**
|
||||
* Run all scheduled effects.
|
||||
*
|
||||
* Execution order of effects within the same zone is guaranteed to be FIFO, but there is no
|
||||
* ordering guarantee between effects scheduled in different zones.
|
||||
*/
|
||||
flush(): void {
|
||||
while (this.queuedEffectCount > 0) {
|
||||
for (const [zone, queue] of this.queues) {
|
||||
// `zone` here must be defined.
|
||||
if (zone === null) {
|
||||
this.flushQueue(queue);
|
||||
} else {
|
||||
zone.run(() => this.flushQueue(queue));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get isQueueEmpty(): boolean {
|
||||
return this.queue.size === 0;
|
||||
private flushQueue(queue: Set<SchedulableEffect>): void {
|
||||
for (const handle of queue) {
|
||||
queue.delete(handle);
|
||||
this.queuedEffectCount--;
|
||||
|
||||
// TODO: what happens if this throws an error?
|
||||
handle.run();
|
||||
}
|
||||
}
|
||||
|
||||
/** @nocollapse */
|
||||
static ɵprov = /** @pureOrBreakMyCode */ ɵɵdefineInjectable({
|
||||
token: EffectManager,
|
||||
token: ZoneAwareQueueingScheduler,
|
||||
providedIn: 'root',
|
||||
factory: () => new EffectManager(),
|
||||
factory: () => new ZoneAwareQueueingScheduler(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper around `ZoneAwareQueueingScheduler` that schedules flushing via the microtask queue
|
||||
* when.
|
||||
*/
|
||||
export class ZoneAwareMicrotaskScheduler implements EffectScheduler {
|
||||
private hasQueuedFlush = false;
|
||||
private delegate = new ZoneAwareQueueingScheduler();
|
||||
private flushTask = () => {
|
||||
// Leave `hasQueuedFlush` as `true` so we don't queue another microtask if more effects are
|
||||
// scheduled during flushing. The flush of the `ZoneAwareQueueingScheduler` delegate is
|
||||
// guaranteed to empty the queue.
|
||||
this.delegate.flush();
|
||||
this.hasQueuedFlush = false;
|
||||
|
||||
// This is a variable initialization, not a method.
|
||||
// tslint:disable-next-line:semicolon
|
||||
};
|
||||
|
||||
scheduleEffect(handle: SchedulableEffect): void {
|
||||
this.delegate.scheduleEffect(handle);
|
||||
|
||||
if (!this.hasQueuedFlush) {
|
||||
queueMicrotask(this.flushTask);
|
||||
this.hasQueuedFlush = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Core reactive node for an Angular effect.
|
||||
*
|
||||
* `EffectHandle` combines the reactive graph's `Watch` base node for effects with the framework's
|
||||
* scheduling abstraction (`EffectScheduler`) as well as automatic cleanup via `DestroyRef` if
|
||||
* available/requested.
|
||||
*/
|
||||
class EffectHandle implements EffectRef, SchedulableEffect {
|
||||
private alive = true;
|
||||
unregisterOnDestroy: (() => void)|undefined;
|
||||
protected watcher: Watch;
|
||||
|
||||
constructor(
|
||||
private scheduler: EffectScheduler,
|
||||
private effectFn: (onCleanup: EffectCleanupRegisterFn) => void,
|
||||
public creationZone: Zone|null, destroyRef: DestroyRef|null,
|
||||
private errorHandler: ErrorHandler|null, allowSignalWrites: boolean) {
|
||||
this.watcher =
|
||||
watch((onCleanup) => this.runEffect(onCleanup), () => this.schedule(), allowSignalWrites);
|
||||
this.unregisterOnDestroy = destroyRef?.onDestroy(() => this.destroy());
|
||||
}
|
||||
|
||||
private runEffect(onCleanup: WatchCleanupRegisterFn): void {
|
||||
if (!this.alive) {
|
||||
// Running a destroyed effect is a no-op.
|
||||
return;
|
||||
}
|
||||
if (ngDevMode && isInNotificationPhase()) {
|
||||
throw new Error(`Schedulers cannot synchronously execute effects while scheduling.`);
|
||||
}
|
||||
|
||||
try {
|
||||
this.effectFn(onCleanup);
|
||||
} catch (err) {
|
||||
this.errorHandler?.handleError(err);
|
||||
}
|
||||
}
|
||||
|
||||
run(): void {
|
||||
this.watcher.run();
|
||||
}
|
||||
|
||||
private schedule(): void {
|
||||
if (!this.alive) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.scheduler.scheduleEffect(this);
|
||||
}
|
||||
|
||||
notify(): void {
|
||||
this.watcher.notify();
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.alive = false;
|
||||
|
||||
this.watcher.cleanup();
|
||||
this.unregisterOnDestroy?.();
|
||||
|
||||
// Note: if the effect is currently scheduled, it's not un-scheduled, and so the scheduler will
|
||||
// retain a reference to it. Attempting to execute it will be a no-op.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A global reactive effect, which can be manually destroyed.
|
||||
*
|
||||
|
|
@ -147,7 +278,16 @@ export function effect(
|
|||
options?: CreateEffectOptions): EffectRef {
|
||||
!options?.injector && assertInInjectionContext(effect);
|
||||
const injector = options?.injector ?? inject(Injector);
|
||||
const effectManager = injector.get(EffectManager);
|
||||
const errorHandler = injector.get(ErrorHandler, null, {optional: true});
|
||||
const destroyRef = options?.manualCleanup !== true ? injector.get(DestroyRef) : null;
|
||||
return effectManager.create(effectFn, destroyRef, !!options?.allowSignalWrites);
|
||||
|
||||
const handle = new EffectHandle(
|
||||
injector.get(APP_EFFECT_SCHEDULER), effectFn,
|
||||
(typeof Zone === 'undefined') ? null : Zone.current, destroyRef, errorHandler,
|
||||
options?.allowSignalWrites ?? false);
|
||||
|
||||
// Effects start dirty.
|
||||
handle.notify();
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@
|
|||
export {defaultEquals, isSignal, Signal, SIGNAL, ValueEqualityFn} from './src/api';
|
||||
export {computed, CreateComputedOptions} from './src/computed';
|
||||
export {setThrowInvalidWriteToSignalError} from './src/errors';
|
||||
export {consumerAfterComputation, consumerBeforeComputation, consumerDestroy, producerAccessed, producerNotifyConsumers, producerUpdatesAllowed, producerUpdateValueVersion, REACTIVE_NODE, ReactiveNode, setActiveConsumer} from './src/graph';
|
||||
export {consumerAfterComputation, consumerBeforeComputation, consumerDestroy, isInNotificationPhase, producerAccessed, producerNotifyConsumers, producerUpdatesAllowed, producerUpdateValueVersion, REACTIVE_NODE, ReactiveNode, setActiveConsumer} from './src/graph';
|
||||
export {CreateSignalOptions, setPostSignalSetFn, signal, WritableSignal} from './src/signal';
|
||||
export {untracked} from './src/untracked';
|
||||
export {Watch, watch, WatchCleanupFn} from './src/watch';
|
||||
export {Watch, watch, WatchCleanupFn, WatchCleanupRegisterFn} from './src/watch';
|
||||
export {setAlternateWeakRefImpl} from './src/weak_ref';
|
||||
|
|
|
|||
|
|
@ -26,6 +26,10 @@ export function setActiveConsumer(consumer: ReactiveNode|null): ReactiveNode|nul
|
|||
return prev;
|
||||
}
|
||||
|
||||
export function isInNotificationPhase(): boolean {
|
||||
return inNotificationPhase;
|
||||
}
|
||||
|
||||
export const REACTIVE_NODE = {
|
||||
version: 0 as Version,
|
||||
dirty: false,
|
||||
|
|
|
|||
|
|
@ -197,9 +197,6 @@
|
|||
{
|
||||
"name": "EVENT_MANAGER_PLUGINS"
|
||||
},
|
||||
{
|
||||
"name": "EffectManager"
|
||||
},
|
||||
{
|
||||
"name": "ElementInstructionMap"
|
||||
},
|
||||
|
|
@ -338,9 +335,6 @@
|
|||
{
|
||||
"name": "NG_TEMPLATE_SELECTOR"
|
||||
},
|
||||
{
|
||||
"name": "NOOP_CLEANUP_FN"
|
||||
},
|
||||
{
|
||||
"name": "NOT_FOUND"
|
||||
},
|
||||
|
|
@ -422,9 +416,6 @@
|
|||
{
|
||||
"name": "REACTIVE_LVIEW_CONSUMER_NODE"
|
||||
},
|
||||
{
|
||||
"name": "REACTIVE_NODE"
|
||||
},
|
||||
{
|
||||
"name": "REMOVE_STYLES_ON_COMPONENT_DESTROY"
|
||||
},
|
||||
|
|
@ -545,9 +536,6 @@
|
|||
{
|
||||
"name": "ViewRef"
|
||||
},
|
||||
{
|
||||
"name": "WATCH_NODE"
|
||||
},
|
||||
{
|
||||
"name": "WebAnimationsPlayer"
|
||||
},
|
||||
|
|
@ -749,12 +737,6 @@
|
|||
{
|
||||
"name": "consumerIsLive"
|
||||
},
|
||||
{
|
||||
"name": "consumerMarkDirty"
|
||||
},
|
||||
{
|
||||
"name": "consumerPollProducersForChange"
|
||||
},
|
||||
{
|
||||
"name": "containsElement"
|
||||
},
|
||||
|
|
@ -1049,9 +1031,6 @@
|
|||
{
|
||||
"name": "importProvidersFrom"
|
||||
},
|
||||
{
|
||||
"name": "inNotificationPhase"
|
||||
},
|
||||
{
|
||||
"name": "includeViewProviders"
|
||||
},
|
||||
|
|
@ -1313,9 +1292,6 @@
|
|||
{
|
||||
"name": "producerRemoveLiveConsumerAtIndex"
|
||||
},
|
||||
{
|
||||
"name": "producerUpdateValueVersion"
|
||||
},
|
||||
{
|
||||
"name": "profiler"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -224,9 +224,6 @@
|
|||
{
|
||||
"name": "EVENT_MANAGER_PLUGINS"
|
||||
},
|
||||
{
|
||||
"name": "EffectManager"
|
||||
},
|
||||
{
|
||||
"name": "ElementInstructionMap"
|
||||
},
|
||||
|
|
@ -365,9 +362,6 @@
|
|||
{
|
||||
"name": "NG_TEMPLATE_SELECTOR"
|
||||
},
|
||||
{
|
||||
"name": "NOOP_CLEANUP_FN"
|
||||
},
|
||||
{
|
||||
"name": "NOT_FOUND"
|
||||
},
|
||||
|
|
@ -461,9 +455,6 @@
|
|||
{
|
||||
"name": "REACTIVE_LVIEW_CONSUMER_NODE"
|
||||
},
|
||||
{
|
||||
"name": "REACTIVE_NODE"
|
||||
},
|
||||
{
|
||||
"name": "REMOVE_STYLES_ON_COMPONENT_DESTROY"
|
||||
},
|
||||
|
|
@ -599,9 +590,6 @@
|
|||
{
|
||||
"name": "ViewRef"
|
||||
},
|
||||
{
|
||||
"name": "WATCH_NODE"
|
||||
},
|
||||
{
|
||||
"name": "WebAnimationsPlayer"
|
||||
},
|
||||
|
|
@ -806,12 +794,6 @@
|
|||
{
|
||||
"name": "consumerIsLive"
|
||||
},
|
||||
{
|
||||
"name": "consumerMarkDirty"
|
||||
},
|
||||
{
|
||||
"name": "consumerPollProducersForChange"
|
||||
},
|
||||
{
|
||||
"name": "containsElement"
|
||||
},
|
||||
|
|
@ -1115,9 +1097,6 @@
|
|||
{
|
||||
"name": "importProvidersFrom"
|
||||
},
|
||||
{
|
||||
"name": "inNotificationPhase"
|
||||
},
|
||||
{
|
||||
"name": "includeViewProviders"
|
||||
},
|
||||
|
|
@ -1388,9 +1367,6 @@
|
|||
{
|
||||
"name": "producerRemoveLiveConsumerAtIndex"
|
||||
},
|
||||
{
|
||||
"name": "producerUpdateValueVersion"
|
||||
},
|
||||
{
|
||||
"name": "profiler"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -134,9 +134,6 @@
|
|||
{
|
||||
"name": "EVENT_MANAGER_PLUGINS"
|
||||
},
|
||||
{
|
||||
"name": "EffectManager"
|
||||
},
|
||||
{
|
||||
"name": "ElementRef"
|
||||
},
|
||||
|
|
@ -269,9 +266,6 @@
|
|||
{
|
||||
"name": "NG_TEMPLATE_SELECTOR"
|
||||
},
|
||||
{
|
||||
"name": "NOOP_CLEANUP_FN"
|
||||
},
|
||||
{
|
||||
"name": "NOT_FOUND"
|
||||
},
|
||||
|
|
@ -347,9 +341,6 @@
|
|||
{
|
||||
"name": "REACTIVE_LVIEW_CONSUMER_NODE"
|
||||
},
|
||||
{
|
||||
"name": "REACTIVE_NODE"
|
||||
},
|
||||
{
|
||||
"name": "REMOVE_STYLES_ON_COMPONENT_DESTROY"
|
||||
},
|
||||
|
|
@ -458,9 +449,6 @@
|
|||
{
|
||||
"name": "ViewRef"
|
||||
},
|
||||
{
|
||||
"name": "WATCH_NODE"
|
||||
},
|
||||
{
|
||||
"name": "ZONE_IS_STABLE_OBSERVABLE"
|
||||
},
|
||||
|
|
@ -617,12 +605,6 @@
|
|||
{
|
||||
"name": "consumerIsLive"
|
||||
},
|
||||
{
|
||||
"name": "consumerMarkDirty"
|
||||
},
|
||||
{
|
||||
"name": "consumerPollProducersForChange"
|
||||
},
|
||||
{
|
||||
"name": "convertToBitFlags"
|
||||
},
|
||||
|
|
@ -881,9 +863,6 @@
|
|||
{
|
||||
"name": "importProvidersFrom"
|
||||
},
|
||||
{
|
||||
"name": "inNotificationPhase"
|
||||
},
|
||||
{
|
||||
"name": "includeViewProviders"
|
||||
},
|
||||
|
|
@ -1109,9 +1088,6 @@
|
|||
{
|
||||
"name": "producerRemoveLiveConsumerAtIndex"
|
||||
},
|
||||
{
|
||||
"name": "producerUpdateValueVersion"
|
||||
},
|
||||
{
|
||||
"name": "profiler"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -182,9 +182,6 @@
|
|||
{
|
||||
"name": "EVENT_MANAGER_PLUGINS"
|
||||
},
|
||||
{
|
||||
"name": "EffectManager"
|
||||
},
|
||||
{
|
||||
"name": "ElementRef"
|
||||
},
|
||||
|
|
@ -362,9 +359,6 @@
|
|||
{
|
||||
"name": "NG_VALUE_ACCESSOR"
|
||||
},
|
||||
{
|
||||
"name": "NOOP_CLEANUP_FN"
|
||||
},
|
||||
{
|
||||
"name": "NOT_FOUND"
|
||||
},
|
||||
|
|
@ -464,9 +458,6 @@
|
|||
{
|
||||
"name": "REACTIVE_LVIEW_CONSUMER_NODE"
|
||||
},
|
||||
{
|
||||
"name": "REACTIVE_NODE"
|
||||
},
|
||||
{
|
||||
"name": "REMOVE_STYLES_ON_COMPONENT_DESTROY"
|
||||
},
|
||||
|
|
@ -611,9 +602,6 @@
|
|||
{
|
||||
"name": "ViewRef"
|
||||
},
|
||||
{
|
||||
"name": "WATCH_NODE"
|
||||
},
|
||||
{
|
||||
"name": "ZONE_IS_STABLE_OBSERVABLE"
|
||||
},
|
||||
|
|
@ -833,12 +821,6 @@
|
|||
{
|
||||
"name": "consumerIsLive"
|
||||
},
|
||||
{
|
||||
"name": "consumerMarkDirty"
|
||||
},
|
||||
{
|
||||
"name": "consumerPollProducersForChange"
|
||||
},
|
||||
{
|
||||
"name": "controlNameBinding"
|
||||
},
|
||||
|
|
@ -1196,9 +1178,6 @@
|
|||
{
|
||||
"name": "importProvidersFrom"
|
||||
},
|
||||
{
|
||||
"name": "inNotificationPhase"
|
||||
},
|
||||
{
|
||||
"name": "includeViewProviders"
|
||||
},
|
||||
|
|
@ -1529,9 +1508,6 @@
|
|||
{
|
||||
"name": "producerRemoveLiveConsumerAtIndex"
|
||||
},
|
||||
{
|
||||
"name": "producerUpdateValueVersion"
|
||||
},
|
||||
{
|
||||
"name": "profiler"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -185,9 +185,6 @@
|
|||
{
|
||||
"name": "EVENT_MANAGER_PLUGINS"
|
||||
},
|
||||
{
|
||||
"name": "EffectManager"
|
||||
},
|
||||
{
|
||||
"name": "ElementRef"
|
||||
},
|
||||
|
|
@ -344,9 +341,6 @@
|
|||
{
|
||||
"name": "NG_VALUE_ACCESSOR"
|
||||
},
|
||||
{
|
||||
"name": "NOOP_CLEANUP_FN"
|
||||
},
|
||||
{
|
||||
"name": "NOT_FOUND"
|
||||
},
|
||||
|
|
@ -455,9 +449,6 @@
|
|||
{
|
||||
"name": "REACTIVE_LVIEW_CONSUMER_NODE"
|
||||
},
|
||||
{
|
||||
"name": "REACTIVE_NODE"
|
||||
},
|
||||
{
|
||||
"name": "REMOVE_STYLES_ON_COMPONENT_DESTROY"
|
||||
},
|
||||
|
|
@ -602,9 +593,6 @@
|
|||
{
|
||||
"name": "ViewRef"
|
||||
},
|
||||
{
|
||||
"name": "WATCH_NODE"
|
||||
},
|
||||
{
|
||||
"name": "ZONE_IS_STABLE_OBSERVABLE"
|
||||
},
|
||||
|
|
@ -806,12 +794,6 @@
|
|||
{
|
||||
"name": "consumerIsLive"
|
||||
},
|
||||
{
|
||||
"name": "consumerMarkDirty"
|
||||
},
|
||||
{
|
||||
"name": "consumerPollProducersForChange"
|
||||
},
|
||||
{
|
||||
"name": "controlPath"
|
||||
},
|
||||
|
|
@ -1157,9 +1139,6 @@
|
|||
{
|
||||
"name": "importProvidersFrom"
|
||||
},
|
||||
{
|
||||
"name": "inNotificationPhase"
|
||||
},
|
||||
{
|
||||
"name": "includeViewProviders"
|
||||
},
|
||||
|
|
@ -1493,9 +1472,6 @@
|
|||
{
|
||||
"name": "producerRemoveLiveConsumerAtIndex"
|
||||
},
|
||||
{
|
||||
"name": "producerUpdateValueVersion"
|
||||
},
|
||||
{
|
||||
"name": "profiler"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -83,9 +83,6 @@
|
|||
{
|
||||
"name": "ENVIRONMENT_INITIALIZER"
|
||||
},
|
||||
{
|
||||
"name": "EffectManager"
|
||||
},
|
||||
{
|
||||
"name": "ElementRef"
|
||||
},
|
||||
|
|
@ -191,9 +188,6 @@
|
|||
{
|
||||
"name": "NG_TEMPLATE_SELECTOR"
|
||||
},
|
||||
{
|
||||
"name": "NOOP_CLEANUP_FN"
|
||||
},
|
||||
{
|
||||
"name": "NOT_FOUND"
|
||||
},
|
||||
|
|
@ -266,9 +260,6 @@
|
|||
{
|
||||
"name": "REACTIVE_LVIEW_CONSUMER_NODE"
|
||||
},
|
||||
{
|
||||
"name": "REACTIVE_NODE"
|
||||
},
|
||||
{
|
||||
"name": "RefCountOperator"
|
||||
},
|
||||
|
|
@ -350,9 +341,6 @@
|
|||
{
|
||||
"name": "ViewRef"
|
||||
},
|
||||
{
|
||||
"name": "WATCH_NODE"
|
||||
},
|
||||
{
|
||||
"name": "ZONE_IS_STABLE_OBSERVABLE"
|
||||
},
|
||||
|
|
@ -479,12 +467,6 @@
|
|||
{
|
||||
"name": "consumerIsLive"
|
||||
},
|
||||
{
|
||||
"name": "consumerMarkDirty"
|
||||
},
|
||||
{
|
||||
"name": "consumerPollProducersForChange"
|
||||
},
|
||||
{
|
||||
"name": "convertToBitFlags"
|
||||
},
|
||||
|
|
@ -704,9 +686,6 @@
|
|||
{
|
||||
"name": "importProvidersFrom"
|
||||
},
|
||||
{
|
||||
"name": "inNotificationPhase"
|
||||
},
|
||||
{
|
||||
"name": "includeViewProviders"
|
||||
},
|
||||
|
|
@ -881,9 +860,6 @@
|
|||
{
|
||||
"name": "producerRemoveLiveConsumerAtIndex"
|
||||
},
|
||||
{
|
||||
"name": "producerUpdateValueVersion"
|
||||
},
|
||||
{
|
||||
"name": "profiler"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -140,9 +140,6 @@
|
|||
{
|
||||
"name": "EVENT_MANAGER_PLUGINS"
|
||||
},
|
||||
{
|
||||
"name": "EffectManager"
|
||||
},
|
||||
{
|
||||
"name": "ElementRef"
|
||||
},
|
||||
|
|
@ -311,9 +308,6 @@
|
|||
{
|
||||
"name": "NODES"
|
||||
},
|
||||
{
|
||||
"name": "NOOP_CLEANUP_FN"
|
||||
},
|
||||
{
|
||||
"name": "NOT_FOUND"
|
||||
},
|
||||
|
|
@ -383,9 +377,6 @@
|
|||
{
|
||||
"name": "REACTIVE_LVIEW_CONSUMER_NODE"
|
||||
},
|
||||
{
|
||||
"name": "REACTIVE_NODE"
|
||||
},
|
||||
{
|
||||
"name": "REFERENCE_NODE_BODY"
|
||||
},
|
||||
|
|
@ -512,9 +503,6 @@
|
|||
{
|
||||
"name": "ViewRef"
|
||||
},
|
||||
{
|
||||
"name": "WATCH_NODE"
|
||||
},
|
||||
{
|
||||
"name": "ZONE_IS_STABLE_OBSERVABLE"
|
||||
},
|
||||
|
|
@ -674,12 +662,6 @@
|
|||
{
|
||||
"name": "consumerIsLive"
|
||||
},
|
||||
{
|
||||
"name": "consumerMarkDirty"
|
||||
},
|
||||
{
|
||||
"name": "consumerPollProducersForChange"
|
||||
},
|
||||
{
|
||||
"name": "convertToBitFlags"
|
||||
},
|
||||
|
|
@ -947,9 +929,6 @@
|
|||
{
|
||||
"name": "importProvidersFrom"
|
||||
},
|
||||
{
|
||||
"name": "inNotificationPhase"
|
||||
},
|
||||
{
|
||||
"name": "includeViewProviders"
|
||||
},
|
||||
|
|
@ -1181,9 +1160,6 @@
|
|||
{
|
||||
"name": "producerRemoveLiveConsumerAtIndex"
|
||||
},
|
||||
{
|
||||
"name": "producerUpdateValueVersion"
|
||||
},
|
||||
{
|
||||
"name": "profiler"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -200,9 +200,6 @@
|
|||
{
|
||||
"name": "EVENT_MANAGER_PLUGINS"
|
||||
},
|
||||
{
|
||||
"name": "EffectManager"
|
||||
},
|
||||
{
|
||||
"name": "ElementRef"
|
||||
},
|
||||
|
|
@ -398,9 +395,6 @@
|
|||
{
|
||||
"name": "NONE"
|
||||
},
|
||||
{
|
||||
"name": "NOOP_CLEANUP_FN"
|
||||
},
|
||||
{
|
||||
"name": "NOT_FOUND"
|
||||
},
|
||||
|
|
@ -536,9 +530,6 @@
|
|||
{
|
||||
"name": "REACTIVE_LVIEW_CONSUMER_NODE"
|
||||
},
|
||||
{
|
||||
"name": "REACTIVE_NODE"
|
||||
},
|
||||
{
|
||||
"name": "REMOVE_STYLES_ON_COMPONENT_DESTROY"
|
||||
},
|
||||
|
|
@ -782,9 +773,6 @@
|
|||
{
|
||||
"name": "ViewRef"
|
||||
},
|
||||
{
|
||||
"name": "WATCH_NODE"
|
||||
},
|
||||
{
|
||||
"name": "XSS_SECURITY_URL"
|
||||
},
|
||||
|
|
@ -989,12 +977,6 @@
|
|||
{
|
||||
"name": "consumerIsLive"
|
||||
},
|
||||
{
|
||||
"name": "consumerMarkDirty"
|
||||
},
|
||||
{
|
||||
"name": "consumerPollProducersForChange"
|
||||
},
|
||||
{
|
||||
"name": "containsSegmentGroup"
|
||||
},
|
||||
|
|
@ -1460,9 +1442,6 @@
|
|||
{
|
||||
"name": "importProvidersFrom"
|
||||
},
|
||||
{
|
||||
"name": "inNotificationPhase"
|
||||
},
|
||||
{
|
||||
"name": "includeViewProviders"
|
||||
},
|
||||
|
|
@ -1802,9 +1781,6 @@
|
|||
{
|
||||
"name": "producerRemoveLiveConsumerAtIndex"
|
||||
},
|
||||
{
|
||||
"name": "producerUpdateValueVersion"
|
||||
},
|
||||
{
|
||||
"name": "profiler"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -113,9 +113,6 @@
|
|||
{
|
||||
"name": "EVENT_MANAGER_PLUGINS"
|
||||
},
|
||||
{
|
||||
"name": "EffectManager"
|
||||
},
|
||||
{
|
||||
"name": "ElementRef"
|
||||
},
|
||||
|
|
@ -245,9 +242,6 @@
|
|||
{
|
||||
"name": "NG_TEMPLATE_SELECTOR"
|
||||
},
|
||||
{
|
||||
"name": "NOOP_CLEANUP_FN"
|
||||
},
|
||||
{
|
||||
"name": "NOT_FOUND"
|
||||
},
|
||||
|
|
@ -311,9 +305,6 @@
|
|||
{
|
||||
"name": "REACTIVE_LVIEW_CONSUMER_NODE"
|
||||
},
|
||||
{
|
||||
"name": "REACTIVE_NODE"
|
||||
},
|
||||
{
|
||||
"name": "REMOVE_STYLES_ON_COMPONENT_DESTROY"
|
||||
},
|
||||
|
|
@ -410,9 +401,6 @@
|
|||
{
|
||||
"name": "ViewRef"
|
||||
},
|
||||
{
|
||||
"name": "WATCH_NODE"
|
||||
},
|
||||
{
|
||||
"name": "ZONE_IS_STABLE_OBSERVABLE"
|
||||
},
|
||||
|
|
@ -557,12 +545,6 @@
|
|||
{
|
||||
"name": "consumerIsLive"
|
||||
},
|
||||
{
|
||||
"name": "consumerMarkDirty"
|
||||
},
|
||||
{
|
||||
"name": "consumerPollProducersForChange"
|
||||
},
|
||||
{
|
||||
"name": "convertToBitFlags"
|
||||
},
|
||||
|
|
@ -788,9 +770,6 @@
|
|||
{
|
||||
"name": "importProvidersFrom"
|
||||
},
|
||||
{
|
||||
"name": "inNotificationPhase"
|
||||
},
|
||||
{
|
||||
"name": "includeViewProviders"
|
||||
},
|
||||
|
|
@ -974,9 +953,6 @@
|
|||
{
|
||||
"name": "producerRemoveLiveConsumerAtIndex"
|
||||
},
|
||||
{
|
||||
"name": "producerUpdateValueVersion"
|
||||
},
|
||||
{
|
||||
"name": "profiler"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -137,9 +137,6 @@
|
|||
{
|
||||
"name": "EVENT_MANAGER_PLUGINS"
|
||||
},
|
||||
{
|
||||
"name": "EffectManager"
|
||||
},
|
||||
{
|
||||
"name": "ElementRef"
|
||||
},
|
||||
|
|
@ -275,9 +272,6 @@
|
|||
{
|
||||
"name": "NG_TEMPLATE_SELECTOR"
|
||||
},
|
||||
{
|
||||
"name": "NOOP_CLEANUP_FN"
|
||||
},
|
||||
{
|
||||
"name": "NOT_FOUND"
|
||||
},
|
||||
|
|
@ -371,9 +365,6 @@
|
|||
{
|
||||
"name": "REACTIVE_LVIEW_CONSUMER_NODE"
|
||||
},
|
||||
{
|
||||
"name": "REACTIVE_NODE"
|
||||
},
|
||||
{
|
||||
"name": "REMOVE_STYLES_ON_COMPONENT_DESTROY"
|
||||
},
|
||||
|
|
@ -527,9 +518,6 @@
|
|||
{
|
||||
"name": "ViewRef"
|
||||
},
|
||||
{
|
||||
"name": "WATCH_NODE"
|
||||
},
|
||||
{
|
||||
"name": "ZONE_IS_STABLE_OBSERVABLE"
|
||||
},
|
||||
|
|
@ -722,12 +710,6 @@
|
|||
{
|
||||
"name": "consumerIsLive"
|
||||
},
|
||||
{
|
||||
"name": "consumerMarkDirty"
|
||||
},
|
||||
{
|
||||
"name": "consumerPollProducersForChange"
|
||||
},
|
||||
{
|
||||
"name": "convertToBitFlags"
|
||||
},
|
||||
|
|
@ -1049,9 +1031,6 @@
|
|||
{
|
||||
"name": "importProvidersFrom"
|
||||
},
|
||||
{
|
||||
"name": "inNotificationPhase"
|
||||
},
|
||||
{
|
||||
"name": "includeViewProviders"
|
||||
},
|
||||
|
|
@ -1319,9 +1298,6 @@
|
|||
{
|
||||
"name": "producerRemoveLiveConsumerAtIndex"
|
||||
},
|
||||
{
|
||||
"name": "producerUpdateValueVersion"
|
||||
},
|
||||
{
|
||||
"name": "profiler"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ describe('NgModule', () => {
|
|||
|
||||
const comp = cf.create(Injector.NULL);
|
||||
|
||||
return new ComponentFixture(comp, null, false);
|
||||
return new ComponentFixture(comp, null, null, false);
|
||||
}
|
||||
|
||||
describe('errors', () => {
|
||||
|
|
|
|||
|
|
@ -145,8 +145,8 @@ describe('di', () => {
|
|||
{}, LViewFlags.CheckAlways, null, null, {
|
||||
rendererFactory: {} as any,
|
||||
sanitizer: null,
|
||||
effectManager: null,
|
||||
afterRenderEventManager: null
|
||||
inlineEffectRunner: null,
|
||||
afterRenderEventManager: null,
|
||||
},
|
||||
{} as any, null, null, null);
|
||||
enterView(contentView);
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export function enterViewWithOneDiv() {
|
|||
const tNode = tView.firstChild = createTNode(tView, null!, TNodeType.Element, 0, 'div', null);
|
||||
const lView = createLView(
|
||||
null, tView, null, LViewFlags.CheckAlways, null, null,
|
||||
{rendererFactory, sanitizer: null, effectManager: null, afterRenderEventManager: null},
|
||||
{rendererFactory, sanitizer: null, inlineEffectRunner: null, afterRenderEventManager: null},
|
||||
renderer, null, null, null);
|
||||
lView[HEADER_OFFSET] = div;
|
||||
tView.data[HEADER_OFFSET] = tNode;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import {AsyncPipe} from '@angular/common';
|
||||
import {AfterViewInit, Component, ContentChildren, createComponent, destroyPlatform, effect, EnvironmentInjector, inject, Injector, Input, NgZone, OnChanges, QueryList, signal, SimpleChanges, ViewChild} from '@angular/core';
|
||||
import {AfterViewInit, Component, ContentChildren, createComponent, createEnvironmentInjector, destroyPlatform, effect, EnvironmentInjector, ErrorHandler, inject, Injector, Input, NgZone, OnChanges, QueryList, signal, SimpleChanges, ViewChild} from '@angular/core';
|
||||
import {toObservable} from '@angular/core/rxjs-interop';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {bootstrapApplication} from '@angular/platform-browser';
|
||||
|
|
@ -17,83 +17,6 @@ describe('effects', () => {
|
|||
beforeEach(destroyPlatform);
|
||||
afterEach(destroyPlatform);
|
||||
|
||||
it('created in the constructor should run during change detection',
|
||||
withBody('<test-cmp></test-cmp>', async () => {
|
||||
const log: string[] = [];
|
||||
@Component({
|
||||
selector: 'test-cmp',
|
||||
standalone: true,
|
||||
template: '',
|
||||
})
|
||||
class Cmp {
|
||||
constructor() {
|
||||
log.push('B');
|
||||
|
||||
effect(() => {
|
||||
log.push('E');
|
||||
});
|
||||
}
|
||||
|
||||
ngDoCheck() {
|
||||
log.push('C');
|
||||
}
|
||||
}
|
||||
|
||||
await bootstrapApplication(Cmp);
|
||||
|
||||
expect(log).toEqual([
|
||||
// B: component bootstrapped
|
||||
'B',
|
||||
// E: effect runs during change detection
|
||||
'E',
|
||||
// C: change detection was observed (first round from `ApplicationRef.tick` called
|
||||
// manually)
|
||||
'C',
|
||||
// C: second change detection happens (from zone becoming stable)
|
||||
'C',
|
||||
]);
|
||||
}));
|
||||
|
||||
it('created in ngOnInit should run during change detection',
|
||||
withBody('<test-cmp></test-cmp>', async () => {
|
||||
const log: string[] = [];
|
||||
@Component({
|
||||
selector: 'test-cmp',
|
||||
standalone: true,
|
||||
template: '',
|
||||
})
|
||||
class Cmp {
|
||||
private injector = inject(Injector);
|
||||
|
||||
constructor() {
|
||||
log.push('B');
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
effect(() => {
|
||||
log.push('E');
|
||||
}, {injector: this.injector});
|
||||
}
|
||||
|
||||
ngDoCheck() {
|
||||
log.push('C');
|
||||
}
|
||||
}
|
||||
|
||||
await bootstrapApplication(Cmp);
|
||||
|
||||
expect(log).toEqual([
|
||||
// B: component bootstrapped
|
||||
'B',
|
||||
// ngDoCheck runs before ngOnInit
|
||||
'C',
|
||||
// E: effect runs during change detection
|
||||
'E',
|
||||
// C: second change detection happens (from zone becoming stable)
|
||||
'C',
|
||||
]);
|
||||
}));
|
||||
|
||||
it('should run effects in the zone in which they get created',
|
||||
withBody('<test-cmp></test-cmp>', async () => {
|
||||
const log: string[] = [];
|
||||
|
|
@ -121,6 +44,28 @@ describe('effects', () => {
|
|||
expect(log).not.toEqual(['angular', 'angular']);
|
||||
}));
|
||||
|
||||
it('should propagate errors to the ErrorHandler', () => {
|
||||
let run = false;
|
||||
|
||||
let lastError: any = null;
|
||||
class FakeErrorHandler extends ErrorHandler {
|
||||
override handleError(error: any): void {
|
||||
lastError = error;
|
||||
}
|
||||
}
|
||||
|
||||
const injector = createEnvironmentInjector(
|
||||
[{provide: ErrorHandler, useFactory: () => new FakeErrorHandler()}],
|
||||
TestBed.inject(EnvironmentInjector));
|
||||
effect(() => {
|
||||
run = true;
|
||||
throw new Error('fail!');
|
||||
}, {injector});
|
||||
expect(() => TestBed.flushEffects()).not.toThrow();
|
||||
expect(run).toBeTrue();
|
||||
expect(lastError.message).toBe('fail!');
|
||||
});
|
||||
|
||||
it('should run effect cleanup function on destroy', async () => {
|
||||
let counterLog: number[] = [];
|
||||
let cleanupCount = 0;
|
||||
|
|
@ -179,6 +124,11 @@ describe('effects', () => {
|
|||
const fixture = TestBed.createComponent(Cmp);
|
||||
fixture.detectChanges();
|
||||
|
||||
// Effects don't run during change detection.
|
||||
expect(didRun).toBeFalse();
|
||||
|
||||
TestBed.flushEffects();
|
||||
|
||||
expect(didRun).toBeTrue();
|
||||
});
|
||||
|
||||
|
|
@ -201,24 +151,13 @@ describe('effects', () => {
|
|||
await bootstrapApplication(Cmp);
|
||||
}));
|
||||
|
||||
it('should allow writing to signals within effects when option set',
|
||||
withBody('<test-cmp></test-cmp>', async () => {
|
||||
@Component({
|
||||
selector: 'test-cmp',
|
||||
standalone: true,
|
||||
template: '',
|
||||
})
|
||||
class Cmp {
|
||||
counter = signal(0);
|
||||
constructor() {
|
||||
effect(() => {
|
||||
expect(() => this.counter.set(1)).not.toThrow();
|
||||
}, {allowSignalWrites: true});
|
||||
}
|
||||
}
|
||||
it('should allow writing to signals within effects when option set', () => {
|
||||
const counter = signal(0);
|
||||
|
||||
await bootstrapApplication(Cmp);
|
||||
}));
|
||||
effect(() => counter.set(1), {allowSignalWrites: true, injector: TestBed.inject(Injector)});
|
||||
TestBed.flushEffects();
|
||||
expect(counter()).toBe(1);
|
||||
});
|
||||
|
||||
it('should allow writing to signals in ngOnChanges', () => {
|
||||
@Component({
|
||||
|
|
@ -339,7 +278,7 @@ describe('effects', () => {
|
|||
expect(fixture.nativeElement.textContent).toBe('1');
|
||||
});
|
||||
|
||||
it('should not execute query setters in the reactive context', () => {
|
||||
it('should not execute query setters in the reactive context', async () => {
|
||||
const state = signal('initial');
|
||||
|
||||
@Component({
|
||||
|
|
@ -382,14 +321,16 @@ describe('effects', () => {
|
|||
|
||||
const fixture = TestBed.createComponent(Cmp);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.noOfCmpCreated).toBe(1);
|
||||
|
||||
state.set('changed');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.noOfCmpCreated).toBe(1);
|
||||
});
|
||||
|
||||
it('should allow toObservable subscription in template (with async pipe)', () => {
|
||||
it('should allow toObservable subscription in template (with async pipe)', async () => {
|
||||
@Component({
|
||||
selector: 'test-cmp',
|
||||
standalone: true,
|
||||
|
|
@ -403,6 +344,8 @@ describe('effects', () => {
|
|||
const fixture = TestBed.createComponent(Cmp);
|
||||
expect(() => fixture.detectChanges(true)).not.toThrow();
|
||||
fixture.detectChanges();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.textContent).toBe('0');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
*/
|
||||
|
||||
import {Sanitizer, Type, ɵAfterRenderEventManager as AfterRenderEventManager} from '@angular/core';
|
||||
import {EffectManager} from '@angular/core/src/render3/reactivity/effect';
|
||||
import {stringifyElement} from '@angular/platform-browser/testing/src/browser_util';
|
||||
|
||||
import {extractDirectiveDef} from '../../src/render3/definition';
|
||||
|
|
@ -75,8 +74,8 @@ export class ViewFixture {
|
|||
null, hostTView, {}, LViewFlags.CheckAlways | LViewFlags.IsRoot, null, null, {
|
||||
rendererFactory,
|
||||
sanitizer: sanitizer || null,
|
||||
effectManager: new EffectManager(),
|
||||
afterRenderEventManager: new AfterRenderEventManager(),
|
||||
inlineEffectRunner: null,
|
||||
},
|
||||
hostRenderer, null, null, null);
|
||||
|
||||
|
|
|
|||
79
packages/core/test/test_bed_effect_spec.ts
Normal file
79
packages/core/test/test_bed_effect_spec.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {Component, effect, inject, Injector} from '@angular/core';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
|
||||
describe('effects in TestBed', () => {
|
||||
it('created in the constructor should run with detectChanges()', () => {
|
||||
const log: string[] = [];
|
||||
@Component({
|
||||
selector: 'test-cmp',
|
||||
standalone: true,
|
||||
template: '',
|
||||
})
|
||||
class Cmp {
|
||||
constructor() {
|
||||
log.push('Ctor');
|
||||
|
||||
effect(() => {
|
||||
log.push('Effect');
|
||||
});
|
||||
}
|
||||
|
||||
ngDoCheck() {
|
||||
log.push('DoCheck');
|
||||
}
|
||||
}
|
||||
|
||||
TestBed.createComponent(Cmp).detectChanges();
|
||||
|
||||
expect(log).toEqual([
|
||||
'Ctor',
|
||||
'Effect',
|
||||
'DoCheck',
|
||||
]);
|
||||
});
|
||||
|
||||
it('created in ngOnInit should not run with detectChanges()', () => {
|
||||
const log: string[] = [];
|
||||
@Component({
|
||||
selector: 'test-cmp',
|
||||
standalone: true,
|
||||
template: '',
|
||||
})
|
||||
class Cmp {
|
||||
private injector = inject(Injector);
|
||||
|
||||
constructor() {
|
||||
log.push('Ctor');
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
effect(() => {
|
||||
log.push('Effect');
|
||||
}, {injector: this.injector});
|
||||
}
|
||||
|
||||
ngDoCheck() {
|
||||
log.push('DoCheck');
|
||||
}
|
||||
}
|
||||
|
||||
TestBed.createComponent(Cmp).detectChanges();
|
||||
|
||||
expect(log).toEqual([
|
||||
// B: component bootstrapped
|
||||
'Ctor',
|
||||
// ngDoCheck runs before ngOnInit
|
||||
'DoCheck',
|
||||
]);
|
||||
|
||||
// effect should not have executed.
|
||||
});
|
||||
});
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ChangeDetectorRef, ComponentRef, DebugElement, ElementRef, getDebugNode, NgZone, RendererFactory2} from '@angular/core';
|
||||
import {ChangeDetectorRef, ComponentRef, DebugElement, ElementRef, getDebugNode, NgZone, RendererFactory2, ɵFlushableEffectRunner as FlushableEffectRunner} from '@angular/core';
|
||||
import {Subscription} from 'rxjs';
|
||||
|
||||
|
||||
|
|
@ -53,7 +53,7 @@ export class ComponentFixture<T> {
|
|||
|
||||
constructor(
|
||||
public componentRef: ComponentRef<T>, public ngZone: NgZone|null,
|
||||
private _autoDetect: boolean) {
|
||||
private effectRunner: FlushableEffectRunner|null, private _autoDetect: boolean) {
|
||||
this.changeDetectorRef = componentRef.changeDetectorRef;
|
||||
this.elementRef = componentRef.location;
|
||||
this.debugElement = <DebugElement>getDebugNode(this.elementRef.nativeElement);
|
||||
|
|
@ -121,6 +121,7 @@ export class ComponentFixture<T> {
|
|||
* Trigger a change detection cycle for the component.
|
||||
*/
|
||||
detectChanges(checkNoChanges: boolean = true): void {
|
||||
this.effectRunner?.flush();
|
||||
if (this.ngZone != null) {
|
||||
// Run the change detection inside the NgZone so that any async tasks as part of the change
|
||||
// detection are captured by the zone and can be waited for in isStable.
|
||||
|
|
|
|||
|
|
@ -36,7 +36,9 @@ import {
|
|||
ɵsetAllowDuplicateNgModuleIdsForTest as setAllowDuplicateNgModuleIdsForTest,
|
||||
ɵsetUnknownElementStrictMode as setUnknownElementStrictMode,
|
||||
ɵsetUnknownPropertyStrictMode as setUnknownPropertyStrictMode,
|
||||
ɵstringify as stringify} from '@angular/core';
|
||||
ɵstringify as stringify,
|
||||
ɵZoneAwareQueueingScheduler as ZoneAwareQueueingScheduler,
|
||||
} from '@angular/core';
|
||||
|
||||
/* clang-format on */
|
||||
|
||||
|
|
@ -139,6 +141,14 @@ export interface TestBed {
|
|||
overrideTemplateUsingTestingModule(component: Type<any>, template: string): TestBed;
|
||||
|
||||
createComponent<T>(component: Type<T>): ComponentFixture<T>;
|
||||
|
||||
|
||||
/**
|
||||
* Execute any pending effects.
|
||||
*
|
||||
* @developerPreview
|
||||
*/
|
||||
flushEffects(): void;
|
||||
}
|
||||
|
||||
let _nextRootElementId = 0;
|
||||
|
|
@ -363,6 +373,10 @@ export class TestBedImpl implements TestBed {
|
|||
return TestBedImpl.INSTANCE.ngModule;
|
||||
}
|
||||
|
||||
static flushEffects(): void {
|
||||
return TestBedImpl.INSTANCE.flushEffects();
|
||||
}
|
||||
|
||||
// Properties
|
||||
|
||||
platform: PlatformRef = null!;
|
||||
|
|
@ -613,7 +627,8 @@ export class TestBedImpl implements TestBed {
|
|||
const initComponent = () => {
|
||||
const componentRef =
|
||||
componentFactory.create(Injector.NULL, [], `#${rootElId}`, this.testModuleRef);
|
||||
return new ComponentFixture<any>(componentRef, ngZone, autoDetect);
|
||||
return new ComponentFixture<any>(
|
||||
componentRef, ngZone, this.inject(ZoneAwareQueueingScheduler, null), autoDetect);
|
||||
};
|
||||
const fixture = ngZone ? ngZone.run(initComponent) : initComponent();
|
||||
this._activeFixtures.push(fixture);
|
||||
|
|
@ -749,6 +764,15 @@ export class TestBedImpl implements TestBed {
|
|||
testRenderer.removeAllRootElements?.();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute any pending effects.
|
||||
*
|
||||
* @developerPreview
|
||||
*/
|
||||
flushEffects(): void {
|
||||
this.inject(ZoneAwareQueueingScheduler).flush();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
import {PlatformLocation} from '@angular/common';
|
||||
import {MockPlatformLocation} from '@angular/common/testing';
|
||||
import {APP_ID, createPlatformFactory, NgModule, PLATFORM_INITIALIZER, platformCore, provideZoneChangeDetection, StaticProvider} from '@angular/core';
|
||||
import {APP_ID, createPlatformFactory, NgModule, PLATFORM_INITIALIZER, platformCore, provideZoneChangeDetection, StaticProvider, ɵEffectScheduler as EffectScheduler, ɵZoneAwareQueueingScheduler as ZoneAwareQueueingScheduler} from '@angular/core';
|
||||
import {BrowserModule, ɵBrowserDomAdapter as BrowserDomAdapter} from '@angular/platform-browser';
|
||||
|
||||
function initBrowserTests() {
|
||||
|
|
@ -36,6 +36,8 @@ export const platformBrowserTesting =
|
|||
{provide: APP_ID, useValue: 'a'},
|
||||
provideZoneChangeDetection(),
|
||||
{provide: PlatformLocation, useClass: MockPlatformLocation},
|
||||
{provide: ZoneAwareQueueingScheduler},
|
||||
{provide: EffectScheduler, useExisting: ZoneAwareQueueingScheduler},
|
||||
]
|
||||
})
|
||||
export class BrowserTestingModule {
|
||||
|
|
|
|||
Loading…
Reference in a new issue