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: ``, + 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 }