From 9be44b4b315741347dc8925b919e4ad019150086 Mon Sep 17 00:00:00 2001 From: Doctorwu Date: Thu, 29 Feb 2024 17:07:21 +0800 Subject: [PATCH 1/5] fix(compiler-core, runtime-core): fix unexpected render in v-for & v-memo close #10392 --- packages/compiler-core/src/transforms/vFor.ts | 8 ++++++-- packages/runtime-core/src/helpers/renderList.ts | 10 ++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/compiler-core/src/transforms/vFor.ts b/packages/compiler-core/src/transforms/vFor.ts index 5d423ee2429..b41b8eff7a1 100644 --- a/packages/compiler-core/src/transforms/vFor.ts +++ b/packages/compiler-core/src/transforms/vFor.ts @@ -205,14 +205,18 @@ export const transformFor = createStructuralDirectiveTransform( if (memo) { const loop = createFunctionExpression( createForLoopParams(forNode.parseResult, [ - createSimpleExpression(`_cached`), + createSimpleExpression(`_cachedMap`), ]), ) loop.body = createBlockStatement([ createCompoundExpression([`const _memo = (`, memo.exp!, `)`]), + createCompoundExpression([ + `const _cached = _cachedMap && _cachedMap.get(`, + keyExp!, + `)`, + ]), createCompoundExpression([ `if (_cached`, - ...(keyExp ? [` && _cached.key === `, keyExp] : []), ` && ${context.helperString( IS_MEMO_SAME, )}(_cached, _memo)) return _cached`, diff --git a/packages/runtime-core/src/helpers/renderList.ts b/packages/runtime-core/src/helpers/renderList.ts index 655435fdd7a..a11f003db2e 100644 --- a/packages/runtime-core/src/helpers/renderList.ts +++ b/packages/runtime-core/src/helpers/renderList.ts @@ -58,11 +58,13 @@ export function renderList( ): VNodeChild[] { let ret: VNodeChild[] const cached = (cache && cache[index!]) as VNode[] | undefined + const cachedMap = + cached && new Map(cached.map((vnode, i) => [vnode.key, vnode])) if (isArray(source) || isString(source)) { ret = new Array(source.length) for (let i = 0, l = source.length; i < l; i++) { - ret[i] = renderItem(source[i], i, undefined, cached && cached[i]) + ret[i] = renderItem(source[i], i, undefined, cachedMap) } } else if (typeof source === 'number') { if (__DEV__ && !Number.isInteger(source)) { @@ -70,19 +72,19 @@ export function renderList( } ret = new Array(source) for (let i = 0; i < source; i++) { - ret[i] = renderItem(i + 1, i, undefined, cached && cached[i]) + ret[i] = renderItem(i + 1, i, undefined, cachedMap) } } 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]), + renderItem(item, i, undefined, cachedMap), ) } 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]) + ret[i] = renderItem(source[key], key, i, cachedMap) } } } else { From 9740640c875a14306900f493b919791dcd2d387a Mon Sep 17 00:00:00 2001 From: Doctorwu Date: Thu, 29 Feb 2024 18:12:33 +0800 Subject: [PATCH 2/5] fix: fix no-key case --- .../__snapshots__/vMemo.spec.ts.snap | 10 ++-- packages/compiler-core/src/transforms/vFor.ts | 11 +++- .../runtime-core/src/helpers/renderList.ts | 59 +++++++++++++++---- 3 files changed, 62 insertions(+), 18 deletions(-) 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 220bc177418..c07638a3cba 100644 --- a/packages/compiler-core/__tests__/transforms/__snapshots__/vMemo.spec.ts.snap +++ b/packages/compiler-core/__tests__/transforms/__snapshots__/vMemo.spec.ts.snap @@ -35,9 +35,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)) + if (_cached && _isMemoSame(_cached, _memo)) return _cached const _item = (_openBlock(), _createElementBlock("span", { key: x }, "foobar")) _item.memo = _memo return _item @@ -51,9 +52,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)) + 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 b41b8eff7a1..0a9c5976151 100644 --- a/packages/compiler-core/src/transforms/vFor.ts +++ b/packages/compiler-core/src/transforms/vFor.ts @@ -205,15 +205,20 @@ export const transformFor = createStructuralDirectiveTransform( if (memo) { const loop = createFunctionExpression( createForLoopParams(forNode.parseResult, [ + createSimpleExpression(`_cachedListItem`), createSimpleExpression(`_cachedMap`), ]), ) loop.body = createBlockStatement([ createCompoundExpression([`const _memo = (`, memo.exp!, `)`]), createCompoundExpression([ - `const _cached = _cachedMap && _cachedMap.get(`, - keyExp!, - `)`, + `const _cached = (`, + ...(keyExp + ? [`(_cachedMap && _cachedMap[`, keyExp!, `]) || `] + : []), + `(_cachedListItem`, + ...(keyExp ? [` && _cachedListItem.key === `, keyExp] : []), + `))`, ]), createCompoundExpression([ `if (_cached`, diff --git a/packages/runtime-core/src/helpers/renderList.ts b/packages/runtime-core/src/helpers/renderList.ts index a11f003db2e..fc7d6cf858b 100644 --- a/packages/runtime-core/src/helpers/renderList.ts +++ b/packages/runtime-core/src/helpers/renderList.ts @@ -1,4 +1,5 @@ import type { VNode, VNodeChild } from '../vnode' +import { isVNode } from '../vnode' import { isArray, isObject, isString } from '@vue/shared' import { warn } from '../warning' @@ -53,18 +54,33 @@ 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 cachedMap = - cached && new Map(cached.map((vnode, i) => [vnode.key, vnode])) + const retMap: Record = {} + const cachedList = (cache && cache[index!]?.list) as VNode[] | undefined + const cachedMap = (cache && cache[index!]?.map) as + | Record + | undefined if (isArray(source) || isString(source)) { ret = new Array(source.length) for (let i = 0, l = source.length; i < l; i++) { - ret[i] = renderItem(source[i], i, undefined, cachedMap) + const item = renderItem( + source[i], + i, + undefined, + 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)) { @@ -72,19 +88,37 @@ export function renderList( } ret = new Array(source) for (let i = 0; i < source; i++) { - ret[i] = renderItem(i + 1, i, undefined, cachedMap) + const item = renderItem(i + 1, i, undefined, 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, cachedMap), - ) + ret = Array.from(source as Iterable, (sourceItem, i) => { + const item = renderItem( + sourceItem, + i, + undefined, + 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, cachedMap) + const item = renderItem(source[key], key, i, cachedList?.[i], cachedMap) + if (isVNode(item) && item.key != null) { + retMap[item.key] = item + } + ret[i] = item } } } else { @@ -92,7 +126,10 @@ export function renderList( } if (cache) { - cache[index!] = ret + cache[index!] = { + list: ret, + map: retMap, + } } return ret } From e6463097824d937f10558fdc90a34d794db56720 Mon Sep 17 00:00:00 2001 From: Doctorwu Date: Thu, 29 Feb 2024 18:28:12 +0800 Subject: [PATCH 3/5] fix: fix cache list item logic --- .../__tests__/transforms/__snapshots__/vMemo.spec.ts.snap | 4 ++-- packages/compiler-core/src/transforms/vFor.ts | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) 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 c07638a3cba..898f18319e1 100644 --- a/packages/compiler-core/__tests__/transforms/__snapshots__/vMemo.spec.ts.snap +++ b/packages/compiler-core/__tests__/transforms/__snapshots__/vMemo.spec.ts.snap @@ -37,7 +37,7 @@ export function render(_ctx, _cache) { return (_openBlock(), _createElementBlock("div", null, [ (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.list, ({ x, y }, __, ___, _cachedListItem, _cachedMap) => { const _memo = ([x, y === _ctx.z]) - const _cached = ((_cachedMap && _cachedMap[x]) || (_cachedListItem && _cachedListItem.key === x)) + 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 @@ -54,7 +54,7 @@ export function render(_ctx, _cache) { return (_openBlock(), _createElementBlock("div", null, [ (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.list, ({ x, y }, __, ___, _cachedListItem, _cachedMap) => { const _memo = ([x, y === _ctx.z]) - const _cached = ((_cachedMap && _cachedMap[x]) || (_cachedListItem && _cachedListItem.key === x)) + 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 0a9c5976151..900d0c3ea84 100644 --- a/packages/compiler-core/src/transforms/vFor.ts +++ b/packages/compiler-core/src/transforms/vFor.ts @@ -217,7 +217,13 @@ export const transformFor = createStructuralDirectiveTransform( ? [`(_cachedMap && _cachedMap[`, keyExp!, `]) || `] : []), `(_cachedListItem`, - ...(keyExp ? [` && _cachedListItem.key === `, keyExp] : []), + ...(keyExp + ? [ + ` && _cachedListItem.key === `, + keyExp, + ` && _cachedListItem`, + ] + : []), `))`, ]), createCompoundExpression([ From 6eb55b8150cd2c3f9a457f3c52e1887a6117b68a Mon Sep 17 00:00:00 2001 From: Doctorwu Date: Fri, 1 Mar 2024 10:03:12 +0800 Subject: [PATCH 4/5] feat: add test case --- .../__tests__/helpers/withMemo.spec.ts | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/runtime-core/__tests__/helpers/withMemo.spec.ts b/packages/runtime-core/__tests__/helpers/withMemo.spec.ts index f4f356dea4f..47d45437a07 100644 --- a/packages/runtime-core/__tests__/helpers/withMemo.spec.ts +++ b/packages/runtime-core/__tests__/helpers/withMemo.spec.ts @@ -215,6 +215,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 }}
`, From e74929c44878dfa2883ebbe2649e55cf89c80e67 Mon Sep 17 00:00:00 2001 From: Doctor Wu Date: Wed, 16 Oct 2024 16:21:42 +0800 Subject: [PATCH 5/5] test(runtime-core): add test case for use v-memo on v-for elements --- .../__tests__/helpers/withMemo.spec.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/runtime-core/__tests__/helpers/withMemo.spec.ts b/packages/runtime-core/__tests__/helpers/withMemo.spec.ts index 42ca0c93a24..c6c9fa3e32f 100644 --- a/packages/runtime-core/__tests__/helpers/withMemo.spec.ts +++ b/packages/runtime-core/__tests__/helpers/withMemo.spec.ts @@ -267,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) + }) })