Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion goldens/size-tracking/aio-payloads.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@
"dark-theme": 93036
}
}
}
}
4 changes: 2 additions & 2 deletions goldens/size-tracking/integration-payloads.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"forms": {
"uncompressed": {
"runtime": 888,
"main": 160778,
"main": 161512,
"polyfills": 33772
}
},
Expand All @@ -61,4 +61,4 @@
"bundle": 1214857
}
}
}
}
2 changes: 1 addition & 1 deletion packages/core/src/render3/context_discovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {EMPTY_ARRAY} from '../util/empty';
import {assertLView} from './assert';
import {LContext} from './interfaces/context';
import {getLViewById, registerLView} from './interfaces/lview_tracking';
import {TNode, TNodeFlags} from './interfaces/node';
import {TNode} from './interfaces/node';
import {RElement, RNode} from './interfaces/renderer_dom';
import {isLView} from './interfaces/type_checks';
import {CONTEXT, HEADER_OFFSET, HOST, ID, LView, TVIEW} from './interfaces/view';
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/render3/instructions/listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ import {profiler, ProfilerEvent} from '../profiler';
import {getCurrentDirectiveDef, getCurrentTNode, getLView, getTView} from '../state';
import {getComponentLViewByIndex, getNativeByTNode, unwrapRNode} from '../util/view_utils';

import {getOrCreateLViewCleanup, getOrCreateTViewCleanup, handleError, loadComponentRenderer, markViewDirty} from './shared';
import {markViewDirty} from './mark_view_dirty';
import {getOrCreateLViewCleanup, getOrCreateTViewCleanup, handleError, loadComponentRenderer} from './shared';



Expand Down
36 changes: 36 additions & 0 deletions packages/core/src/render3/instructions/mark_view_dirty.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* @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 {isRootView} from '../interfaces/type_checks';
import {FLAGS, LView, LViewFlags} from '../interfaces/view';
import {getLViewParent} from '../util/view_traversal_utils';

