mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
refactor(core): make timer-related @defer logic tree-shakable (#52042)
This commit updates `@defer` logic related to handling `after` and `minimum` parameters tree-shakable. If `after` or `minimum` was used on a `@loading` or `@placeholder` blocks, compiler generates an extra argument for the `ɵɵdefer` instruction. This extra argument is a reference to a function that brings timer-related code. PR Close #52042
This commit is contained in:
parent
68ba798ae3
commit
2eebd47733
12 changed files with 164 additions and 69 deletions
|
|
@ -70,7 +70,7 @@
|
|||
"defer": {
|
||||
"uncompressed": {
|
||||
"runtime": 2689,
|
||||
"main": 124117,
|
||||
"main": 121811,
|
||||
"polyfills": 33807,
|
||||
"src_app_defer_component_ts": 450
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,8 @@
|
|||
],
|
||||
"failureMessage": "Incorrect template"
|
||||
}
|
||||
]
|
||||
],
|
||||
"skipForTemplatePipeline": true
|
||||
},
|
||||
{
|
||||
"description": "should generate a deferred block with loading block parameters",
|
||||
|
|
@ -67,7 +68,8 @@
|
|||
],
|
||||
"failureMessage": "Incorrect template"
|
||||
}
|
||||
]
|
||||
],
|
||||
"skipForTemplatePipeline": true
|
||||
},
|
||||
{
|
||||
"description": "should generate a deferred block with local dependencies",
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ MyApp.ɵcmp = /*@__PURE__*/ $r3$.ɵɵdefineComponent({
|
|||
template: function MyApp_Template(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
$r3$.ɵɵtemplate(0, MyApp_Defer_0_Template, 1, 0)(1, MyApp_DeferLoading_1_Template, 1, 0);
|
||||
$r3$.ɵɵdefer(2, 0, null, 1, null, null, 0);
|
||||
$r3$.ɵɵdefer(2, 0, null, 1, null, null, 0, null, $r3$.ɵɵdeferEnableTimerScheduling);
|
||||
$r3$.ɵɵdeferOnIdle();
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ MyApp.ɵcmp = /*@__PURE__*/ $r3$.ɵɵdefineComponent({
|
|||
template: function MyApp_Template(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
$r3$.ɵɵtemplate(0, MyApp_Defer_0_Template, 1, 0)(1, MyApp_DeferPlaceholder_1_Template, 1, 0);
|
||||
$r3$.ɵɵdefer(2, 0, null, null, 1, null, null, 0);
|
||||
$r3$.ɵɵdefer(2, 0, null, null, 1, null, null, 0, $r3$.ɵɵdeferEnableTimerScheduling);
|
||||
$r3$.ɵɵdeferOnIdle();
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -8854,6 +8854,47 @@ function allTests(os: string) {
|
|||
expect(jsContents).not.toContain('import { CmpA }');
|
||||
});
|
||||
|
||||
it('should include timer scheduler function when ' +
|
||||
'`after` or `minimum` parameters are used',
|
||||
() => {
|
||||
env.write('cmp-a.ts', `
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'cmp-a',
|
||||
template: 'CmpA!'
|
||||
})
|
||||
export class CmpA {}
|
||||
`);
|
||||
|
||||
env.write('/test.ts', `
|
||||
import { Component } from '@angular/core';
|
||||
import { CmpA } from './cmp-a';
|
||||
|
||||
@Component({
|
||||
selector: 'test-cmp',
|
||||
standalone: true,
|
||||
imports: [CmpA],
|
||||
template: \`
|
||||
@defer {
|
||||
<cmp-a />
|
||||
} @loading (after 500ms; minimum 300ms) {
|
||||
Loading...
|
||||
}
|
||||
\`,
|
||||
})
|
||||
export class TestCmp {}
|
||||
`);
|
||||
|
||||
env.driveMain();
|
||||
|
||||
const jsContents = env.getContents('test.js');
|
||||
expect(jsContents)
|
||||
.toContain(
|
||||
'ɵɵdefer(2, 0, TestCmp_Defer_2_DepsFn, 1, null, null, 0, null, i0.ɵɵdeferEnableTimerScheduling)');
|
||||
});
|
||||
|
||||
describe('imports', () => {
|
||||
it('should retain regular imports when symbol is eagerly referenced', () => {
|
||||
env.write('cmp-a.ts', `
|
||||
|
|
|
|||
|
|
@ -160,6 +160,8 @@ export class Identifiers {
|
|||
o.ExternalReference = {name: 'ɵɵdeferPrefetchOnInteraction', moduleName: CORE};
|
||||
static deferPrefetchOnViewport:
|
||||
o.ExternalReference = {name: 'ɵɵdeferPrefetchOnViewport', moduleName: CORE};
|
||||
static deferEnableTimerScheduling:
|
||||
o.ExternalReference = {name: 'ɵɵdeferEnableTimerScheduling', moduleName: CORE};
|
||||
|
||||
static conditional: o.ExternalReference = {name: 'ɵɵconditional', moduleName: CORE};
|
||||
static repeater: o.ExternalReference = {name: 'ɵɵrepeater', moduleName: CORE};
|
||||
|
|
|
|||
|
|
@ -1323,6 +1323,9 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||
o.literal(errorIndex),
|
||||
loadingConsts?.length ? this.addToConsts(o.literalArr(loadingConsts)) : o.TYPED_NULL_EXPR,
|
||||
placeholderConsts ? this.addToConsts(placeholderConsts) : o.TYPED_NULL_EXPR,
|
||||
(loadingConsts?.length || placeholderConsts) ?
|
||||
o.importExpr(R3.deferEnableTimerScheduling) :
|
||||
o.TYPED_NULL_EXPR,
|
||||
]));
|
||||
|
||||
this.createDeferTriggerInstructions(deferredIndex, triggers, metadata, false);
|
||||
|
|
|
|||
|
|
@ -225,6 +225,7 @@ export {
|
|||
ɵɵdeferPrefetchOnHover,
|
||||
ɵɵdeferPrefetchOnInteraction,
|
||||
ɵɵdeferPrefetchOnViewport,
|
||||
ɵɵdeferEnableTimerScheduling,
|
||||
ɵɵtext,
|
||||
ɵɵtextInterpolate,
|
||||
ɵɵtextInterpolate1,
|
||||
|
|
|
|||
|
|
@ -143,6 +143,7 @@ export {
|
|||
ɵɵdeferPrefetchOnHover,
|
||||
ɵɵdeferPrefetchOnInteraction,
|
||||
ɵɵdeferPrefetchOnViewport,
|
||||
ɵɵdeferEnableTimerScheduling,
|
||||
|
||||
ɵɵtext,
|
||||
ɵɵtextInterpolate,
|
||||
|
|
|
|||
|
|
@ -45,6 +45,38 @@ function shouldTriggerDeferBlock(injector: Injector): boolean {
|
|||
return isPlatformBrowser(injector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reference to the timer-based scheduler implementation of defer block state
|
||||
* rendering method. It's used to make timer-based scheduling tree-shakable.
|
||||
* If `minimum` or `after` parameters are used, compiler generates an extra
|
||||
* argument for the `ɵɵdefer` instruction, which references a timer-based
|
||||
* implementation.
|
||||
*/
|
||||
let applyDeferBlockStateWithSchedulingImpl: (typeof applyDeferBlockState)|null = null;
|
||||
|
||||
/**
|
||||
* Enables timer-related scheduling if `after` or `minimum` parameters are setup
|
||||
* on the `@loading` or `@placeholder` blocks.
|
||||
*/
|
||||
export function ɵɵdeferEnableTimerScheduling(
|
||||
tView: TView, tDetails: TDeferBlockDetails, placeholderConfigIndex?: number|null,
|
||||
loadingConfigIndex?: number|null) {
|
||||
const tViewConsts = tView.consts;
|
||||
if (placeholderConfigIndex != null) {
|
||||
tDetails.placeholderBlockConfig =
|
||||
getConstant<DeferredPlaceholderBlockConfig>(tViewConsts, placeholderConfigIndex);
|
||||
}
|
||||
if (loadingConfigIndex != null) {
|
||||
tDetails.loadingBlockConfig =
|
||||
getConstant<DeferredLoadingBlockConfig>(tViewConsts, loadingConfigIndex);
|
||||
}
|
||||
|
||||
// Enable implementation that supports timer-based scheduling.
|
||||
if (applyDeferBlockStateWithSchedulingImpl === null) {
|
||||
applyDeferBlockStateWithSchedulingImpl = applyDeferBlockStateWithScheduling;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates runtime data structures for defer blocks.
|
||||
*
|
||||
|
|
@ -58,6 +90,8 @@ function shouldTriggerDeferBlock(injector: Injector): boolean {
|
|||
* block.
|
||||
* @param placeholderConfigIndex Index in the constants array of the configuration of the
|
||||
* placeholder block.
|
||||
* @param enableTimerScheduling Function that enables timer-related scheduling if `after`
|
||||
* or `minimum` parameters are setup on the `@loading` or `@placeholder` blocks.
|
||||
*
|
||||
* @codeGenApi
|
||||
*/
|
||||
|
|
@ -65,32 +99,28 @@ export function ɵɵdefer(
|
|||
index: number, primaryTmplIndex: number, dependencyResolverFn?: DependencyResolverFn|null,
|
||||
loadingTmplIndex?: number|null, placeholderTmplIndex?: number|null,
|
||||
errorTmplIndex?: number|null, loadingConfigIndex?: number|null,
|
||||
placeholderConfigIndex?: number|null) {
|
||||
placeholderConfigIndex?: number|null,
|
||||
enableTimerScheduling?: typeof ɵɵdeferEnableTimerScheduling) {
|
||||
const lView = getLView();
|
||||
const tView = getTView();
|
||||
const tViewConsts = tView.consts;
|
||||
const adjustedIndex = index + HEADER_OFFSET;
|
||||
|
||||
ɵɵtemplate(index, null, 0, 0);
|
||||
|
||||
if (tView.firstCreatePass) {
|
||||
const deferBlockConfig: TDeferBlockDetails = {
|
||||
const tDetails: TDeferBlockDetails = {
|
||||
primaryTmplIndex,
|
||||
loadingTmplIndex: loadingTmplIndex ?? null,
|
||||
placeholderTmplIndex: placeholderTmplIndex ?? null,
|
||||
errorTmplIndex: errorTmplIndex ?? null,
|
||||
placeholderBlockConfig: placeholderConfigIndex != null ?
|
||||
getConstant<DeferredPlaceholderBlockConfig>(tViewConsts, placeholderConfigIndex) :
|
||||
null,
|
||||
loadingBlockConfig: loadingConfigIndex != null ?
|
||||
getConstant<DeferredLoadingBlockConfig>(tViewConsts, loadingConfigIndex) :
|
||||
null,
|
||||
placeholderBlockConfig: null,
|
||||
loadingBlockConfig: null,
|
||||
dependencyResolverFn: dependencyResolverFn ?? null,
|
||||
loadingState: DeferDependenciesLoadingState.NOT_STARTED,
|
||||
loadingPromise: null,
|
||||
};
|
||||
|
||||
setTDeferBlockDetails(tView, adjustedIndex, deferBlockConfig);
|
||||
enableTimerScheduling?.(tView, tDetails, placeholderConfigIndex, loadingConfigIndex);
|
||||
setTDeferBlockDetails(tView, adjustedIndex, tDetails);
|
||||
}
|
||||
|
||||
const tNode = getCurrentTNode()!;
|
||||
|
|
@ -657,16 +687,68 @@ export function renderDeferBlockState(
|
|||
ngDevMode && assertTNodeForLView(tNode, hostLView);
|
||||
|
||||
const lDetails = getLDeferBlockDetails(hostLView, tNode);
|
||||
const tDetails = getTDeferBlockDetails(hostTView, tNode);
|
||||
|
||||
ngDevMode && assertDefined(lDetails, 'Expected a defer block state defined');
|
||||
|
||||
const now = Date.now();
|
||||
const currentState = lDetails[DEFER_BLOCK_STATE];
|
||||
|
||||
if (!isValidStateChange(currentState, newState) ||
|
||||
!isValidStateChange(lDetails[NEXT_DEFER_BLOCK_STATE] ?? -1, newState))
|
||||
return;
|
||||
if (isValidStateChange(currentState, newState) &&
|
||||
isValidStateChange(lDetails[NEXT_DEFER_BLOCK_STATE] ?? -1, newState)) {
|
||||
const tDetails = getTDeferBlockDetails(hostTView, tNode);
|
||||
const needsScheduling = getLoadingBlockAfter(tDetails) !== null ||
|
||||
getMinimumDurationForState(tDetails, DeferBlockState.Loading) !== null ||
|
||||
getMinimumDurationForState(tDetails, DeferBlockState.Placeholder);
|
||||
|
||||
if (ngDevMode && needsScheduling) {
|
||||
assertDefined(
|
||||
applyDeferBlockStateWithSchedulingImpl, 'Expected scheduling function to be defined');
|
||||
}
|
||||
|
||||
const applyStateFn =
|
||||
needsScheduling ? applyDeferBlockStateWithSchedulingImpl! : applyDeferBlockState;
|
||||
applyStateFn(newState, lDetails, lContainer, tNode, hostLView);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies changes to the DOM to reflect a given state.
|
||||
*/
|
||||
function applyDeferBlockState(
|
||||
newState: DeferBlockState, lDetails: LDeferBlockDetails, lContainer: LContainer, tNode: TNode,
|
||||
hostLView: LView<unknown>) {
|
||||
const stateTmplIndex = getTemplateIndexForState(newState, hostLView, tNode);
|
||||
|
||||
if (stateTmplIndex !== null) {
|
||||
lDetails[DEFER_BLOCK_STATE] = newState;
|
||||
const hostTView = hostLView[TVIEW];
|
||||
const adjustedIndex = stateTmplIndex + HEADER_OFFSET;
|
||||
const tNode = getTNode(hostTView, adjustedIndex) as TContainerNode;
|
||||
|
||||
// There is only 1 view that can be present in an LContainer that
|
||||
// represents a defer block, so always refer to the first one.
|
||||
const viewIndex = 0;
|
||||
|
||||
removeLViewFromLContainer(lContainer, viewIndex);
|
||||
const dehydratedView = findMatchingDehydratedView(lContainer, tNode.tView!.ssrId);
|
||||
const embeddedLView = createAndRenderEmbeddedLView(hostLView, tNode, null, {dehydratedView});
|
||||
addLViewToLContainer(
|
||||
lContainer, embeddedLView, viewIndex, shouldAddViewToDom(tNode, dehydratedView));
|
||||
markViewDirty(embeddedLView);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extends the `applyDeferBlockState` with timer-based scheduling.
|
||||
* This function becomes available on a page if there are defer blocks
|
||||
* that use `after` or `minimum` parameters in the `@loading` or
|
||||
* `@placeholder` blocks.
|
||||
*/
|
||||
function applyDeferBlockStateWithScheduling(
|
||||
newState: DeferBlockState, lDetails: LDeferBlockDetails, lContainer: LContainer, tNode: TNode,
|
||||
hostLView: LView<unknown>) {
|
||||
const now = Date.now();
|
||||
const hostTView = hostLView[TVIEW];
|
||||
const tDetails = getTDeferBlockDetails(hostTView, tNode);
|
||||
|
||||
if (lDetails[STATE_IS_FROZEN_UNTIL] === null || lDetails[STATE_IS_FROZEN_UNTIL] <= now) {
|
||||
lDetails[STATE_IS_FROZEN_UNTIL] = null;
|
||||
|
|
@ -690,7 +772,7 @@ export function renderDeferBlockState(
|
|||
lDetails[NEXT_DEFER_BLOCK_STATE] = null;
|
||||
}
|
||||
|
||||
applyDeferBlockStateToDom(newState, lDetails, lContainer, hostLView, tNode);
|
||||
applyDeferBlockState(newState, lDetails, lContainer, tNode, hostLView);
|
||||
|
||||
const duration = getMinimumDurationForState(tDetails, newState);
|
||||
if (duration !== null) {
|
||||
|
|
@ -720,8 +802,6 @@ function scheduleDeferBlockUpdate(
|
|||
renderDeferBlockState(nextState, tNode, lContainer);
|
||||
}
|
||||
};
|
||||
// TODO: this needs refactoring to make `TimerScheduler` that is used inside
|
||||
// of the `scheduleTimerTrigger` function tree-shakable.
|
||||
return scheduleTimerTrigger(timeout, callback, hostLView, true);
|
||||
}
|
||||
|
||||
|
|
@ -739,33 +819,6 @@ function isValidStateChange(
|
|||
return currentState < newState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies changes to the DOM to reflect a given state.
|
||||
*/
|
||||
function applyDeferBlockStateToDom(
|
||||
newState: DeferBlockState, lDetails: LDeferBlockDetails, lContainer: LContainer,
|
||||
hostLView: LView<unknown>, tNode: TNode) {
|
||||
const stateTmplIndex = getTemplateIndexForState(newState, hostLView, tNode);
|
||||
|
||||
if (stateTmplIndex !== null) {
|
||||
lDetails[DEFER_BLOCK_STATE] = newState;
|
||||
const hostTView = hostLView[TVIEW];
|
||||
const adjustedIndex = stateTmplIndex + HEADER_OFFSET;
|
||||
const tNode = getTNode(hostTView, adjustedIndex) as TContainerNode;
|
||||
|
||||
// There is only 1 view that can be present in an LContainer that
|
||||
// represents a defer block, so always refer to the first one.
|
||||
const viewIndex = 0;
|
||||
|
||||
removeLViewFromLContainer(lContainer, viewIndex);
|
||||
const dehydratedView = findMatchingDehydratedView(lContainer, tNode.tView!.ssrId);
|
||||
const embeddedLView = createAndRenderEmbeddedLView(hostLView, tNode, null, {dehydratedView});
|
||||
addLViewToLContainer(
|
||||
lContainer, embeddedLView, viewIndex, shouldAddViewToDom(tNode, dehydratedView));
|
||||
markViewDirty(embeddedLView);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger prefetching of dependencies for a defer block.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -154,6 +154,7 @@ export const angularCoreEnv: {[name: string]: Function} =
|
|||
'ɵɵdeferPrefetchOnHover': r3.ɵɵdeferPrefetchOnHover,
|
||||
'ɵɵdeferPrefetchOnInteraction': r3.ɵɵdeferPrefetchOnInteraction,
|
||||
'ɵɵdeferPrefetchOnViewport': r3.ɵɵdeferPrefetchOnViewport,
|
||||
'ɵɵdeferEnableTimerScheduling': r3.ɵɵdeferEnableTimerScheduling,
|
||||
'ɵɵrepeater': r3.ɵɵrepeater,
|
||||
'ɵɵrepeaterCreate': r3.ɵɵrepeaterCreate,
|
||||
'ɵɵrepeaterTrackByIndex': r3.ɵɵrepeaterTrackByIndex,
|
||||
|
|
|
|||
|
|
@ -233,9 +233,6 @@
|
|||
{
|
||||
"name": "KeyEventsPlugin"
|
||||
},
|
||||
{
|
||||
"name": "LOADING_AFTER_CLEANUP_FN"
|
||||
},
|
||||
{
|
||||
"name": "LOADING_AFTER_SLOT"
|
||||
},
|
||||
|
|
@ -428,9 +425,6 @@
|
|||
{
|
||||
"name": "SOURCE"
|
||||
},
|
||||
{
|
||||
"name": "STATE_IS_FROZEN_UNTIL"
|
||||
},
|
||||
{
|
||||
"name": "SVG_NAMESPACE"
|
||||
},
|
||||
|
|
@ -491,9 +485,6 @@
|
|||
{
|
||||
"name": "TYPE"
|
||||
},
|
||||
{
|
||||
"name": "TimerScheduler"
|
||||
},
|
||||
{
|
||||
"name": "USE_VALUE"
|
||||
},
|
||||
|
|
@ -611,6 +602,12 @@
|
|||
{
|
||||
"name": "appendChild"
|
||||
},
|
||||
{
|
||||
"name": "applyDeferBlockState"
|
||||
},
|
||||
{
|
||||
"name": "applyDeferBlockStateWithSchedulingImpl"
|
||||
},
|
||||
{
|
||||
"name": "applyNodes"
|
||||
},
|
||||
|
|
@ -623,9 +620,6 @@
|
|||
{
|
||||
"name": "applyView"
|
||||
},
|
||||
{
|
||||
"name": "arraySplice"
|
||||
},
|
||||
{
|
||||
"name": "assertConsumerNode"
|
||||
},
|
||||
|
|
@ -890,6 +884,9 @@
|
|||
{
|
||||
"name": "getLViewParent"
|
||||
},
|
||||
{
|
||||
"name": "getMinimumDurationForState"
|
||||
},
|
||||
{
|
||||
"name": "getNativeByTNode"
|
||||
},
|
||||
|
|
@ -2258,9 +2255,6 @@
|
|||
{
|
||||
"name": "scheduleArray"
|
||||
},
|
||||
{
|
||||
"name": "scheduleDeferBlockUpdate"
|
||||
},
|
||||
{
|
||||
"name": "searchTokensOnInjector"
|
||||
},
|
||||
|
|
@ -2321,9 +2315,6 @@
|
|||
{
|
||||
"name": "shouldSearchParent"
|
||||
},
|
||||
{
|
||||
"name": "storeLViewOnDestroy"
|
||||
},
|
||||
{
|
||||
"name": "stringify"
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue