/
githubmirror
/
angular
Обзор
Документация
Войти
/
githubmirror
/
angular
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
packages/core/src/render3/reactivity/effect.ts
334 строки
10 KB
SkyZeroZx
docs(docs-infra): Add build-time validation for API and guide links using route manifest
08 июл 2026, 20:24
08 июл 2026, 20:24
c1829f6
Код
Авторство
О чём код?
/** * @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.dev/license */ import { SIGNAL, consumerDestroy, isInNotificationPhase, setActiveConsumer, BaseEffectNode, BASE_EFFECT_NODE, runEffect, } from '../../../primitives/signals'; import {FLAGS, LViewFlags, LView, EFFECTS} from '../interfaces/view'; import {markAncestorsForTraversal} from '../util/view_utils'; import {inject} from '../../di/injector_compatibility'; import {Injector} from '../../di/injector'; import {assertNotInReactiveContext} from './asserts'; import {assertInInjectionContext} from '../../di/contextual'; import {DestroyRef, NodeInjectorDestroyRef} from '../../linker/destroy_ref'; import {ViewContext} from '../view_context'; import { ChangeDetectionScheduler, NotificationSource, } from '../../change_detection/scheduling/zoneless_scheduling'; import {setIsRefreshingViews} from '../state'; import {EffectScheduler, SchedulableEffect} from './root_effect_scheduler'; import {emitEffectCreatedEvent, setInjectorProfilerContext} from '../debug/injector_profiler'; /** * A global reactive effect, which can be manually destroyed. * * @publicApi 20.0 * * @see [Destroying effects](guide/signals/effect#destroying-effects) */ export interface EffectRef { /** * Shut down the effect, removing it from any upcoming scheduled executions. */ destroy(): void; } export class EffectRefImpl implements EffectRef { [SIGNAL]: EffectNode; constructor(node: EffectNode) { this[SIGNAL] = node; } destroy(): void { this[SIGNAL].destroy(); } } /** * Options passed to the `effect` function. * * @publicApi 20.0 */ export interface CreateEffectOptions { /** * The `Injector` in which to create the effect. * * If this is not provided, the current [injection context](guide/di/dependency-injection-context) * will be used instead (via `inject`). */ injector?: Injector; /** * Whether the `effect` should require manual cleanup. * * If this is `false` (the default) the effect will automatically register itself to be cleaned up * with the current `DestroyRef`. * * If this is `true` and you want to use the effect outside an injection context, you still * need to provide an `Injector` to the effect. */ manualCleanup?: boolean; /** * @deprecated no longer required, signal writes are allowed by default. */ allowSignalWrites?: boolean; /** * A debug name for the effect. Used in Angular DevTools to identify the effect. */ debugName?: string; } /** * An effect can, optionally, register a cleanup function. If registered, the cleanup is executed * before the next effect run. The cleanup function makes it possible to "cancel" any work that the * previous effect run might have started. * * @see [Effect cleanup functions](guide/signals/effect#effect-cleanup-functions) * * @publicApi 20.0 */ export type EffectCleanupFn = () => void; /** * A callback passed to the effect function that makes it possible to register cleanup logic. * * @see [Effect cleanup functions](guide/signals/effect#effect-cleanup-functions) * * @publicApi 20.0 */ export type EffectCleanupRegisterFn = (cleanupFn: EffectCleanupFn) => void; /** * Registers an "effect" that will be scheduled & executed whenever the signals that it reads * changes. * * Angular has two different kinds of effect: component effects and root effects. Component effects * are created when `effect()` is called from a component, directive, or within a service of a * component/directive. Root effects are created when `effect()` is called from outside the * component tree, such as in a root service. * * The two effect types differ in their timing. Component effects run as a component lifecycle * event during Angular's synchronization (change detection) process, and can safely read input * signals or create/destroy views that depend on component state. Root effects run as microtasks * and have no connection to the component tree or change detection. * * `effect()` must be run in injection context, unless the `injector` option is manually specified. * * @see [Effects](guide/signals/effect#effects) * * @publicApi 20.0 */ export function effect( effectFn: (onCleanup: EffectCleanupRegisterFn) => void, options?: CreateEffectOptions, ): EffectRef { ngDevMode && assertNotInReactiveContext( effect, 'Call `effect` outside of a reactive context. For example, schedule the ' + 'effect inside the component constructor.', ); if (ngDevMode && !options?.injector) { assertInInjectionContext(effect); } if (ngDevMode && options?.allowSignalWrites !== undefined) { console.warn( `The 'allowSignalWrites' flag is deprecated and no longer impacts effect() (writes are always allowed)`, ); } const injector = options?.injector ?? inject(Injector); let destroyRef = options?.manualCleanup !== true ? injector.get(DestroyRef) : null; let node: EffectNode; const viewContext = injector.get(ViewContext, null, {optional: true}); const notifier = injector.get(ChangeDetectionScheduler); if (viewContext !== null) { // This effect was created in the context of a view, and will be associated with the view. node = createViewEffect(viewContext.view, notifier, effectFn); if (destroyRef instanceof NodeInjectorDestroyRef && destroyRef._lView === viewContext.view) { // The effect is being created in the same view as the `DestroyRef` references, so it will be // automatically destroyed without the need for an explicit `DestroyRef` registration. destroyRef = null; } } else { // This effect was created outside the context of a view, and will be scheduled independently. node = createRootEffect(effectFn, injector.get(EffectScheduler), notifier); } node.injector = injector; if (destroyRef !== null) { // If we need to register for cleanup, do that here. node.onDestroyFns = [destroyRef.onDestroy(() => node.destroy())]; } const effectRef = new EffectRefImpl(node); if (ngDevMode) { node.debugName = options?.debugName ?? ''; const prevInjectorProfilerContext = setInjectorProfilerContext({injector, token: null}); try { emitEffectCreatedEvent(effectRef); } finally { setInjectorProfilerContext(prevInjectorProfilerContext); } } return effectRef; } export interface EffectNode extends BaseEffectNode, SchedulableEffect { cleanupFns: EffectCleanupFn[] | undefined; injector: Injector; notifier: ChangeDetectionScheduler; onDestroyFns: (() => void)[] | null; } export interface ViewEffectNode extends EffectNode { view: LView; } export interface RootEffectNode extends EffectNode { scheduler: EffectScheduler; } export const EFFECT_NODE: Omit<EffectNode, 'fn' | 'destroy' | 'injector' | 'notifier'> = /* @__PURE__ */ (() => ({ ...BASE_EFFECT_NODE, cleanupFns: undefined, zone: null, onDestroyFns: null, run(this: EffectNode): void { if (ngDevMode && isInNotificationPhase()) { throw new Error(`Schedulers cannot synchronously execute watches while scheduling.`); } // We clear `setIsRefreshingViews` so that `markForCheck()` within the body of an effect will // cause CD to reach the component in question. const prevRefreshingViews = setIsRefreshingViews(false); try { runEffect(this); } finally { setIsRefreshingViews(prevRefreshingViews); } }, cleanup(this: EffectNode): void { if (!this.cleanupFns?.length) { return; } const prevConsumer = setActiveConsumer(null); try { // Attempt to run the cleanup functions. Regardless of failure or success, we consider // cleanup "completed" and clear the list for the next run of the effect. Note that an error // from the cleanup function will still crash the current run of the effect. while (this.cleanupFns.length) { this.cleanupFns.pop()!(); } } finally { this.cleanupFns = []; setActiveConsumer(prevConsumer); } }, }))(); export const ROOT_EFFECT_NODE: Omit<RootEffectNode, 'fn' | 'scheduler' | 'notifier' | 'injector'> = /* @__PURE__ */ (() => ({ ...EFFECT_NODE, consumerMarkedDirty(this: RootEffectNode) { this.scheduler.schedule(this); this.notifier.notify(NotificationSource.RootEffect); }, destroy(this: RootEffectNode) { consumerDestroy(this); if (this.onDestroyFns !== null) { for (const fn of this.onDestroyFns) { fn(); } } this.cleanup(); this.scheduler.remove(this); }, }))(); export const VIEW_EFFECT_NODE: Omit<ViewEffectNode, 'fn' | 'view' | 'injector' | 'notifier'> = /* @__PURE__ */ (() => ({ ...EFFECT_NODE, consumerMarkedDirty(this: ViewEffectNode): void { this.view[FLAGS] |= LViewFlags.HasChildViewsToRefresh; markAncestorsForTraversal(this.view); this.notifier.notify(NotificationSource.ViewEffect); }, destroy(this: ViewEffectNode): void { consumerDestroy(this); if (this.onDestroyFns !== null) { for (const fn of this.onDestroyFns) { fn(); } } this.cleanup(); this.view[EFFECTS]?.delete(this); }, }))(); export function createViewEffect( view: LView, notifier: ChangeDetectionScheduler, fn: (onCleanup: EffectCleanupRegisterFn) => void, ): ViewEffectNode { const node = Object.create(VIEW_EFFECT_NODE) as ViewEffectNode; node.view = view; node.zone = typeof Zone !== 'undefined' ? Zone.current : null; node.notifier = notifier; node.fn = createEffectFn(node, fn); view[EFFECTS] ??= new Set(); view[EFFECTS].add(node); node.consumerMarkedDirty(node); return node; } export function createRootEffect( fn: (onCleanup: EffectCleanupRegisterFn) => void, scheduler: EffectScheduler, notifier: ChangeDetectionScheduler, ): RootEffectNode { const node = Object.create(ROOT_EFFECT_NODE) as RootEffectNode; node.fn = createEffectFn(node, fn); node.scheduler = scheduler; node.notifier = notifier; node.zone = typeof Zone !== 'undefined' ? Zone.current : null; node.scheduler.add(node); node.notifier.notify(NotificationSource.RootEffect); return node; } function createEffectFn(node: EffectNode, fn: (onCleanup: EffectCleanupRegisterFn) => void) { return () => { fn((cleanupFn) => (node.cleanupFns ??= []).push(cleanupFn)); }; }