/**
* Marks current view and all ancestors dirty.
*
* Returns the root view because it is found as a byproduct of marking the view tree
* dirty, and can be used by methods that consume markViewDirty() to easily schedule
* change detection. Otherwise, such methods would need to traverse up the view tree
* an additional time to get the root view and schedule a tick on it.
*
* @param lView The starting LView to mark dirty
* @returns the root LView
*/
export function markViewDirty(lView: LView): LView|null {
while (lView) {
lView[FLAGS] |= LViewFlags.Dirty;
const parent = getLViewParent(lView);
// Stop traversing up as soon as you find a root view that wasn't attached to any container
if (isRootView(lView) && !parent) {
return lView;
}
// continue otherwise
lView = parent!;
}
return null;
}
43 changes: 13 additions & 30 deletions packages/core/src/render3/instructions/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,12 @@ import {Renderer, RendererFactory} from '../interfaces/renderer';
import {RComment, RElement, RNode, RText} from '../interfaces/renderer_dom';
import {SanitizerFn} from '../interfaces/sanitization';
import {isComponentDef, isComponentHost, isContentQueryHost, isRootView} from '../interfaces/type_checks';
import {CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_COMPONENT_VIEW, DECLARATION_VIEW, EMBEDDED_VIEW_INJECTOR, FLAGS, HEADER_OFFSET, HOST, HostBindingOpCodes, HYDRATION, ID, InitPhaseState, INJECTOR, LView, LViewFlags, NEXT, ON_DESTROY_HOOKS, PARENT, RENDERER, RENDERER_FACTORY, SANITIZER, T_HOST, TData, TRANSPLANTED_VIEWS_TO_REFRESH, TVIEW, TView, TViewType} from '../interfaces/view';
import {CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_COMPONENT_VIEW, DECLARATION_VIEW, EMBEDDED_VIEW_INJECTOR, FLAGS, HEADER_OFFSET, HOST, HostBindingOpCodes, HYDRATION, ID, InitPhaseState, INJECTOR, LView, LViewFlags, NEXT, ON_DESTROY_HOOKS, PARENT, REACTIVE_HOST_BINDING_CONSUMER, REACTIVE_TEMPLATE_CONSUMER, RENDERER, RENDERER_FACTORY, SANITIZER, T_HOST, TData, TRANSPLANTED_VIEWS_TO_REFRESH, TVIEW, TView, TViewType} from '../interfaces/view';
import {assertPureTNodeType, assertTNodeType} from '../node_assert';
import {updateTextNode} from '../node_manipulation';
import {isInlineTemplate, isNodeMatchingSelectorList} from '../node_selector_matcher';
import {profiler, ProfilerEvent} from '../profiler';
import {commitLViewConsumerIfHasProducers, getReactiveLViewConsumer} from '../reactive_lview_consumer';
import {enterView, getBindingsEnabled, getCurrentDirectiveIndex, getCurrentParentTNode, getCurrentTNodePlaceholderOk, getSelectedIndex, isCurrentTNodeParent, isInCheckNoChangesMode, isInI18nBlock, leaveView, setBindingIndex, setBindingRootForHostBindings, setCurrentDirectiveIndex, setCurrentQueryIndex, setCurrentTNode, setIsInCheckNoChangesMode, setSelectedIndex} from '../state';
import {NO_CHANGE} from '../tokens';
import {mergeHostAttrs} from '../util/attrs_utils';
Expand All @@ -65,6 +66,7 @@ import {handleUnknownPropertyError, isPropertyValid, matchingSchemas} from './el
export function processHostBindingOpCodes(tView: TView, lView: LView): void {
const hostBindingOpCodes = tView.hostBindingOpCodes;
if (hostBindingOpCodes === null) return;
const consumer = getReactiveLViewConsumer(lView, REACTIVE_HOST_BINDING_CONSUMER);
try {
for (let i = 0; i < hostBindingOpCodes.length; i++) {
const opCode = hostBindingOpCodes[i] as number;
Expand All @@ -78,10 +80,12 @@ export function processHostBindingOpCodes(tView: TView, lView: LView): void {
const hostBindingFn = hostBindingOpCodes[++i] as HostBindingsFunction<any>;
setBindingRootForHostBindings(bindingRootIndx, directiveIdx);
const context = lView[directiveIdx];
hostBindingFn(RenderFlags.Update, context);
consumer.runInContext(hostBindingFn, RenderFlags.Update, context);
}
}
} finally {
lView[REACTIVE_HOST_BINDING_CONSUMER] === null &&
commitLViewConsumerIfHasProducers(lView, REACTIVE_HOST_BINDING_CONSUMER);
setSelectedIndex(-1);
}
}
Expand Down Expand Up @@ -146,6 +150,8 @@ export function createLView<T>(
lView[ID] = getUniqueLViewId();
lView[HYDRATION] = hydrationInfo;
lView[EMBEDDED_VIEW_INJECTOR as any] = embeddedViewInjector;
lView[REACTIVE_TEMPLATE_CONSUMER] = null;

ngDevMode &&
assertEqual(
tView.type == TViewType.Embedded ? parentLView !== null : true, true,
Expand Down Expand Up @@ -484,6 +490,7 @@ export function refreshView<T>(

function executeTemplate<T>(
tView: TView, lView: LView<T>, templateFn: ComponentTemplate<T>, rf: RenderFlags, context: T) {
const consumer = getReactiveLViewConsumer(lView, REACTIVE_TEMPLATE_CONSUMER);
const prevSelectedIndex = getSelectedIndex();
const isUpdatePhase = rf & RenderFlags.Update;
try {
Expand All @@ -497,8 +504,11 @@ function executeTemplate<T>(
const preHookType =
isUpdatePhase ? ProfilerEvent.TemplateUpdateStart : ProfilerEvent.TemplateCreateStart;
profiler(preHookType, context as unknown as {});
templateFn(rf, context);
consumer.runInContext(templateFn, rf, context);
} finally {
if (lView[REACTIVE_TEMPLATE_CONSUMER] === null) {
commitLViewConsumerIfHasProducers(lView, REACTIVE_TEMPLATE_CONSUMER);
}
setSelectedIndex(prevSelectedIndex);

const postHookType =
Expand Down Expand Up @@ -1792,33 +1802,6 @@ export function addToViewTree<T extends LView|LContainer>(lView: LView, lViewOrL
///////////////////////////////
//// Change detection
///////////////////////////////


/**
* Marks current view and all ancestors dirty.
*
* Returns the root view because it is found as a byproduct of marking the view tree
* dirty, and can be used by methods that consume markViewDirty() to easily schedule
* change detection. Otherwise, such methods would need to traverse up the view tree
* an additional time to get the root view and schedule a tick on it.
*
* @param lView The starting LView to mark dirty
* @returns the root LView
*/
export function markViewDirty(lView: LView): LView|null {
while (lView) {
lView[FLAGS] |= LViewFlags.Dirty;
const parent = getLViewParent(lView);
// Stop traversing up as soon as you find a root view that wasn't attached to any container
if (isRootView(lView) && !parent) {
return lView;
}
// continue otherwise
lView = parent!;
}
return null;
}

export function detectChangesInternal<T>(
tView: TView, lView: LView, context: T, notifyErrorHandler = true) {
const rendererFactory = lView[RENDERER_FACTORY];
Expand Down
19 changes: 17 additions & 2 deletions packages/core/src/render3/interfaces/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {ProviderToken} from '../../di/provider_token';
import {DehydratedView} from '../../hydration/interfaces';
import {SchemaMetadata} from '../../metadata/schema';
import {Sanitizer} from '../../sanitization/sanitizer';
import type {ReactiveLViewConsumer} from '../reactive_lview_consumer';

import {LContainer} from './container';
import {ComponentDef, ComponentTemplate, DirectiveDef, DirectiveDefList, HostBindingsFunction, PipeDef, PipeDefList, ViewQueriesFunction} from './definition';
Expand Down Expand Up @@ -51,15 +52,16 @@ export const ID = 20;
export const EMBEDDED_VIEW_INJECTOR = 21;
export const ON_DESTROY_HOOKS = 22;
export const HYDRATION = 23;

export const REACTIVE_TEMPLATE_CONSUMER = 24;
export const REACTIVE_HOST_BINDING_CONSUMER = 25;
/**
* Size of LView's header. Necessary to adjust for it when setting slots.
*
* IMPORTANT: `HEADER_OFFSET` should only be referred to the in the `ɵɵ*` instructions to translate
* instruction index into `LView` index. All other indexes should be in the `LView` index space and
* there should be no need to refer to `HEADER_OFFSET` anywhere else.
*/
export const HEADER_OFFSET = 24;
export const HEADER_OFFSET = 26;


// This interface replaces the real LView interface if it is an arg or a
Expand Down Expand Up @@ -343,6 +345,19 @@ export interface LView<T = unknown> extends Array<any> {
* entries.
*/
[ON_DESTROY_HOOKS]: Array<() => void>|null;

/**
* The `Consumer` for this `LView`'s template so that signal reads can be tracked.
*
* This is initially `null` and gets assigned a consumer after template execution
* if any signals were read.
*/
[REACTIVE_TEMPLATE_CONSUMER]: ReactiveLViewConsumer|null;

/**
* Same as REACTIVE_TEMPLATE_CONSUMER, but for the host bindings of the LView.
*/
[REACTIVE_HOST_BINDING_CONSUMER]: ReactiveLViewConsumer|null;
}

/** Flags associated with an LView (saved in LView[FLAGS]) */
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/render3/node_manipulation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {TElementNode, TIcuContainerNode, TNode, TNodeFlags, TNodeType, TProjecti
import {Renderer} from './interfaces/renderer';
import {RComment, RElement, RNode, RTemplate, RText} from './interfaces/renderer_dom';
import {isLContainer, isLView} from './interfaces/type_checks';
import {CHILD_HEAD, CLEANUP, DECLARATION_COMPONENT_VIEW, DECLARATION_LCONTAINER, DestroyHookData, FLAGS, HookData, HookFn, HOST, LView, LViewFlags, NEXT, ON_DESTROY_HOOKS, PARENT, QUERIES, RENDERER, T_HOST, TVIEW, TView, TViewType} from './interfaces/view';
import {CHILD_HEAD, CLEANUP, DECLARATION_COMPONENT_VIEW, DECLARATION_LCONTAINER, DestroyHookData, FLAGS, HookData, HookFn, HOST, LView, LViewFlags, NEXT, ON_DESTROY_HOOKS, PARENT, QUERIES, REACTIVE_HOST_BINDING_CONSUMER, REACTIVE_TEMPLATE_CONSUMER, RENDERER, T_HOST, TVIEW, TView, TViewType} from './interfaces/view';
import {assertTNodeType} from './node_assert';
import {profiler, ProfilerEvent} from './profiler';
import {setUpAttributes} from './util/attrs_utils';
Expand Down Expand Up @@ -378,6 +378,10 @@ export function detachView(lContainer: LContainer, removeIndex: number): LView|u
export function destroyLView(tView: TView, lView: LView) {
if (!(lView[FLAGS] & LViewFlags.Destroyed)) {
const renderer = lView[RENDERER];

lView[REACTIVE_TEMPLATE_CONSUMER]?.destroy();
lView[REACTIVE_HOST_BINDING_CONSUMER]?.destroy();

if (renderer.destroyNode) {
applyView(tView, lView, renderer, WalkTNodeTreeAction.Destroy, null, null);
}
Expand Down
92 changes: 92 additions & 0 deletions packages/core/src/render3/reactive_lview_consumer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* @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 {BaseConsumer, setActiveConsumer} from '../signals/src/graph';
import {assertDefined, assertEqual} from '../util/assert';

import {markViewDirty} from './instructions/mark_view_dirty';
import {ComponentTemplate, HostBindingsFunction, RenderFlags} from './interfaces/definition';
import {LView, REACTIVE_HOST_BINDING_CONSUMER, REACTIVE_TEMPLATE_CONSUMER} from './interfaces/view';

const NG_DEV_MODE = typeof ngDevMode === 'undefined' || ngDevMode;

export class ReactiveLViewConsumer extends BaseConsumer {
private _lView: LView|null = null;

set lView(lView: LView) {
NG_DEV_MODE && assertEqual(this._lView, null, 'Consumer already associated with a view.');
this._lView = lView;
}

override notify() {
NG_DEV_MODE &&
assertDefined(
this._lView,
'Updating a signal during template or host binding execution is not allowed.');
markViewDirty(this._lView!);
}

runInContext(
fn: HostBindingsFunction<unknown>|ComponentTemplate<unknown>, rf: RenderFlags,
ctx: unknown): void {
const prevConsumer = setActiveConsumer(this);
this.trackingVersion++;
try {
fn(rf, ctx);
} finally {
setActiveConsumer(prevConsumer);
}
}

destroy(): void {
// Incrementing the version means that every producer which tries to update this consumer will
// consider its record stale, and not notify.
this.trackingVersion++;
}
}

let currentConsumer: ReactiveLViewConsumer|null = null;

function getOrCreateCurrentLViewConsumer() {
currentConsumer ??= new ReactiveLViewConsumer();
return currentConsumer;
}

/**
* Create a new template consumer pointing at the specified LView.
* Sometimes, a previously created consumer may be reused, in order to save on allocations. In that
* case, the LView will be updated.
*/
export function getReactiveLViewConsumer(
lView: LView, slot: typeof REACTIVE_TEMPLATE_CONSUMER|typeof REACTIVE_HOST_BINDING_CONSUMER):
ReactiveLViewConsumer {
return lView[slot] ?? getOrCreateCurrentLViewConsumer();
}

/**
* Assigns the `currentTemplateContext` to its LView's `REACTIVE_CONSUMER` slot if there are tracked
* producers.
*
* The presence of producers means that a signal was read while the consumer was the active
* consumer.
*
* If no producers are present, we do not assign the current template context. This also means we
* can just reuse the template context for the next LView.
*/
export function commitLViewConsumerIfHasProducers(
lView: LView,
slot: typeof REACTIVE_TEMPLATE_CONSUMER|typeof REACTIVE_HOST_BINDING_CONSUMER): void {
const consumer = getOrCreateCurrentLViewConsumer();
if (consumer.producers.size === 0) {
return;
}

lView[slot] = currentConsumer;
consumer.lView = lView;
currentConsumer = new ReactiveLViewConsumer();
}
2 changes: 1 addition & 1 deletion packages/core/src/render3/util/change_detection_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import {assertDefined} from '../../util/assert';
import {getComponentViewByInstance} from '../context_discovery';
import {detectChanges} from '../instructions/change_detection';
import {markViewDirty} from '../instructions/shared';
import {markViewDirty} from '../instructions/mark_view_dirty';

import {getRootComponents} from './discovery_utils';

Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/render3/view_ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import {removeFromArray} from '../util/array_utils';
import {assertEqual} from '../util/assert';

import {collectNativeNodes} from './collect_native_nodes';
import {checkNoChangesInternal, detectChangesInternal, markViewDirty} from './instructions/shared';
import {markViewDirty} from './instructions/mark_view_dirty';
import {checkNoChangesInternal, detectChangesInternal} from './instructions/shared';
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';
Expand Down
15 changes: 14 additions & 1 deletion packages/core/src/signals/src/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {WeakRef} from './weak_ref';
import {newWeakRef, WeakRef} from './weak_ref';

/**
* Identifier for a `Producer`, which is a branded `number`.
Expand Down Expand Up @@ -275,6 +275,19 @@ export interface Consumer {
notify(): void;
}

/**
* A abstract implementation of `Consumer` that allows implementations to share much of the
* necessary boilerplate.
*/
export abstract class BaseConsumer implements Consumer {
readonly id = nextReactiveId();
readonly ref = newWeakRef(this);
readonly producers = new Map<ProducerId, Edge>();
trackingVersion = 0;

abstract notify(): void;
}

/**
* Function called to check the stale status of dependencies (producers) for a given consumer. This
* is a verification step before refreshing a given consumer: if none of the the dependencies
Expand Down
Loading