Skip to content

Commit

Permalink
refactor(core): better organization of @defer runtime code
Browse files Browse the repository at this point in the history
This commit splits the `render3/instructions/defer.ts` file (that contained most of the runtime code) into smalle
r files that are easier to maintain.

There are no functional changes in this PR, just organizing code.
  • Loading branch information
AndrewKushnir committed Oct 10, 2023
1 parent da056a1 commit 659072f
Show file tree
Hide file tree
Showing 12 changed files with 836 additions and 755 deletions.
5 changes: 3 additions & 2 deletions packages/core/src/core_private_export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ export {ALLOW_MULTIPLE_PLATFORMS as ɵALLOW_MULTIPLE_PLATFORMS, internalCreateAp
export {ENABLED_SSR_FEATURES as ɵENABLED_SSR_FEATURES, IMAGE_CONFIG as ɵIMAGE_CONFIG, IMAGE_CONFIG_DEFAULTS as ɵIMAGE_CONFIG_DEFAULTS, ImageConfig as ɵImageConfig} from './application_tokens';
export {defaultIterableDiffers as ɵdefaultIterableDiffers, defaultKeyValueDiffers as ɵdefaultKeyValueDiffers} from './change_detection/change_detection';
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';
export {DeferBlockBehavior as ɵDeferBlockBehavior, DeferBlockConfig as ɵDeferBlockConfig, DeferBlockState as ɵDeferBlockState} from './defer/interfaces';
export {convertToBitFlags as ɵconvertToBitFlags, setCurrentInjector as ɵsetCurrentInjector} from './di/injector_compatibility';
export {getInjectableDef as ɵgetInjectableDef, ɵɵInjectableDeclaration, ɵɵInjectorDef} from './di/interface/defs';
export {InternalEnvironmentProviders as ɵInternalEnvironmentProviders, isEnvironmentProviders as ɵisEnvironmentProviders} from './di/interface/provider';
Expand All @@ -29,8 +32,6 @@ export {ComponentFactory as ɵComponentFactory} from './linker/component_factory
export {clearResolutionOfComponentResourcesQueue as ɵclearResolutionOfComponentResourcesQueue, isComponentDefPendingResolution as ɵisComponentDefPendingResolution, resolveComponentResources as ɵresolveComponentResources, restoreComponentResolutionQueue as ɵrestoreComponentResolutionQueue} from './metadata/resource_loading';
export {ReflectionCapabilities as ɵReflectionCapabilities} from './reflection/reflection_capabilities';
export {InjectorProfilerContext as ɵInjectorProfilerContext, setInjectorProfilerContext as ɵsetInjectorProfilerContext} from './render3/debug/injector_profiler';
export {DeferBlockDetails as ɵDeferBlockDetails, getDeferBlocks as ɵgetDeferBlocks, renderDeferBlockState as ɵrenderDeferBlockState, triggerResourceLoading as ɵtriggerResourceLoading} from './render3/instructions/defer';
export {DeferBlockBehavior as ɵDeferBlockBehavior, DeferBlockConfig as ɵDeferBlockConfig, DeferBlockState as ɵDeferBlockState} from './render3/interfaces/defer';
export {allowSanitizationBypassAndThrow as ɵallowSanitizationBypassAndThrow, BypassType as ɵBypassType, getSanitizationBypassType as ɵgetSanitizationBypassType, SafeHtml as ɵSafeHtml, SafeResourceUrl as ɵSafeResourceUrl, SafeScript as ɵSafeScript, SafeStyle as ɵSafeStyle, SafeUrl as ɵSafeUrl, SafeValue as ɵSafeValue, unwrapSafeValue as ɵunwrapSafeValue} from './sanitization/bypass';
export {_sanitizeHtml as ɵ_sanitizeHtml} from './sanitization/html_sanitizer';
export {_sanitizeUrl as ɵ_sanitizeUrl} from './sanitization/url_sanitizer';
Expand Down
77 changes: 77 additions & 0 deletions packages/core/src/defer/cleanup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*!
* @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 {Injector, ɵɵdefineInjectable} from '../di';

import {TDeferBlockDetails} from './interfaces';

/**
* Registers a cleanup function associated with a prefetching trigger
* of a given defer block.
*/
export function registerTDetailsCleanup(
injector: Injector, tDetails: TDeferBlockDetails, key: string, cleanupFn: VoidFunction) {
injector.get(DeferBlockCleanupManager).add(tDetails, key, cleanupFn);
}
/**
* Invokes all registered prefetch cleanup triggers
* and removes all cleanup functions afterwards.
*/
export function invokeTDetailsCleanup(injector: Injector, tDetails: TDeferBlockDetails) {
injector.get(DeferBlockCleanupManager).cleanup(tDetails);
}
/**
* Internal service to keep track of cleanup functions associated
* with defer blocks. This class is used to manage cleanup functions
* created for prefetching triggers.
*/
export class DeferBlockCleanupManager {
private blocks = new Map<TDeferBlockDetails, Map<string, VoidFunction[]>>();

add(tDetails: TDeferBlockDetails, key: string, callback: VoidFunction) {
if (!this.blocks.has(tDetails)) {
this.blocks.set(tDetails, new Map());
}
const block = this.blocks.get(tDetails)!;
if (!block.has(key)) {
block.set(key, []);
}
const callbacks = block.get(key)!;
callbacks.push(callback);
}

has(tDetails: TDeferBlockDetails, key: string): boolean {
return !!this.blocks.get(tDetails)?.has(key);
}

cleanup(tDetails: TDeferBlockDetails) {
const block = this.blocks.get(tDetails);
if (block) {
for (const callbacks of Object.values(block)) {
for (const callback of callbacks) {
callback();
}
}
this.blocks.delete(tDetails);
}
}

ngOnDestroy() {
for (const [block] of this.blocks) {
this.cleanup(block);
}
this.blocks.clear();
}

/** @nocollapse */
static ɵprov = /** @pureOrBreakMyCode */ ɵɵdefineInjectable({
token: DeferBlockCleanupManager,
providedIn: 'root',
factory: () => new DeferBlockCleanupManager(),
});
}
60 changes: 60 additions & 0 deletions packages/core/src/defer/discovery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* @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 {CONTAINER_HEADER_OFFSET, LContainer} from '../render3/interfaces/container';
import {TNode} from '../render3/interfaces/node';
import {isLContainer, isLView} from '../render3/interfaces/type_checks';
import {HEADER_OFFSET, LView, TVIEW} from '../render3/interfaces/view';

import {TDeferBlockDetails} from './interfaces';
import {getTDeferBlockDetails, isTDeferBlockDetails} from './utils';

/**
* Defer block instance for testing.
*/
export interface DeferBlockDetails {
lContainer: LContainer;
lView: LView;
tNode: TNode;
tDetails: TDeferBlockDetails;
}

/**
* Retrieves all defer blocks in a given LView.
*
* @param lView lView with defer blocks
* @param deferBlocks defer block aggregator array
*/
export function getDeferBlocks(lView: LView, deferBlocks: DeferBlockDetails[]) {
const tView = lView[TVIEW];
for (let i = HEADER_OFFSET; i < tView.bindingStartIndex; i++) {
if (isLContainer(lView[i])) {
const lContainer = lView[i];
// An LContainer may represent an instance of a defer block, in which case
// we store it as a result. Otherwise, keep iterating over LContainer views and
// look for defer blocks.
const isLast = i === tView.bindingStartIndex - 1;
if (!isLast) {
const tNode = tView.data[i] as TNode;
const tDetails = getTDeferBlockDetails(tView, tNode);
if (isTDeferBlockDetails(tDetails)) {
deferBlocks.push({lContainer, lView, tNode, tDetails});
// This LContainer represents a defer block, so we exit
// this iteration and don't inspect views in this LContainer.
continue;
}
}
for (let i = CONTAINER_HEADER_OFFSET; i < lContainer.length; i++) {
getDeferBlocks(lContainer[i] as LView, deferBlocks);
}
} else if (isLView(lView[i])) {
// This is a component, enter the `getDeferBlocks` recursively.
getDeferBlocks(lView[i], deferBlocks);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,18 @@
* found in the LICENSE file at https://angular.io/license
*/

import {inject, Injector, ɵɵdefineInjectable} from '../../di';
import {NgZone} from '../../zone';
import {inject, Injector, ɵɵdefineInjectable} from '../di';
import {afterRender} from '../render3/after_render_hooks';
import {assertLContainer, assertLView} from '../render3/assert';
import {CONTAINER_HEADER_OFFSET} from '../render3/interfaces/container';
import {TNode} from '../render3/interfaces/node';
import {FLAGS, HEADER_OFFSET, INJECTOR, LView, LViewFlags} from '../render3/interfaces/view';
import {getNativeByIndex, removeLViewOnDestroy, storeLViewOnDestroy, walkUpViews} from '../render3/util/view_utils';
import {assertElement, assertEqual} from '../util/assert';
import {NgZone} from '../zone';

import {DEFER_BLOCK_STATE, DeferBlockInternalState, DeferBlockState} from './interfaces';
import {getLDeferBlockDetails} from './utils';

/** Configuration object used to register passive and capturing events. */
const eventListenerOptions: AddEventListenerOptions = {
Expand Down Expand Up @@ -205,3 +215,117 @@ class DeferIntersectionManager {
}
}
}

/**
* Helper function to get the LView in which a deferred block's trigger is rendered.
* @param deferredHostLView LView in which the deferred block is defined.
* @param deferredTNode TNode defining the deferred block.
* @param walkUpTimes Number of times to go up in the view hierarchy to find the trigger's view.
* A negative value means that the trigger is inside the block's placeholder, while an undefined
* value means that the trigger is in the same LView as the deferred block.
*/
export function getTriggerLView(
deferredHostLView: LView, deferredTNode: TNode, walkUpTimes: number|undefined): LView|null {
// The trigger is in the same view, we don't need to traverse.
if (walkUpTimes == null) {
return deferredHostLView;
}

// A positive value or zero means that the trigger is in a parent view.
if (walkUpTimes >= 0) {
return walkUpViews(walkUpTimes, deferredHostLView);
}

// If the value is negative, it means that the trigger is inside the placeholder.
const deferredContainer = deferredHostLView[deferredTNode.index];
ngDevMode && assertLContainer(deferredContainer);
const triggerLView = deferredContainer[CONTAINER_HEADER_OFFSET] ?? null;

// We need to null check, because the placeholder might not have been rendered yet.
if (ngDevMode && triggerLView !== null) {
const lDetails = getLDeferBlockDetails(deferredHostLView, deferredTNode);
const renderedState = lDetails[DEFER_BLOCK_STATE];
assertEqual(
renderedState, DeferBlockState.Placeholder,
'Expected a placeholder to be rendered in this defer block.');
assertLView(triggerLView);
}

return triggerLView;
}

/**
* Gets the element that a deferred block's trigger is pointing to.
* @param triggerLView LView in which the trigger is defined.
* @param triggerIndex Index at which the trigger element should've been rendered.
*/
export function getTriggerElement(triggerLView: LView, triggerIndex: number): Element {
const element = getNativeByIndex(HEADER_OFFSET + triggerIndex, triggerLView);
ngDevMode && assertElement(element);
return element as Element;
}

/**
* Registers a DOM-node based trigger.
* @param initialLView LView in which the defer block is rendered.
* @param tNode TNode representing the defer block.
* @param triggerIndex Index at which to find the trigger element.
* @param walkUpTimes Number of times to go up/down in the view hierarchy to find the trigger.
* @param registerFn Function that will register the DOM events.
* @param callback Callback to be invoked when the trigger receives the event that should render
* the deferred block.
*/
export function registerDomTrigger(
initialLView: LView, tNode: TNode, triggerIndex: number, walkUpTimes: number|undefined,
registerFn: (element: Element, callback: VoidFunction, injector: Injector) => VoidFunction,
callback: VoidFunction) {
const injector = initialLView[INJECTOR]!;

// Assumption: the `afterRender` reference should be destroyed
// automatically so we don't need to keep track of it.
const afterRenderRef = afterRender(() => {
const lDetails = getLDeferBlockDetails(initialLView, tNode);
const renderedState = lDetails[DEFER_BLOCK_STATE];

// If the block was loaded before the trigger was resolved, we don't need to do anything.
if (renderedState !== DeferBlockInternalState.Initial &&
renderedState !== DeferBlockState.Placeholder) {
afterRenderRef.destroy();
return;
}

const triggerLView = getTriggerLView(initialLView, tNode, walkUpTimes);

// Keep polling until we resolve the trigger's LView.
// `afterRender` should stop automatically if the view is destroyed.
if (!triggerLView) {
return;
}

// It's possible that the trigger's view was destroyed before we resolved the trigger element.
if (triggerLView[FLAGS] & LViewFlags.Destroyed) {
afterRenderRef.destroy();
return;
}

// TODO: add integration with `DeferBlockCleanupManager`.
const element = getTriggerElement(triggerLView, triggerIndex);
const cleanup = registerFn(element, () => {
callback();
removeLViewOnDestroy(triggerLView, cleanup);
if (initialLView !== triggerLView) {
removeLViewOnDestroy(initialLView, cleanup);
}
cleanup();
}, injector);

afterRenderRef.destroy();
storeLViewOnDestroy(triggerLView, cleanup);

// Since the trigger and deferred block might be in different
// views, we have to register the callback in both locations.
if (initialLView !== triggerLView) {
storeLViewOnDestroy(initialLView, cleanup);
}
}, {injector});
}
Loading

0 comments on commit 659072f

Please sign in to comment.