fix(core): make DefaultIterableDiffer keep the order of duplicates (#23941)
Previously, in `_mismatch()`, the `DefaultIterableDiffer` first checks `_linkedRecords` for `itemTrackBy`, then checks `_unlinkedRecords`. This cause the `DefaultIterableDiffer` to move "later" items that match the `itemTrackBy` from the old collection, rather than using the "earlier" one. Now we check `_unlinkedRecords` first, so that the `DefaultIterableDiffer` can give a more stable and reasonable result after diffing. For example, rather than (`a1` and `a2` have same trackById) ``` a1 b c a2 => b a2 c a1 ``` we get ``` a1 b c a2 => b a1 c a2 ``` where a1 and a2 retain their original order despite both having the same track by value. Fixes #23815 PR Close #23941
This commit is contained in:
parent
cffb00ec11
commit
a8269264bf
|
@ -281,23 +281,24 @@ export class DefaultIterableDiffer<V> implements IterableDiffer<V>, IterableChan
|
||||||
this._remove(record);
|
this._remove(record);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to see if we have seen the item before.
|
// See if we have evicted the item, which used to be at some anterior position of _itHead list.
|
||||||
record = this._linkedRecords === null ? null : this._linkedRecords.get(itemTrackBy, index);
|
record = this._unlinkedRecords === null ? null : this._unlinkedRecords.get(itemTrackBy, null);
|
||||||
if (record !== null) {
|
if (record !== null) {
|
||||||
// We have seen this before, we need to move it forward in the collection.
|
// It is an item which we have evicted earlier: reinsert it back into the list.
|
||||||
// But first we need to check if identity changed, so we can update in view if necessary
|
// But first we need to check if identity changed, so we can update in view if necessary.
|
||||||
if (!Object.is(record.item, item)) this._addIdentityChange(record, item);
|
if (!Object.is(record.item, item)) this._addIdentityChange(record, item);
|
||||||
|
|
||||||
this._moveAfter(record, previousRecord, index);
|
this._reinsertAfter(record, previousRecord, index);
|
||||||
} else {
|
} else {
|
||||||
// Never seen it, check evicted list.
|
// Attempt to see if the item is at some posterior position of _itHead list.
|
||||||
record = this._unlinkedRecords === null ? null : this._unlinkedRecords.get(itemTrackBy, null);
|
record = this._linkedRecords === null ? null : this._linkedRecords.get(itemTrackBy, index);
|
||||||
if (record !== null) {
|
if (record !== null) {
|
||||||
// It is an item which we have evicted earlier: reinsert it back into the list.
|
// We have the item in _itHead at/after `index` position. We need to move it forward in the
|
||||||
// But first we need to check if identity changed, so we can update in view if necessary
|
// collection.
|
||||||
|
// But first we need to check if identity changed, so we can update in view if necessary.
|
||||||
if (!Object.is(record.item, item)) this._addIdentityChange(record, item);
|
if (!Object.is(record.item, item)) this._addIdentityChange(record, item);
|
||||||
|
|
||||||
this._reinsertAfter(record, previousRecord, index);
|
this._moveAfter(record, previousRecord, index);
|
||||||
} else {
|
} else {
|
||||||
// It is a new item: add it.
|
// It is a new item: add it.
|
||||||
record =
|
record =
|
||||||
|
|
|
@ -561,6 +561,65 @@ class ComplexItem {
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should keep the order of duplicates', () => {
|
||||||
|
const l1 = [
|
||||||
|
new ComplexItem('a', 'blue'),
|
||||||
|
new ComplexItem('b', 'yellow'),
|
||||||
|
new ComplexItem('c', 'orange'),
|
||||||
|
new ComplexItem('a', 'red'),
|
||||||
|
];
|
||||||
|
differ.check(l1);
|
||||||
|
|
||||||
|
const l2 = [
|
||||||
|
new ComplexItem('b', 'yellow'),
|
||||||
|
new ComplexItem('a', 'blue'),
|
||||||
|
new ComplexItem('c', 'orange'),
|
||||||
|
new ComplexItem('a', 'red'),
|
||||||
|
];
|
||||||
|
differ.check(l2);
|
||||||
|
|
||||||
|
expect(iterableDifferToString(differ)).toEqual(iterableChangesAsString({
|
||||||
|
collection: [
|
||||||
|
'{id: b, color: yellow}[1->0]', '{id: a, color: blue}[0->1]', '{id: c, color: orange}',
|
||||||
|
'{id: a, color: red}'
|
||||||
|
],
|
||||||
|
identityChanges: [
|
||||||
|
'{id: b, color: yellow}[1->0]', '{id: a, color: blue}[0->1]', '{id: c, color: orange}',
|
||||||
|
'{id: a, color: red}'
|
||||||
|
],
|
||||||
|
previous: [
|
||||||
|
'{id: a, color: blue}[0->1]', '{id: b, color: yellow}[1->0]', '{id: c, color: orange}',
|
||||||
|
'{id: a, color: red}'
|
||||||
|
],
|
||||||
|
moves: ['{id: b, color: yellow}[1->0]', '{id: a, color: blue}[0->1]'],
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not have identity changed', () => {
|
||||||
|
const l1 = [
|
||||||
|
new ComplexItem('a', 'blue'),
|
||||||
|
new ComplexItem('b', 'yellow'),
|
||||||
|
new ComplexItem('c', 'orange'),
|
||||||
|
new ComplexItem('a', 'red'),
|
||||||
|
];
|
||||||
|
differ.check(l1);
|
||||||
|
|
||||||
|
const l2 = [l1[1], l1[0], l1[2], l1[3]];
|
||||||
|
differ.check(l2);
|
||||||
|
|
||||||
|
expect(iterableDifferToString(differ)).toEqual(iterableChangesAsString({
|
||||||
|
collection: [
|
||||||
|
'{id: b, color: yellow}[1->0]', '{id: a, color: blue}[0->1]', '{id: c, color: orange}',
|
||||||
|
'{id: a, color: red}'
|
||||||
|
],
|
||||||
|
previous: [
|
||||||
|
'{id: a, color: blue}[0->1]', '{id: b, color: yellow}[1->0]', '{id: c, color: orange}',
|
||||||
|
'{id: a, color: red}'
|
||||||
|
],
|
||||||
|
moves: ['{id: b, color: yellow}[1->0]', '{id: a, color: blue}[0->1]'],
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
it('should track removals normally', () => {
|
it('should track removals normally', () => {
|
||||||
const l = buildItemList(['a', 'b', 'c']);
|
const l = buildItemList(['a', 'b', 'c']);
|
||||||
differ.check(l);
|
differ.check(l);
|
||||||
|
|
Loading…
Reference in New Issue