diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/vMemo.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/vMemo.spec.ts.snap
index 86e0b3d2fd5..eb825c9d78c 100644
--- a/packages/compiler-core/__tests__/transforms/__snapshots__/vMemo.spec.ts.snap
+++ b/packages/compiler-core/__tests__/transforms/__snapshots__/vMemo.spec.ts.snap
@@ -53,9 +53,10 @@ exports[`compiler: v-memo transform > on template v-for 1`] = `
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [
- (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.list, ({ x, y }, __, ___, _cached) => {
+ (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.list, ({ x, y }, __, ___, _cachedListItem, _cachedMap) => {
const _memo = ([x, y === _ctx.z])
- if (_cached && _cached.key === x && _isMemoSame(_cached, _memo)) return _cached
+ const _cached = ((_cachedMap && _cachedMap[x]) || (_cachedListItem && _cachedListItem.key === x && _cachedListItem))
+ if (_cached && _isMemoSame(_cached, _memo)) return _cached
const _item = (_openBlock(), _createElementBlock("span", { key: x }, "foobar"))
_item.memo = _memo
return _item
@@ -69,9 +70,10 @@ exports[`compiler: v-memo transform > on v-for 1`] = `
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [
- (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.list, ({ x, y }, __, ___, _cached) => {
+ (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.list, ({ x, y }, __, ___, _cachedListItem, _cachedMap) => {
const _memo = ([x, y === _ctx.z])
- if (_cached && _cached.key === x && _isMemoSame(_cached, _memo)) return _cached
+ const _cached = ((_cachedMap && _cachedMap[x]) || (_cachedListItem && _cachedListItem.key === x && _cachedListItem))
+ if (_cached && _isMemoSame(_cached, _memo)) return _cached
const _item = (_openBlock(), _createElementBlock("div", { key: x }, [
_createElementVNode("span", null, "foobar")
]))
diff --git a/packages/compiler-core/src/transforms/vFor.ts b/packages/compiler-core/src/transforms/vFor.ts
index 0dca0ba9ab4..4c42d060642 100644
--- a/packages/compiler-core/src/transforms/vFor.ts
+++ b/packages/compiler-core/src/transforms/vFor.ts
@@ -220,14 +220,29 @@ export const transformFor: NodeTransform = createStructuralDirectiveTransform(
if (memo) {
const loop = createFunctionExpression(
createForLoopParams(forNode.parseResult, [
- createSimpleExpression(`_cached`),
+ createSimpleExpression(`_cachedListItem`),
+ createSimpleExpression(`_cachedMap`),
]),
)
loop.body = createBlockStatement([
createCompoundExpression([`const _memo = (`, memo.exp!, `)`]),
+ createCompoundExpression([
+ `const _cached = (`,
+ ...(keyExp
+ ? [`(_cachedMap && _cachedMap[`, keyExp!, `]) || `]
+ : []),
+ `(_cachedListItem`,
+ ...(keyExp
+ ? [
+ ` && _cachedListItem.key === `,
+ keyExp,
+ ` && _cachedListItem`,
+ ]
+ : []),
+ `))`,
+ ]),
createCompoundExpression([
`if (_cached`,
- ...(keyExp ? [` && _cached.key === `, keyExp] : []),
` && ${context.helperString(
IS_MEMO_SAME,
)}(_cached, _memo)) return _cached`,
diff --git a/packages/runtime-core/__tests__/helpers/withMemo.spec.ts b/packages/runtime-core/__tests__/helpers/withMemo.spec.ts
index 32f89b1d8e9..c6c9fa3e32f 100644
--- a/packages/runtime-core/__tests__/helpers/withMemo.spec.ts
+++ b/packages/runtime-core/__tests__/helpers/withMemo.spec.ts
@@ -226,6 +226,35 @@ describe('v-memo', () => {
expect(el.innerHTML).toBe(`
2
2
2
`)
})
+ test('on v-for /w should memo keyd vnode', async () => {
+ const runner = vitest.fn()
+ const [el, vm] = mount({
+ template: `
+ {{item.id}}{{ runner() }}
+
`,
+ data: () => ({
+ list: [{ id: 1 }, { id: 2 }, { id: 3 }],
+ }),
+ methods: {
+ runner,
+ },
+ })
+ expect(el.innerHTML).toBe(`1
2
3
`)
+ expect(runner).toHaveBeenCalledTimes(3)
+
+ vm.list = [{ id: 1 }, { id: 3 }]
+ await nextTick()
+ // should not re evaluate runner
+ expect(el.innerHTML).toBe(`1
3
`)
+ expect(runner).toHaveBeenCalledTimes(3)
+
+ vm.list = [{ id: 1 }, { id: 3 }, { id: 2 }]
+ await nextTick()
+ // should only evaluate the new item
+ expect(el.innerHTML).toBe(`1
3
2
`)
+ expect(runner).toHaveBeenCalledTimes(4)
+ })
+
test('v-memo dependency is NaN should be equal', async () => {
const [el, vm] = mount({
template: `{{ y }}
`,
@@ -238,4 +267,24 @@ describe('v-memo', () => {
await nextTick()
expect(el.innerHTML).toBe(`0
`)
})
+
+ test('should cache results correctly when use v-memo on the v-for element', async () => {
+ const runner = vi.fn()
+ const [_, vm] = mount({
+ template: `
+ {{item.id}}{{ runner() }}
+ `,
+ data: () => ({
+ list: new Array(10).fill(0).map((_, i) => i),
+ }),
+ methods: {
+ runner,
+ },
+ })
+ expect(runner).toHaveBeenCalledTimes(10)
+
+ vm.list[5] = -1
+ await nextTick()
+ expect(runner).toHaveBeenCalledTimes(11)
+ })
})
diff --git a/packages/runtime-core/src/helpers/renderList.ts b/packages/runtime-core/src/helpers/renderList.ts
index bbcbcc13044..fff10c95a92 100644
--- a/packages/runtime-core/src/helpers/renderList.ts
+++ b/packages/runtime-core/src/helpers/renderList.ts
@@ -1,4 +1,4 @@
-import type { VNode, VNodeChild } from '../vnode'
+import { type VNode, type VNodeChild, isVNode } from '../vnode'
import {
isReactive,
isShallow,
@@ -59,11 +59,20 @@ export function renderList(
export function renderList(
source: any,
renderItem: (...args: any[]) => VNodeChild,
- cache?: any[],
+ cache?: {
+ list: any[]
+ map: Record
+ }[],
index?: number,
): VNodeChild[] {
let ret: VNodeChild[]
- const cached = (cache && cache[index!]) as VNode[] | undefined
+ const retMap: Record = {}
+ const cachedList = (cache && cache[index!] && cache[index!].list) as
+ | VNode[]
+ | undefined
+ const cachedMap = (cache && cache[index!] && cache[index!].map) as
+ | Record
+ | undefined
const sourceIsArray = isArray(source)
if (sourceIsArray || isString(source)) {
@@ -75,12 +84,17 @@ export function renderList(
}
ret = new Array(source.length)
for (let i = 0, l = source.length; i < l; i++) {
- ret[i] = renderItem(
+ const item = renderItem(
needsWrap ? toReactive(source[i]) : source[i],
i,
undefined,
- cached && cached[i],
+ cachedList && cachedList[i],
+ cachedMap,
)
+ if (isVNode(item) && item!.key != null) {
+ retMap[item!.key] = item!
+ }
+ ret[i] = item
}
} else if (typeof source === 'number') {
if (__DEV__ && !Number.isInteger(source)) {
@@ -88,19 +102,49 @@ export function renderList(
}
ret = new Array(source)
for (let i = 0; i < source; i++) {
- ret[i] = renderItem(i + 1, i, undefined, cached && cached[i])
+ const item = renderItem(
+ i + 1,
+ i,
+ undefined,
+ cachedList && cachedList[i],
+ cachedMap,
+ )
+ if (isVNode(item) && item.key != null) {
+ retMap[item.key] = item
+ }
+ ret[i] = item
}
} else if (isObject(source)) {
if (source[Symbol.iterator as any]) {
- ret = Array.from(source as Iterable, (item, i) =>
- renderItem(item, i, undefined, cached && cached[i]),
- )
+ ret = Array.from(source as Iterable, (sourceItem, i) => {
+ const item = renderItem(
+ sourceItem,
+ i,
+ undefined,
+ cachedList && cachedList[i],
+ cachedMap,
+ )
+ if (isVNode(item) && item.key != null) {
+ retMap[item.key] = item
+ }
+ return item
+ })
} else {
const keys = Object.keys(source)
ret = new Array(keys.length)
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
- ret[i] = renderItem(source[key], key, i, cached && cached[i])
+ const item = renderItem(
+ source[key],
+ key,
+ i,
+ cachedList && cachedList[i],
+ cachedMap,
+ )
+ if (isVNode(item) && item.key != null) {
+ retMap[item.key] = item
+ }
+ ret[i] = item
}
}
} else {
@@ -108,7 +152,10 @@ export function renderList(
}
if (cache) {
- cache[index!] = ret
+ cache[index!] = {
+ list: ret,
+ map: retMap,
+ }
}
return ret
}