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. 11. 22. 33. 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. 11. 22. 33. 4',
+ '0. a1. 22. 33. 4',
)
// remove
@@ -439,7 +439,7 @@ describe('createFor', () => {
setList()
await nextTick()
expect(host.innerHTML).toBe(
- '0. 11. 32. 4',
+ '0. a1. 32. 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) => {