Skip to content

Commit 7d42dc3

Browse files
pkozlowski-opensourcealxhub
authored andcommitted
feat(core): the new list reconciliation algorithm for built-in for (#51980)
This commit plugs the new list reconciliation into the new built-in repeater. PR Close #51980
1 parent 4f04d1c commit 7d42dc3

File tree

2 files changed

+81
-91
lines changed

2 files changed

+81
-91
lines changed

packages/core/src/render3/instructions/control_flow.ts

Lines changed: 73 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,18 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {DefaultIterableDiffer, IterableChangeRecord, TrackByFunction} from '../../change_detection';
9+
import {TrackByFunction} from '../../change_detection';
10+
import {DehydratedContainerView} from '../../hydration/interfaces';
1011
import {findMatchingDehydratedView} from '../../hydration/views';
1112
import {assertDefined} from '../../util/assert';
1213
import {assertLContainer, assertLView, assertTNode} from '../assert';
1314
import {bindingUpdated} from '../bindings';
1415
import {CONTAINER_HEADER_OFFSET, LContainer} from '../interfaces/container';
1516
import {ComponentTemplate} from '../interfaces/definition';
1617
import {TNode} from '../interfaces/node';
17-
import {CONTEXT, DECLARATION_COMPONENT_VIEW, HEADER_OFFSET, LView, TVIEW, TView} from '../interfaces/view';
18-
import {detachView} from '../node_manipulation';
18+
import {CONTEXT, DECLARATION_COMPONENT_VIEW, HEADER_OFFSET, HYDRATION, LView, TVIEW, TView} from '../interfaces/view';
19+
import {LiveCollection, reconcile} from '../list_reconciliation';
20+
import {destroyLView, detachView} from '../node_manipulation';
1921
import {getLView, nextBindingIndex} from '../state';
2022
import {getTNode} from '../util/view_utils';
2123
import {addLViewToLContainer, createAndRenderEmbeddedLView, getLViewFromLContainer, removeLViewFromLContainer, shouldAddViewToDom} from '../view_manipulation';
@@ -99,7 +101,7 @@ export function ɵɵrepeaterTrackByIdentity<T>(_: number, value: T) {
99101
}
100102

101103
class RepeaterMetadata {
102-
constructor(public hasEmptyBlock: boolean, public differ: DefaultIterableDiffer<unknown>) {}
104+
constructor(public hasEmptyBlock: boolean, public trackByFn: TrackByFunction<unknown>) {}
103105
}
104106

105107
/**
@@ -135,7 +137,7 @@ export function ɵɵrepeaterCreate(
135137
// new function. For pure functions it's not necessary.
136138
trackByFn.bind(hostLView[DECLARATION_COMPONENT_VIEW][CONTEXT]) :
137139
trackByFn;
138-
const metadata = new RepeaterMetadata(hasEmptyBlock, new DefaultIterableDiffer(boundTrackBy));
140+
const metadata = new RepeaterMetadata(hasEmptyBlock, boundTrackBy);
139141
hostLView[HEADER_OFFSET + index] = metadata;
140142

141143
ɵɵtemplate(index + 1, templateFn, decls, vars);
@@ -150,6 +152,48 @@ export function ɵɵrepeaterCreate(
150152
}
151153
}
152154

155+
class LiveCollectionLContainerImpl extends
156+
LiveCollection<LView<RepeaterContext<unknown>>, RepeaterContext<unknown>> {
157+
constructor(
158+
private lContainer: LContainer, private hostLView: LView, private templateTNode: TNode,
159+
private trackByFn: TrackByFunction<unknown>) {
160+
super();
161+
}
162+
163+
override get length(): number {
164+
return this.lContainer.length - CONTAINER_HEADER_OFFSET;
165+
}
166+
override at(index: number): LView<RepeaterContext<unknown>> {
167+
return getExistingLViewFromLContainer(this.lContainer, index);
168+
}
169+
override key(index: number): unknown {
170+
return this.trackByFn(index, this.at(index)[CONTEXT].$implicit);
171+
}
172+
override attach(index: number, lView: LView<RepeaterContext<unknown>>): void {
173+
const dehydratedView = lView[HYDRATION] as DehydratedContainerView;
174+
addLViewToLContainer(
175+
this.lContainer, lView, index, shouldAddViewToDom(this.templateTNode, dehydratedView));
176+
}
177+
override detach(index: number): LView<RepeaterContext<unknown>> {
178+
return detachExistingView<RepeaterContext<unknown>>(this.lContainer, index);
179+
}
180+
override create(index: number, value: unknown): LView<RepeaterContext<unknown>> {
181+
const dehydratedView =
182+
findMatchingDehydratedView(this.lContainer, this.templateTNode.tView!.ssrId);
183+
const embeddedLView = createAndRenderEmbeddedLView(
184+
this.hostLView, this.templateTNode, new RepeaterContext(this.lContainer, value, index),
185+
{dehydratedView});
186+
187+
return embeddedLView;
188+
}
189+
override destroy(lView: LView<RepeaterContext<unknown>>): void {
190+
destroyLView(lView[TVIEW], lView);
191+
}
192+
override updateValue(index: number, value: unknown): void {
193+
this.at(index)[CONTEXT].$implicit = value;
194+
}
195+
}
196+
153197
/**
154198
* The repeater instruction does update-time diffing of a provided collection (against the
155199
* collection seen previously) and maps changes in the collection to views structure (by adding,
@@ -165,79 +209,43 @@ export function ɵɵrepeater(
165209
const hostLView = getLView();
166210
const hostTView = hostLView[TVIEW];
167211
const metadata = hostLView[HEADER_OFFSET + metadataSlotIdx] as RepeaterMetadata;
212+
const containerIndex = metadataSlotIdx + 1;
213+
const lContainer = getLContainer(hostLView, HEADER_OFFSET + containerIndex);
214+
const itemTemplateTNode = getExistingTNode(hostTView, containerIndex);
168215

169-
const differ = metadata.differ;
170-
const changes = differ.diff(collection);
171-
172-
// handle repeater changes
173-
if (changes !== null) {
174-
const containerIndex = metadataSlotIdx + 1;
175-
const itemTemplateTNode = getExistingTNode(hostTView, containerIndex);
176-
const lContainer = getLContainer(hostLView, HEADER_OFFSET + containerIndex);
177-
let needsIndexUpdate = false;
178-
changes.forEachOperation(
179-
(item: IterableChangeRecord<unknown>, adjustedPreviousIndex: number|null,
180-
currentIndex: number|null) => {
181-
if (item.previousIndex === null) {
182-
// add
183-
const newViewIdx = adjustToLastLContainerIndex(lContainer, currentIndex);
184-
const dehydratedView =
185-
findMatchingDehydratedView(lContainer, itemTemplateTNode.tView!.ssrId);
186-
const embeddedLView = createAndRenderEmbeddedLView(
187-
hostLView, itemTemplateTNode,
188-
new RepeaterContext(lContainer, item.item, newViewIdx), {dehydratedView});
189-
addLViewToLContainer(
190-
lContainer, embeddedLView, newViewIdx,
191-
shouldAddViewToDom(itemTemplateTNode, dehydratedView));
192-
needsIndexUpdate = true;
193-
} else if (currentIndex === null) {
194-
// remove
195-
adjustedPreviousIndex = adjustToLastLContainerIndex(lContainer, adjustedPreviousIndex);
196-
removeLViewFromLContainer(lContainer, adjustedPreviousIndex);
197-
needsIndexUpdate = true;
198-
} else if (adjustedPreviousIndex !== null) {
199-
// move
200-
const existingLView =
201-
detachExistingView<RepeaterContext<unknown>>(lContainer, adjustedPreviousIndex);
202-
addLViewToLContainer(lContainer, existingLView, currentIndex);
203-
needsIndexUpdate = true;
204-
}
205-
});
206-
207-
// A trackBy function might return the same value even if the underlying item changed - re-bind
208-
// it in the context.
209-
changes.forEachIdentityChange((record: IterableChangeRecord<unknown>) => {
210-
const viewIdx = adjustToLastLContainerIndex(lContainer, record.currentIndex);
211-
const lView = getExistingLViewFromLContainer<RepeaterContext<unknown>>(lContainer, viewIdx);
212-
lView[CONTEXT].$implicit = record.item;
213-
});
216+
reconcile(
217+
new LiveCollectionLContainerImpl(
218+
lContainer, hostLView, itemTemplateTNode, metadata.trackByFn),
219+
collection, metadata.trackByFn);
214220

215-
// moves in the container might caused context's index to get out of order, re-adjust
216-
if (needsIndexUpdate) {
217-
for (let i = 0; i < lContainer.length - CONTAINER_HEADER_OFFSET; i++) {
218-
const lView = getExistingLViewFromLContainer<RepeaterContext<unknown>>(lContainer, i);
219-
lView[CONTEXT].$index = i;
220-
}
221-
}
221+
// moves in the container might caused context's index to get out of order, re-adjust
222+
// PERF: we could try to book-keep moves and do this index re-adjust as need, at the cost of the
223+
// additional code complexity
224+
for (let i = 0; i < lContainer.length - CONTAINER_HEADER_OFFSET; i++) {
225+
const lView = getExistingLViewFromLContainer<RepeaterContext<unknown>>(lContainer, i);
226+
lView[CONTEXT].$index = i;
222227
}
223228

224229
// handle empty blocks
230+
// PERF: maybe I could skip allocation of memory for the empty block? Isn't it the "fix" on the
231+
// compiler side that we've been discussing? Talk to K & D!
225232
const bindingIndex = nextBindingIndex();
226233
if (metadata.hasEmptyBlock) {
227-
const hasItemsInCollection = differ.length > 0;
228-
if (bindingUpdated(hostLView, bindingIndex, hasItemsInCollection)) {
234+
const isCollectionEmpty = lContainer.length - CONTAINER_HEADER_OFFSET === 0;
235+
if (bindingUpdated(hostLView, bindingIndex, isCollectionEmpty)) {
229236
const emptyTemplateIndex = metadataSlotIdx + 2;
230-
const lContainer = getLContainer(hostLView, HEADER_OFFSET + emptyTemplateIndex);
231-
if (hasItemsInCollection) {
232-
removeLViewFromLContainer(lContainer, 0);
233-
} else {
237+
const lContainerForEmpty = getLContainer(hostLView, HEADER_OFFSET + emptyTemplateIndex);
238+
if (isCollectionEmpty) {
234239
const emptyTemplateTNode = getExistingTNode(hostTView, emptyTemplateIndex);
235240
const dehydratedView =
236-
findMatchingDehydratedView(lContainer, emptyTemplateTNode.tView!.ssrId);
241+
findMatchingDehydratedView(lContainerForEmpty, emptyTemplateTNode.tView!.ssrId);
237242
const embeddedLView = createAndRenderEmbeddedLView(
238243
hostLView, emptyTemplateTNode, undefined, {dehydratedView});
239244
addLViewToLContainer(
240-
lContainer, embeddedLView, 0, shouldAddViewToDom(emptyTemplateTNode, dehydratedView));
245+
lContainerForEmpty, embeddedLView, 0,
246+
shouldAddViewToDom(emptyTemplateTNode, dehydratedView));
247+
} else {
248+
removeLViewFromLContainer(lContainerForEmpty, 0);
241249
}
242250
}
243251
}
@@ -250,10 +258,6 @@ function getLContainer(lView: LView, index: number): LContainer {
250258
return lContainer;
251259
}
252260

253-
function adjustToLastLContainerIndex(lContainer: LContainer, index: number|null): number {
254-
return index !== null ? index : lContainer.length - CONTAINER_HEADER_OFFSET;
255-
}
256-
257261
function detachExistingView<T>(lContainer: LContainer, index: number): LView<T> {
258262
const existingLView = detachView(lContainer, index);
259263
ngDevMode && assertLView(existingLView);

packages/core/test/acceptance/control_flow_exploration_spec.ts

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -449,36 +449,29 @@ describe('control flow', () => {
449449

450450
it('should be able to access component properties in the tracking function from a loop at the root of the template',
451451
() => {
452-
const calls: string[][] = [];
452+
const calls = new Set();
453453

454454
@Component({
455455
template: `@for ((item of items); track trackingFn(item, compProp)) {{{item}}}`,
456456
})
457457
class TestComponent {
458-
items = ['one', 'two', 'three'];
458+
items = ['a', 'b'];
459459
compProp = 'hello';
460460

461461
trackingFn(item: string, message: string) {
462-
calls.push([item, message]);
462+
calls.add(`${item}:${message}`);
463463
return item;
464464
}
465465
}
466466

467467
const fixture = TestBed.createComponent(TestComponent);
468468
fixture.detectChanges();
469-
expect(calls).toEqual([
470-
['one', 'hello'],
471-
['two', 'hello'],
472-
['three', 'hello'],
473-
['one', 'hello'],
474-
['two', 'hello'],
475-
['three', 'hello'],
476-
]);
469+
expect([...calls].sort()).toEqual(['a:hello', 'b:hello']);
477470
});
478471

479472
it('should be able to access component properties in the tracking function from a nested template',
480473
() => {
481-
const calls: string[][] = [];
474+
const calls = new Set();
482475

483476
@Component({
484477
template: `
@@ -492,25 +485,18 @@ describe('control flow', () => {
492485
`,
493486
})
494487
class TestComponent {
495-
items = ['one', 'two', 'three'];
488+
items = ['a', 'b'];
496489
compProp = 'hello';
497490

498491
trackingFn(item: string, message: string) {
499-
calls.push([item, message]);
492+
calls.add(`${item}:${message}`);
500493
return item;
501494
}
502495
}
503496

504497
const fixture = TestBed.createComponent(TestComponent);
505498
fixture.detectChanges();
506-
expect(calls).toEqual([
507-
['one', 'hello'],
508-
['two', 'hello'],
509-
['three', 'hello'],
510-
['one', 'hello'],
511-
['two', 'hello'],
512-
['three', 'hello'],
513-
]);
499+
expect([...calls].sort()).toEqual(['a:hello', 'b:hello']);
514500
});
515501
});
516502
});

0 commit comments

Comments
 (0)