diff --git a/packages/runtime-vapor/__tests__/for.spec.ts b/packages/runtime-vapor/__tests__/for.spec.ts index 8a2d88a930d..f12ec8d8e05 100644 --- a/packages/runtime-vapor/__tests__/for.spec.ts +++ b/packages/runtime-vapor/__tests__/for.spec.ts @@ -426,12 +426,12 @@ describe('createFor', () => { '
  • 0. 1
  • 1. 2
  • 2. 3
  • 3. 4
  • ', ) - // change deep value should not update + // change deep value and refresh source list.value[0].name = 'a' setList() await nextTick() expect(host.innerHTML).toBe( - '
  • 0. 1
  • 1. 2
  • 2. 3
  • 3. 4
  • ', + '
  • 0. a
  • 1. 2
  • 2. 3
  • 3. 4
  • ', ) // remove @@ -439,7 +439,7 @@ describe('createFor', () => { setList() await nextTick() expect(host.innerHTML).toBe( - '
  • 0. 1
  • 1. 3
  • 2. 4
  • ', + '
  • 0. a
  • 1. 3
  • 2. 4
  • ', ) // clear @@ -448,6 +448,82 @@ describe('createFor', () => { expect(host.innerHTML).toBe('') }) + test('should update same item references when source is refreshed', async () => { + let rawList = [{ number: 0 }, { number: 1 }] + const list = shallowRef(rawList) + + const { host } = define(() => { + const n1 = createFor( + () => list.value, + item => { + const span = document.createElement('li') + renderEffect(() => { + span.textContent = JSON.stringify(item.value) + }) + return span + }, + (_item, key) => `${key}-test`, + ) + return n1 + }).render() + + expect(host.innerHTML).toBe( + '
  • {"number":0}
  • {"number":1}
  • ', + ) + + rawList[0].number = 2 + list.value = rawList.slice() + await nextTick() + expect(host.innerHTML).toBe( + '
  • {"number":2}
  • {"number":1}
  • ', + ) + + list.value[0].number = 3 + triggerRef(list) + await nextTick() + expect(host.innerHTML).toBe( + '
  • {"number":3}
  • {"number":1}
  • ', + ) + + rawList = [{ number: 0 }, { number: 1 }] + list.value = rawList + await nextTick() + expect(host.innerHTML).toBe( + '
  • {"number":0}
  • {"number":1}
  • ', + ) + }) + + test('should update same reactive item references when source is replaced', async () => { + const rawList = [{ number: 0 }, { number: 1 }] + const list = ref(rawList) + + const { host } = define(() => { + const n1 = createFor( + () => list.value, + item => { + const span = document.createElement('li') + renderEffect(() => { + span.textContent = JSON.stringify(item.value) + }) + return span + }, + (_item, key) => `${key}-test`, + ) + return n1 + }).render() + + expect(host.innerHTML).toBe( + '
  • {"number":0}
  • {"number":1}
  • ', + ) + + rawList[0].number = 2 + list.value = rawList.slice() + await nextTick() + expect(host.innerHTML).toBe( + '
  • {"number":2}
  • {"number":1}
  • ', + ) + }) + test('should optimize call frequency during list operations', async () => { let sourceCalledTimes = 0 let renderCalledTimes = 0 @@ -519,6 +595,14 @@ describe('createFor', () => { await nextTick() expectCalledTimesToBe('Update every 10th row', 0, 0, length() / 10, 0) + // Replace a row with the same key + list.value[0] = { + id: list.value[0].id, + label: list.value[0].label + 10000, + } + await nextTick() + expectCalledTimesToBe('Replace a row with the same key', 1, 0, 1, 0) + // Append rows list.value.push(...createItems(100)) await nextTick() @@ -638,6 +722,15 @@ describe('createFor', () => { await nextTick() expectCalledTimesToBe('Update every 10th row', 0, 0, length() / 10, 0) + // Replace a row with the same key + list.value[0] = { + id: list.value[0].id, + label: shallowRef(list.value[0].label.value + 10000), + } + triggerRef(list) + await nextTick() + expectCalledTimesToBe('Replace a row with the same key', 1, 0, 1, 0) + // Append rows list.value.push(...createItems(100)) triggerRef(list) diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts index 6936e036b1c..f108afd0fdb 100644 --- a/packages/runtime-vapor/src/apiCreateFor.ts +++ b/packages/runtime-vapor/src/apiCreateFor.ts @@ -9,6 +9,7 @@ import { shallowRef, toReactive, toReadonly, + triggerRef, watch, } from '@vue/reactivity' import { isArray, isObject, isString } from '@vue/shared' @@ -164,8 +165,11 @@ export const createFor = ( } else if (!getKey) { // unkeyed fast path const commonLength = Math.min(newLength, oldLength) + let shouldTriggerSameItems = oldLength === newLength for (let i = 0; i < commonLength; i++) { - update((newBlocks[i] = oldBlocks[i]), getItem(source, i)[0]) + if (update((newBlocks[i] = oldBlocks[i]), getItem(source, i)[0])) { + shouldTriggerSameItems = false + } } for (let i = oldLength; i < newLength; i++) { mount(source, i) @@ -173,6 +177,9 @@ export const createFor = ( for (let i = newLength; i < oldLength; i++) { unmount(oldBlocks[i]) } + if (shouldTriggerSameItems) { + triggerSameItemObjectRefs(newBlocks) + } } else { if (__DEV__) { const keyToIndexMap: Map = new Map() @@ -203,6 +210,7 @@ export const createFor = ( let endOffset = 0 let queuedBlocksLength = 0 let oldKeyIndexPairsLength = 0 + let shouldTriggerSameItems = oldLength === newLength while (endOffset < commonLength) { const index = newLength - endOffset - 1 @@ -210,10 +218,13 @@ export const createFor = ( const key = getKey(...item) const existingBlock = oldBlocks[oldLength - endOffset - 1] if (existingBlock.key !== key) break - update(existingBlock, ...item) + if (update(existingBlock, ...item)) { + shouldTriggerSameItems = false + } newBlocks[index] = existingBlock endOffset++ } + if (endOffset !== commonLength) shouldTriggerSameItems = false const e1 = commonLength - endOffset const e2 = oldLength - endOffset @@ -357,9 +368,11 @@ export const createFor = ( block.prevAnchor = block.next = block.prev = undefined } } + if (shouldTriggerSameItems) { + triggerSameItemObjectRefs(newBlocks) + } } } - frag.nodes = [(oldBlocks = newBlocks)] if (parentAnchor) frag.nodes.push(parentAnchor) @@ -512,7 +525,8 @@ export const createFor = ( newKey?: any, newIndex?: any, ) => { - if (newItem !== itemRef.value) { + const itemChanged = newItem !== itemRef.value + if (itemChanged) { itemRef.value = newItem } if (keyRef && newKey !== undefined && newKey !== keyRef.value) { @@ -521,6 +535,14 @@ export const createFor = ( if (indexRef && newIndex !== undefined && newIndex !== indexRef.value) { indexRef.value = newIndex } + return itemChanged + } + + function triggerSameItemObjectRefs(blocks: ForBlock[]): void { + for (let i = 0; i < blocks.length; i++) { + const itemRef = blocks[i].itemRef + if (isObject(itemRef.value)) triggerRef(itemRef) + } } const unmount = (block: ForBlock, doRemove = true, doDeregister = true) => {