Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
9be44b4
fix(compiler-core, runtime-core): fix unexpected render in v-for & v-…
Doctor-wu Feb 29, 2024
86056b7
Merge branch 'main' into fix-v-for-memo
Doctor-wu Feb 29, 2024
9740640
fix: fix no-key case
Doctor-wu Feb 29, 2024
e646309
fix: fix cache list item logic
Doctor-wu Feb 29, 2024
6eb55b8
feat: add test case
Doctor-wu Mar 1, 2024
acd88b9
Merge branch 'main' into fix-v-for-memo
Doctor-wu Mar 7, 2024
f214d64
Merge branch 'main' into fix-v-for-memo
Doctor-wu Mar 14, 2024
da9db3d
Merge branch 'main' into fix-v-for-memo
Doctor-wu Mar 17, 2024
55cb6e3
Merge branch 'main' into fix-v-for-memo
Doctor-wu Mar 18, 2024
9f6838d
Merge branch 'main' into fix-v-for-memo
Doctor-wu Mar 18, 2024
8770512
Merge branch 'main' into fix-v-for-memo
Doctor-wu Mar 21, 2024
3b5e6c6
Merge branch 'main' into fix-v-for-memo
Doctor-wu Apr 14, 2024
65a4901
Merge branch 'main' into fix-v-for-memo
Doctor-wu Apr 15, 2024
86f29dc
Merge branch 'main' into fix-v-for-memo
Doctor-wu Apr 16, 2024
d91cd53
Merge branch 'main' into fix-v-for-memo
Doctor-wu Apr 22, 2024
2e5ef6f
Merge branch 'main' into fix-v-for-memo
Doctor-wu Apr 23, 2024
0ca128c
Merge branch 'main' into fix-v-for-memo
Doctor-wu Apr 29, 2024
3c8dae1
Merge branch 'main' into fix-v-for-memo
Doctor-wu May 5, 2024
34ca523
Merge branch 'main' into fix-v-for-memo
Doctor-wu Jun 24, 2024
ba2cefa
feat(runtime-core): Merge branch 'main' into fix-v-for-memo
Doctor-wu Oct 16, 2024
e74929c
test(runtime-core): add test case for use v-memo on v-for elements
Doctor-wu Oct 16, 2024
45bcf83
Merge branch 'main' into fix-v-for-memo
Doctor-wu Oct 17, 2024
5167f16
Merge branch 'main' into fix-v-for-memo
Doctor-wu Nov 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
]))
Expand Down
19 changes: 17 additions & 2 deletions packages/compiler-core/src/transforms/vFor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down
49 changes: 49 additions & 0 deletions packages/runtime-core/__tests__/helpers/withMemo.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,35 @@ describe('v-memo', () => {
expect(el.innerHTML).toBe(`<div>2</div><div>2</div><div>2</div>`)
})

test('on v-for /w should memo keyd vnode', async () => {
const runner = vitest.fn()
const [el, vm] = mount({
template: `<div v-for="item in list" :key="item.id" v-memo="[item.id]">
{{item.id}}{{ runner() }}
</div>`,
data: () => ({
list: [{ id: 1 }, { id: 2 }, { id: 3 }],
}),
methods: {
runner,
},
})
expect(el.innerHTML).toBe(`<div>1</div><div>2</div><div>3</div>`)
expect(runner).toHaveBeenCalledTimes(3)

vm.list = [{ id: 1 }, { id: 3 }]
await nextTick()
// should not re evaluate runner
expect(el.innerHTML).toBe(`<div>1</div><div>3</div>`)
expect(runner).toHaveBeenCalledTimes(3)

vm.list = [{ id: 1 }, { id: 3 }, { id: 2 }]
await nextTick()
// should only evaluate the new item
expect(el.innerHTML).toBe(`<div>1</div><div>3</div><div>2</div>`)
expect(runner).toHaveBeenCalledTimes(4)
})

test('v-memo dependency is NaN should be equal', async () => {
const [el, vm] = mount({
template: `<div v-memo="[x]">{{ y }}</div>`,
Expand All @@ -238,4 +267,24 @@ describe('v-memo', () => {
await nextTick()
expect(el.innerHTML).toBe(`<div>0</div>`)
})

test('should cache results correctly when use v-memo on the v-for element', async () => {
const runner = vi.fn()
const [_, vm] = mount({
template: `<template v-for="item in list" :key="item" v-memo="[item]">
{{item.id}}{{ runner() }}
</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)
})
})
69 changes: 58 additions & 11 deletions packages/runtime-core/src/helpers/renderList.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { VNode, VNodeChild } from '../vnode'
import { type VNode, type VNodeChild, isVNode } from '../vnode'
import {
isReactive,
isShallow,
Expand Down Expand Up @@ -59,11 +59,20 @@ export function renderList<T>(
export function renderList(
source: any,
renderItem: (...args: any[]) => VNodeChild,
cache?: any[],
cache?: {
list: any[]
map: Record<string | number | symbol, VNode>
}[],
index?: number,
): VNodeChild[] {
let ret: VNodeChild[]
const cached = (cache && cache[index!]) as VNode[] | undefined
const retMap: Record<string | number | symbol, VNode> = {}
const cachedList = (cache && cache[index!] && cache[index!].list) as
| VNode[]
| undefined
const cachedMap = (cache && cache[index!] && cache[index!].map) as
| Record<string | number | symbol, VNode>
| undefined
const sourceIsArray = isArray(source)

if (sourceIsArray || isString(source)) {
Expand All @@ -75,40 +84,78 @@ 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)) {
warn(`The v-for range expect an integer value but got ${source}.`)
}
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<any>, (item, i) =>
renderItem(item, i, undefined, cached && cached[i]),
)
ret = Array.from(source as Iterable<any>, (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 {
ret = []
}

if (cache) {
cache[index!] = ret
cache[index!] = {
list: ret,
map: retMap,
}
}
return ret
}
Loading