Commit 6fd38a88 authored by Roman Arora's avatar Roman Arora Committed by Chromium LUCI CQ

Reland "Tab Search: Infinite list - Efficient list DOM item updates"

This is a reland of 339975cc

This CL was reverted because it broke the Tab search list item contents
that are supposed to be visible once a search filter is applied and then
removed. This is now fixed in the updated patchset.

Original change's description:
> Tab Search: Infinite list - Efficient list DOM item updates
>
> Leverage the ListPropertyUpdateBehavior logic that uses Polymer's
> splices and calculateSplices functions to efficiently modify a single
> list item on tab updates, and avoid unnecessary scroll height
> calculations.
>
> Additionally, tab updates for not yet rendered items do not result in
> DOM updates.
>
> Proposal doc & benchmarks: https://docs.google.com/document/d/1FcuhM98moL-BYB1DJGXrpwf-JqMz1aI-RQV08giArBY/edit#heading=h.xzptrog8pyxf
>
> Most relevant pinpoint benchmarks:
> https://pinpoint-dot-chromeperf.appspot.com/job/15db6158d20000
> https://pinpoint-dot-chromeperf.appspot.com/job/107ab7f4d20000
> https://pinpoint-dot-chromeperf.appspot.com/job/16ccacccd20000
>
> Bug: 1149400
> Change-Id: I07fae967033b13830708da3035db3e8b2f0ff9d6
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2578119
> Commit-Queue: Roman Arora <romanarora@chromium.org>
> Reviewed-by: dpapad <dpapad@chromium.org>
> Reviewed-by: Esmael Elmoslimany <aee@chromium.org>
> Reviewed-by: Thomas Lukaszewicz <tluk@chromium.org>
> Cr-Commit-Position: refs/heads/master@{#842182}

Bug: 1149400
Change-Id: Ia8ccf6d9ecdd80006de7a132fefcbe0357a68d5c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2622723Reviewed-by: default avatarEsmael Elmoslimany <aee@chromium.org>
Reviewed-by: default avatarThomas Lukaszewicz <tluk@chromium.org>
Commit-Queue: Roman Arora <romanarora@chromium.org>
Cr-Commit-Position: refs/heads/master@{#842787}
parent dd2a8049
...@@ -197,7 +197,7 @@ Polymer({ ...@@ -197,7 +197,7 @@ Polymer({
if (!this.updateList( if (!this.updateList(
'chooserExceptions', x => x.displayName, exceptions, 'chooserExceptions', x => x.displayName, exceptions,
true /* uidBasedUpdate */)) { true /* identityBasedUpdate= */)) {
// The chooser objects have not been changed, so check if their site // The chooser objects have not been changed, so check if their site
// permissions have changed. The |exceptions| and |this.chooserExceptions| // permissions have changed. The |exceptions| and |this.chooserExceptions|
// arrays should be the same length. // arrays should be the same length.
......
...@@ -182,6 +182,7 @@ js_library("infinite_list") { ...@@ -182,6 +182,7 @@ js_library("infinite_list") {
deps = [ deps = [
"//third_party/polymer/v3_0/components-chromium/iron-selector:iron-selector", "//third_party/polymer/v3_0/components-chromium/iron-selector:iron-selector",
"//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled", "//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
"//ui/webui/resources/js:list_property_update_behavior.m",
] ]
} }
......
...@@ -58,7 +58,7 @@ ...@@ -58,7 +58,7 @@
search-result-text="[[searchResultText_]]"> search-result-text="[[searchResultText_]]">
</tab-search-search-field> </tab-search-search-field>
<div hidden="[[!filteredOpenTabs_.length]]"> <div hidden="[[!filteredOpenTabs_.length]]">
<infinite-list id="tabsList" items="[[filteredOpenTabs_]]" > <infinite-list id="tabsList" items="[[filteredOpenTabs_]]">
<template is="dom-repeat"> <template is="dom-repeat">
<tab-search-item id="[[item.tab.tabId]]" aria-label="[[ariaLabel_(item)]]" <tab-search-item id="[[item.tab.tabId]]" aria-label="[[ariaLabel_(item)]]"
class="mwb-list-item" data="[[item]]" on-click="onItemClick_" class="mwb-list-item" data="[[item]]" on-click="onItemClick_"
......
...@@ -40,10 +40,9 @@ export class TabSearchAppElement extends PolymerElement { ...@@ -40,10 +40,9 @@ export class TabSearchAppElement extends PolymerElement {
value: '', value: '',
}, },
/** @private {?Array<!WindowTabs>} */ /** @private {?Array<!TabData>}*/
openTabs_: { openTabs_: {
type: Array, type: Array,
observer: 'openTabsChanged_',
}, },
/** @private {!Array<!TabData>} */ /** @private {!Array<!TabData>} */
...@@ -169,7 +168,7 @@ export class TabSearchAppElement extends PolymerElement { ...@@ -169,7 +168,7 @@ export class TabSearchAppElement extends PolymerElement {
'Tabs.TabSearch.WebUI.TabListDataReceived', 'Tabs.TabSearch.WebUI.TabListDataReceived',
Math.round(Date.now() - getTabsStartTimestamp)); Math.round(Date.now() - getTabsStartTimestamp));
this.openTabs_ = profileTabs.windows; this.openTabsChanged_(profileTabs.windows);
}); });
} }
...@@ -178,19 +177,13 @@ export class TabSearchAppElement extends PolymerElement { ...@@ -178,19 +177,13 @@ export class TabSearchAppElement extends PolymerElement {
* @private * @private
*/ */
onTabUpdated_(updatedTab) { onTabUpdated_(updatedTab) {
const updatedTabId = updatedTab.tabId; // Replace the tab with the same tabId and trigger rerender.
const windows = this.openTabs_; for (let i = 0; i < this.openTabs_.length; ++i) {
if (windows) { if (this.openTabs_[i].tab.tabId === updatedTab.tabId) {
for (const window of windows) { this.openTabs_[i] =
const {tabs} = window; this.tabData_(updatedTab, this.openTabs_[i].inActiveWindow);
for (let i = 0; i < tabs.length; ++i) { this.updateFilteredTabs_(this.openTabs_);
// Replace the tab with the same tabId and trigger rerender. return;
if (tabs[i].tabId === updatedTabId) {
tabs[i] = updatedTab;
this.openTabs_ = windows.concat();
return;
}
}
} }
} }
} }
...@@ -200,14 +193,21 @@ export class TabSearchAppElement extends PolymerElement { ...@@ -200,14 +193,21 @@ export class TabSearchAppElement extends PolymerElement {
* @private * @private
*/ */
onTabsRemoved_(tabIds) { onTabsRemoved_(tabIds) {
const windows = this.openTabs_; if (!this.openTabs_) {
if (windows) { return;
const ids = new Set(tabIds); }
for (const window of windows) {
window.tabs = window.tabs.filter(tab => (!ids.has(tab.tabId))); const ids = new Set(tabIds);
// Splicing in descending index order to avoid affecting preceding indices
// that are to be removed.
for (let i = this.openTabs_.length - 1; i >= 0; i--) {
if (ids.has(this.openTabs_[i].tab.tabId)) {
this.openTabs_.splice(i, 1);
} }
this.openTabs_ = windows.concat();
} }
this.filteredOpenTabs_ =
this.filteredOpenTabs_.filter(tabData => !ids.has(tabData.tab.tabId));
} }
/** /**
...@@ -304,11 +304,17 @@ export class TabSearchAppElement extends PolymerElement { ...@@ -304,11 +304,17 @@ export class TabSearchAppElement extends PolymerElement {
} }
/** /**
* @param {!Array<!WindowTabs>} newOpenTabs * @param {!Array<!WindowTabs>} newOpenWindowTabs
* @private * @private
*/ */
openTabsChanged_(newOpenTabs) { openTabsChanged_(newOpenWindowTabs) {
this.updateFilteredTabs_(newOpenTabs); this.openTabs_ = [];
newOpenWindowTabs.forEach(({active, tabs}) => {
tabs.forEach(tab => {
this.openTabs_.push(this.tabData_(tab, active));
});
});
this.updateFilteredTabs_(this.openTabs_);
// If there was no previously selected index, set the first item as // If there was no previously selected index, set the first item as
// selected; else retain the currently selected index. If the list // selected; else retain the currently selected index. If the list
...@@ -415,19 +421,22 @@ export class TabSearchAppElement extends PolymerElement { ...@@ -415,19 +421,22 @@ export class TabSearchAppElement extends PolymerElement {
} }
/** /**
* @param {!Array<!WindowTabs>} windowTabs * @param {!Tab} tab
* @param {boolean} inActiveWindow
* @return {!TabData}
* @private * @private
*/ */
updateFilteredTabs_(windowTabs) { tabData_(tab, inActiveWindow) {
const result = []; const hostname = new URL(tab.url).hostname;
windowTabs.forEach(window => { return /** @type {!TabData} */ ({hostname, inActiveWindow, tab});
window.tabs.forEach(tab => { }
const hostname = new URL(tab.url).hostname;
const inActiveWindow = window.active; /**
result.push({hostname, inActiveWindow, tab}); * @param {!Array<!TabData>} tabs
}); * @private
}); */
result.sort((a, b) => { updateFilteredTabs_(tabs) {
tabs.sort((a, b) => {
// Move the active tab to the bottom of the list // Move the active tab to the bottom of the list
// because it's not likely users want to click on it. // because it's not likely users want to click on it.
if (this.moveActiveTabToBottom_) { if (this.moveActiveTabToBottom_) {
...@@ -444,8 +453,9 @@ export class TabSearchAppElement extends PolymerElement { ...@@ -444,8 +453,9 @@ export class TabSearchAppElement extends PolymerElement {
a.tab.lastActiveTimeTicks.internalValue) : a.tab.lastActiveTimeTicks.internalValue) :
0; 0;
}); });
this.filteredOpenTabs_ = this.filteredOpenTabs_ =
fuzzySearch(this.searchText_, result, this.fuzzySearchOptions_); fuzzySearch(this.searchText_, tabs, this.fuzzySearchOptions_);
this.searchResultText_ = this.getA11ySearchResultText_(); this.searchResultText_ = this.getA11ySearchResultText_();
} }
......
...@@ -11,11 +11,12 @@ import {TabData} from './tab_data.js'; ...@@ -11,11 +11,12 @@ import {TabData} from './tab_data.js';
* @param {string} input * @param {string} input
* @param {!Array<!TabData>} records * @param {!Array<!TabData>} records
* @param {!Object} options * @param {!Object} options
* @return {!Array<!TabData>} * @return {!Array<!TabData>} A new array of entries satisfying the input. If no
* search input is present, returns a shallow copy of the records.
*/ */
export function fuzzySearch(input, records, options) { export function fuzzySearch(input, records, options) {
if (input.length === 0) { if (input.length === 0) {
return records; return [...records];
} }
// Fuse does not handle exact match searches well. It indiscriminately // Fuse does not handle exact match searches well. It indiscriminately
// searches for direct matches that appear anywhere in the string. This // searches for direct matches that appear anywhere in the string. This
......
...@@ -9,7 +9,6 @@ ...@@ -9,7 +9,6 @@
</style> </style>
<div id="items"> <div id="items">
<iron-selector id="selector" on-keydown="onKeyDown_" <iron-selector id="selector" on-keydown="onKeyDown_"
on-iron-items-changed="updateScrollerSize_"
on-iron-select="onSelectedChanged_" role="listbox" on-iron-select="onSelectedChanged_" role="listbox"
selected-class="selected"> selected-class="selected">
<slot></slot> <slot></slot>
......
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
import 'chrome://resources/polymer/v3_0/iron-selector/iron-selector.js'; import 'chrome://resources/polymer/v3_0/iron-selector/iron-selector.js';
import {assert, assertInstanceof} from 'chrome://resources/js/assert.m.js'; import {assert, assertInstanceof} from 'chrome://resources/js/assert.m.js';
import {updateListProperty} from 'chrome://resources/js/list_property_update_behavior.m.js';
import {listenOnce} from 'chrome://resources/js/util.m.js'; import {listenOnce} from 'chrome://resources/js/util.m.js';
import {afterNextRender, DomRepeat, html, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js'; import {afterNextRender, DomRepeat, html, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
...@@ -92,9 +93,9 @@ export class InfiniteList extends PolymerElement { ...@@ -92,9 +93,9 @@ export class InfiniteList extends PolymerElement {
/** @private */ /** @private */
getDomItems_() { getDomItems_() {
const selectorChildren = this.$.selector.children; const selector = /** @type {!IronSelectorElement} */ (this.$.selector);
return Array.prototype.slice.call( return Array.prototype.slice.call(
selectorChildren, 0, selectorChildren.length - 1); selector.children, 0, selector.children.length - 1);
} }
/** /**
...@@ -155,6 +156,7 @@ export class InfiniteList extends PolymerElement { ...@@ -155,6 +156,7 @@ export class InfiniteList extends PolymerElement {
if (aboveScrollTopItemCount + this.chunkItemThreshold > if (aboveScrollTopItemCount + this.chunkItemThreshold >
this.domRepeat_.items.length) { this.domRepeat_.items.length) {
this.ensureDomItemsAvailableStartingAt_(aboveScrollTopItemCount); this.ensureDomItemsAvailableStartingAt_(aboveScrollTopItemCount);
this.updateScrollerSize_();
} }
} }
} }
...@@ -189,12 +191,13 @@ export class InfiniteList extends PolymerElement { ...@@ -189,12 +191,13 @@ export class InfiniteList extends PolymerElement {
* @private * @private
*/ */
domItemAverageHeight_() { domItemAverageHeight_() {
if (!this.$.selector.items || this.$.selector.items.length === 0) { const selector = /** @type {!IronSelectorElement} */ (this.$.selector);
if (!selector.items || selector.items.length === 0) {
return 0; return 0;
} }
const domItemCount = this.$.selector.items.length; const domItemCount = selector.items.length;
const lastDomItem = this.$.selector.items[domItemCount - 1]; const lastDomItem = selector.items[domItemCount - 1];
return (lastDomItem.offsetTop + lastDomItem.offsetHeight) / domItemCount; return (lastDomItem.offsetTop + lastDomItem.offsetHeight) / domItemCount;
} }
...@@ -202,17 +205,36 @@ export class InfiniteList extends PolymerElement { ...@@ -202,17 +205,36 @@ export class InfiniteList extends PolymerElement {
* Ensures that when the items property changes, only a chunk of the items * Ensures that when the items property changes, only a chunk of the items
* needed to fill the current scroll position view are added to the DOM, thus * needed to fill the current scroll position view are added to the DOM, thus
* improving rendering performance. * improving rendering performance.
*
* @param {!Array} newItems
* @param {!Array} oldItems
* @private * @private
*/ */
onItemsChanged_() { onItemsChanged_(newItems, oldItems) {
if (this.domRepeat_ && this.items) { if (!this.domRepeat_) {
const domItemAvgHeight = this.domItemAverageHeight_(); return;
const aboveScrollTopItemCount = domItemAvgHeight !== 0 ? }
Math.round(this.scrollTop / domItemAvgHeight) :
0;
if (!oldItems || oldItems.length === 0) {
this.domRepeat_.set('items', []); this.domRepeat_.set('items', []);
this.ensureDomItemsAvailableStartingAt_(aboveScrollTopItemCount); this.ensureDomItemsAvailableStartingAt_(0);
listenOnce(this.$.selector, 'iron-items-changed', () => {
this.updateScrollerSize_();
});
return;
}
updateListProperty(
this.domRepeat_, 'items', tabData => tabData,
newItems.slice(
0,
Math.min(
Math.max(this.domRepeat_.items.length, this.chunkItemCount),
newItems.length)),
true /* identityBasedUpdate= */);
if (newItems.length !== oldItems.length) {
this.updateScrollerSize_(); this.updateScrollerSize_();
} }
} }
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
/** @fileoverview Suite of tests for the ListPropertyUpdateBehavior. */ /** @fileoverview Suite of tests for the ListPropertyUpdateBehavior. */
// #import {ListPropertyUpdateBehavior} from 'chrome://resources/js/list_property_update_behavior.m.js'; // #import {ListPropertyUpdateBehavior, updateListProperty} from 'chrome://resources/js/list_property_update_behavior.m.js';
// #import {Polymer} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js'; // #import {Polymer} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
suite('ListPropertyUpdateBehavior', function() { suite('ListPropertyUpdateBehavior', function() {
...@@ -76,7 +76,7 @@ suite('ListPropertyUpdateBehavior', function() { ...@@ -76,7 +76,7 @@ suite('ListPropertyUpdateBehavior', function() {
updateComplexArray(newArray) { updateComplexArray(newArray) {
if (this.updateList( if (this.updateList(
'complexArray', x => x.letter, newArray, 'complexArray', x => x.letter, newArray,
true /* uidBasedUpdate */)) { true /* identityBasedUpdate */)) {
return {topArrayChanged: true, wordsArrayChanged: false}; return {topArrayChanged: true, wordsArrayChanged: false};
} }
...@@ -305,4 +305,48 @@ suite('ListPropertyUpdateBehavior', function() { ...@@ -305,4 +305,48 @@ suite('ListPropertyUpdateBehavior', function() {
assertTrue(testElement.updateList('complexArray', x => x.letter, newArray)); assertTrue(testElement.updateList('complexArray', x => x.letter, newArray));
assertDeepEquals(['apricot'], testElement.complexArray[0].words); assertDeepEquals(['apricot'], testElement.complexArray[0].words);
}); });
test('updateListProperty() function triggers notifySplices()', () => {
// Ensure that the array is updated when an element is removed from the
// end.
let elementRemoved = testElement.complexArray.slice(0, 2);
updateListProperty(
testElement, 'complexArray', obj => obj, elementRemoved, true);
assertComplexArrayEquals(testElement.complexArray, elementRemoved);
// Ensure that the array is updated when an element is removed from the
// beginning.
testElement.resetComplexArray();
elementRemoved = testElement.complexArray.slice(1);
updateListProperty(
testElement, 'complexArray', obj => obj, elementRemoved, true);
assertComplexArrayEquals(testElement.complexArray, elementRemoved);
// Ensure that the array is updated when an element is added to the end.
testElement.resetComplexArray();
let elementAdded = testElement.complexArray.slice();
elementAdded.push({letter: 'd', words: ['door', 'dice']});
updateListProperty(
testElement, 'complexArray', obj => obj, elementAdded, true);
assertComplexArrayEquals(testElement.complexArray, elementAdded);
// Ensure that the array is updated when an element is added to the
// beginning.
testElement.resetComplexArray();
elementAdded = [{letter: 'A', words: ['Alphabet']}];
elementAdded.push(...testElement.complexArray);
updateListProperty(
testElement, 'complexArray', obj => obj, elementAdded, true);
assertComplexArrayEquals(testElement.complexArray, elementAdded);
// Ensure that the array is updated when the entire array is different.
testElement.resetComplexArray();
const newArray = [
{letter: 'w', words: ['water', 'woods']},
{letter: 'x', words: ['xylophone']}, {letter: 'y', words: ['yo-yo']},
{letter: 'z', words: ['zebra', 'zephyr']}
];
updateListProperty(testElement, 'complexArray', obj => obj, newArray, true);
assertComplexArrayEquals(testElement.complexArray, newArray);
});
}); });
...@@ -87,6 +87,7 @@ suite('InfiniteListTest', () => { ...@@ -87,6 +87,7 @@ suite('InfiniteListTest', () => {
test('ScrollHeight', async () => { test('ScrollHeight', async () => {
const tabItems = sampleTabItems(sampleSiteNames()); const tabItems = sampleTabItems(sampleSiteNames());
await setupTest(tabItems); await setupTest(tabItems);
await waitAfterNextRender(infiniteList);
assertEquals(0, infiniteList.scrollTop); assertEquals(0, infiniteList.scrollTop);
...@@ -99,6 +100,28 @@ suite('InfiniteListTest', () => { ...@@ -99,6 +100,28 @@ suite('InfiniteListTest', () => {
assertEquals(tabItemHeight * tabItems.length, infiniteList.scrollHeight); assertEquals(tabItemHeight * tabItems.length, infiniteList.scrollHeight);
}); });
test('ListUpdates', async () => {
let siteNames = Array.from({length: 1}, (_, i) => 'site' + (i + 1));
const tabItems = sampleTabItems(siteNames);
await setupTest(tabItems);
assertEquals(1, queryRows().length);
// Ensure that on updating the list with an array smaller in size
// than the chunkItemCount property, all the array items are rendered.
siteNames = Array.from({length: 3}, (_, i) => 'site' + (i + 1));
infiniteList.items = sampleTabItems(siteNames);
await waitAfterNextRender(infiniteList);
assertEquals(3, queryRows().length);
// Ensure that on updating the list with an array greater in size than
// the chunkItemCount property, only a chunk of array items are rendered.
siteNames =
Array.from({length: 2 * CHUNK_ITEM_COUNT}, (_, i) => 'site' + (i + 1));
infiniteList.items = sampleTabItems(siteNames);
await waitAfterNextRender(infiniteList);
assertEquals(CHUNK_ITEM_COUNT, queryRows().length);
});
test('SelectedIndex', async () => { test('SelectedIndex', async () => {
const siteNames = Array.from({length: 50}, (_, i) => 'site' + (i + 1)); const siteNames = Array.from({length: 50}, (_, i) => 'site' + (i + 1));
const tabItems = sampleTabItems(siteNames); const tabItems = sampleTabItems(siteNames);
......
...@@ -21,44 +21,60 @@ ...@@ -21,44 +21,60 @@
/* #export */ const ListPropertyUpdateBehavior = { /* #export */ const ListPropertyUpdateBehavior = {
/** /**
* @param {string} propertyPath * @param {string} propertyPath
* @param {function(!Object): string} itemUidGetter * @param {function(!Object): (!Object|string)} identityGetter
* @param {!Array<!Object>} updatedList * @param {!Array<!Object>} updatedList
* @param {boolean} uidBasedUpdate * @param {boolean=} identityBasedUpdate
* @returns {boolean} True if notifySplices was called. * @returns {boolean} True if notifySplices was called.
*/ */
updateList(propertyPath, itemUidGetter, updatedList, uidBasedUpdate = false) { updateList(
const list = this.get(propertyPath); propertyPath, identityGetter, updatedList, identityBasedUpdate = false) {
const splices = Polymer.ArraySplice.calculateSplices( return updateListProperty(
updatedList.map(itemUidGetter), list.map(itemUidGetter)); this, propertyPath, identityGetter, updatedList, identityBasedUpdate);
},
};
splices.forEach(splice => { /**
const index = splice.index; * @param {Object} instance
const deleteCount = splice.removed.length; * @param {string} propertyPath
// Transform splices to the expected format of notifySplices(). * @param {function(!Object): (!Object|string)} identityGetter
// Convert !Array<string> to !Array<!Object>. * @param {!Array<!Object>} updatedList
splice.removed = list.slice(index, index + deleteCount); * @param {boolean=} identityBasedUpdate
splice.object = list; * @returns {boolean} True if notifySplices was called.
splice.type = 'splice'; */
/* #export */ function updateListProperty(
instance, propertyPath, identityGetter, updatedList,
identityBasedUpdate = false) {
const list = instance.get(propertyPath);
const splices = Polymer.ArraySplice.calculateSplices(
updatedList.map(identityGetter), list.map(identityGetter));
const added = updatedList.slice(index, index + splice.addedCount); splices.forEach(splice => {
const spliceParams = [index, deleteCount].concat(added); const index = splice.index;
list.splice.apply(list, spliceParams); const deleteCount = splice.removed.length;
}); // Transform splices to the expected format of notifySplices().
// Convert !Array<string> to !Array<!Object>.
splice.removed = list.slice(index, index + deleteCount);
splice.object = list;
splice.type = 'splice';
let updated = splices.length > 0; const added = updatedList.slice(index, index + splice.addedCount);
if (!uidBasedUpdate) { const spliceParams = [index, deleteCount].concat(added);
list.forEach((item, index) => { list.splice.apply(list, spliceParams);
const updatedItem = updatedList[index]; });
if (JSON.stringify(item) !== JSON.stringify(updatedItem)) {
this.set([propertyPath, index], updatedItem);
updated = true;
}
});
}
if (splices.length > 0) { let updated = splices.length > 0;
this.notifySplices(propertyPath, splices); if (!identityBasedUpdate) {
} list.forEach((item, index) => {
return updated; const updatedItem = updatedList[index];
}, if (JSON.stringify(item) !== JSON.stringify(updatedItem)) {
}; instance.set([propertyPath, index], updatedItem);
updated = true;
}
});
}
if (splices.length > 0) {
instance.notifySplices(propertyPath, splices);
}
return updated;
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment