Skip to content
This repository was archived by the owner on Jul 19, 2025. It is now read-only.

Commit 884c190

Browse files
authored
feat: v-memo for v-for (#276)
1 parent cc58f65 commit 884c190

File tree

9 files changed

+157
-71
lines changed

9 files changed

+157
-71
lines changed

benchmark/client/App.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ const isSelected = createSelector(selected)
113113
v-for="row of rows"
114114
:key="row.id"
115115
:class="{ danger: isSelected(row.id) }"
116+
v-memo="[row.label, row.id === selected]"
116117
>
117118
<td>{{ row.id }}</td>
118119
<td>
Lines changed: 76 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { walkIdentifiers } from '@vue/compiler-dom'
1+
import { type SimpleExpressionNode, walkIdentifiers } from '@vue/compiler-dom'
22
import { genBlock } from './block'
33
import { genExpression } from './expression'
44
import type { CodegenContext } from '../generate'
@@ -16,75 +16,21 @@ export function genFor(
1616
context: CodegenContext,
1717
): CodeFragment[] {
1818
const { vaporHelper } = context
19-
const { source, value, key, index, render, keyProp, once, id } = oper
19+
const { source, value, key, index, render, keyProp, once, id, memo } = oper
2020

2121
let isDestructureAssignment = false
2222
let rawValue: string | null = null
2323
const rawKey = key && key.content
2424
const rawIndex = index && index.content
2525

2626
const sourceExpr = ['() => (', ...genExpression(source, context), ')']
27-
28-
const idsOfValue = new Set<string>()
29-
if (value) {
30-
rawValue = value && value.content
31-
if ((isDestructureAssignment = !!value.ast)) {
32-
walkIdentifiers(
33-
value.ast,
34-
(id, _, __, ___, isLocal) => {
35-
if (isLocal) idsOfValue.add(id.name)
36-
},
37-
true,
38-
)
39-
} else {
40-
idsOfValue.add(rawValue)
41-
}
42-
}
43-
44-
const [depth, exitScope] = context.enterScope()
45-
let propsName: string
46-
const idMap: Record<string, string | null> = {}
47-
if (context.options.prefixIdentifiers) {
48-
propsName = `_ctx${depth}`
49-
Array.from(idsOfValue).forEach(
50-
(id, idIndex) => (idMap[id] = `${propsName}[${idIndex}].value`),
51-
)
52-
if (rawKey) idMap[rawKey] = `${propsName}[${idsOfValue.size}].value`
53-
if (rawIndex) idMap[rawIndex] = `${propsName}[${idsOfValue.size + 1}].value`
54-
} else {
55-
propsName = `[${[rawValue || ((rawKey || rawIndex) && '_'), rawKey || (rawIndex && '__'), rawIndex].filter(Boolean).join(', ')}]`
56-
}
57-
58-
let blockFn = context.withId(
59-
() => genBlock(render, context, [propsName]),
60-
idMap,
61-
)
62-
exitScope()
63-
64-
let getKeyFn: CodeFragment[] | false = false
65-
if (keyProp) {
66-
const idMap: Record<string, null> = {}
67-
if (rawKey) idMap[rawKey] = null
68-
if (rawIndex) idMap[rawIndex] = null
69-
idsOfValue.forEach(id => (idMap[id] = null))
70-
71-
const expr = context.withId(() => genExpression(keyProp, context), idMap)
72-
getKeyFn = [
73-
...genMulti(
74-
['(', ')', ', '],
75-
rawValue ? rawValue : rawKey || rawIndex ? '_' : undefined,
76-
rawKey ? rawKey : rawIndex ? '__' : undefined,
77-
rawIndex,
78-
),
79-
' => (',
80-
...expr,
81-
')',
82-
]
83-
}
27+
const idsInValue = getIdsInValue()
28+
let blockFn = genBlockFn()
29+
const simpleIdMap: Record<string, null> = genSimpleIdMap()
8430

8531
if (isDestructureAssignment) {
8632
const idMap: Record<string, null> = {}
87-
idsOfValue.forEach(id => (idMap[id] = null))
33+
idsInValue.forEach(id => (idMap[id] = null))
8834
if (rawKey) idMap[rawKey] = null
8935
if (rawIndex) idMap[rawIndex] = null
9036
const destructureAssignmentFn: CodeFragment[] = [
@@ -96,7 +42,7 @@ export function genFor(
9642
rawIndex,
9743
),
9844
') => ',
99-
...genMulti(DELIMITERS_ARRAY, ...idsOfValue, rawKey, rawIndex),
45+
...genMulti(DELIMITERS_ARRAY, ...idsInValue, rawKey, rawIndex),
10046
]
10147

10248
blockFn = genCall(
@@ -113,10 +59,77 @@ export function genFor(
11359
vaporHelper('createFor'),
11460
sourceExpr,
11561
blockFn,
116-
getKeyFn,
117-
false, // todo: getMemo
62+
genCallback(keyProp),
63+
genCallback(memo),
11864
false, // todo: hydrationNode
11965
once && 'true',
12066
),
12167
]
68+
69+
function getIdsInValue() {
70+
const idsInValue = new Set<string>()
71+
if (value) {
72+
rawValue = value && value.content
73+
if ((isDestructureAssignment = !!value.ast)) {
74+
walkIdentifiers(
75+
value.ast,
76+
(id, _, __, ___, isLocal) => {
77+
if (isLocal) idsInValue.add(id.name)
78+
},
79+
true,
80+
)
81+
} else {
82+
idsInValue.add(rawValue)
83+
}
84+
}
85+
return idsInValue
86+
}
87+
88+
function genBlockFn() {
89+
const [depth, exitScope] = context.enterScope()
90+
let propsName: string
91+
const idMap: Record<string, string | null> = {}
92+
if (context.options.prefixIdentifiers) {
93+
propsName = `_ctx${depth}`
94+
Array.from(idsInValue).forEach(
95+
(id, idIndex) => (idMap[id] = `${propsName}[${idIndex}].value`),
96+
)
97+
if (rawKey) idMap[rawKey] = `${propsName}[${idsInValue.size}].value`
98+
if (rawIndex)
99+
idMap[rawIndex] = `${propsName}[${idsInValue.size + 1}].value`
100+
} else {
101+
propsName = `[${[rawValue || ((rawKey || rawIndex) && '_'), rawKey || (rawIndex && '__'), rawIndex].filter(Boolean).join(', ')}]`
102+
}
103+
104+
const blockFn = context.withId(
105+
() => genBlock(render, context, [propsName]),
106+
idMap,
107+
)
108+
exitScope()
109+
return blockFn
110+
}
111+
112+
function genSimpleIdMap() {
113+
const idMap: Record<string, null> = {}
114+
if (rawKey) idMap[rawKey] = null
115+
if (rawIndex) idMap[rawIndex] = null
116+
idsInValue.forEach(id => (idMap[id] = null))
117+
return idMap
118+
}
119+
120+
function genCallback(expr: SimpleExpressionNode | undefined) {
121+
if (!expr) return false
122+
const res = context.withId(() => genExpression(expr, context), simpleIdMap)
123+
return [
124+
...genMulti(
125+
['(', ')', ', '],
126+
rawValue ? rawValue : rawKey || rawIndex ? '_' : undefined,
127+
rawKey ? rawKey : rawIndex ? '__' : undefined,
128+
rawIndex,
129+
),
130+
' => (',
131+
...res,
132+
')',
133+
]
134+
}
122135
}

packages/compiler-vapor/src/ir/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export interface IRFor {
7777
value?: SimpleExpressionNode
7878
key?: SimpleExpressionNode
7979
index?: SimpleExpressionNode
80+
memo?: SimpleExpressionNode
8081
}
8182

8283
export interface ForIRNode extends BaseIRNode, IRFor {

packages/compiler-vapor/src/transforms/vFor.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
IRNodeTypes,
1616
type VaporDirectiveNode,
1717
} from '../ir'
18-
import { findProp, propToExpression } from '../utils'
18+
import { findDir, findProp, propToExpression } from '../utils'
1919
import { newBlock, wrapTemplate } from './utils'
2020

2121
export const transformVFor: NodeTransform = createStructuralDirectiveTransform(
@@ -45,6 +45,7 @@ export function processFor(
4545
const { source, value, key, index } = parseResult
4646

4747
const keyProp = findProp(node, 'key')
48+
const memo = findDir(node, 'memo')
4849
const keyProperty = keyProp && propToExpression(keyProp)
4950
context.node = node = wrapTemplate(node, ['for'])
5051
context.dynamic.flags |= DynamicFlag.NON_TEMPLATE | DynamicFlag.INSERT
@@ -65,6 +66,7 @@ export function processFor(
6566
keyProp: keyProperty,
6667
render,
6768
once: context.inVOnce,
69+
memo: memo && memo.exp,
6870
})
6971
}
7072
}

packages/runtime-vapor/src/apiCreateFor.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { currentInstance } from './component'
1919
import { componentKey } from './component'
2020
import type { DynamicSlot } from './componentSlots'
2121
import { renderEffect } from './renderEffect'
22+
import { withMemo } from './memo'
2223

2324
interface ForBlock extends Fragment {
2425
scope: EffectScope
@@ -264,7 +265,15 @@ export const createFor = (
264265
memo: getMemo && getMemo(item, key, index),
265266
[fragmentKey]: true,
266267
})
267-
block.nodes = scope.run(() => renderItem(state))!
268+
block.nodes = scope.run(() => {
269+
if (getMemo) {
270+
return withMemo(
271+
() => block.memo!,
272+
() => renderItem(state),
273+
)
274+
}
275+
return renderItem(state)
276+
})!
268277

269278
// TODO v-memo
270279
// if (getMemo) block.update()
@@ -306,7 +315,7 @@ export const createFor = (
306315
}
307316
}
308317

309-
if (needsUpdate) setState(block, newItem, newKey, newIndex)
318+
if (needsUpdate) updateState(block, newItem, newKey, newIndex)
310319
}
311320

312321
function updateWithoutMemo(
@@ -321,9 +330,8 @@ export const createFor = (
321330
newKey !== key.value ||
322331
newIndex !== index.value ||
323332
// shallowRef list
324-
(!isReactive(newItem) && isObject(newItem))
325-
326-
if (needsUpdate) setState(block, newItem, newKey, newIndex)
333+
(isObject(newItem) && !isReactive(newItem))
334+
if (needsUpdate) updateState(block, newItem, newKey, newIndex)
327335
}
328336

329337
function unmount({ nodes, scope }: ForBlock) {
@@ -332,7 +340,7 @@ export const createFor = (
332340
}
333341
}
334342

335-
function setState(
343+
function updateState(
336344
block: ForBlock,
337345
newItem: any,
338346
newKey: any,

packages/runtime-vapor/src/componentMetadata.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export function getMetadata(
2121
export function recordPropMetadata(el: Node, key: string, value: any): any {
2222
const metadata = getMetadata(el)[MetadataKind.prop]
2323
const prev = metadata[key]
24-
metadata[key] = value
24+
if (prev !== value) metadata[key] = value
2525
return prev
2626
}
2727

packages/runtime-vapor/src/memo.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export const memoStack: Array<() => any[]> = []
2+
3+
export function withMemo<T>(memo: () => any[], callback: () => T): T {
4+
memoStack.push(memo)
5+
const res = callback()
6+
memoStack.pop()
7+
return res
8+
}

packages/runtime-vapor/src/renderEffect.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
queuePostFlushCb,
1313
} from './scheduler'
1414
import { VaporErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
15+
import { memoStack } from './memo'
1516

1617
export function renderEffect(cb: () => void): void {
1718
const instance = getCurrentInstance()
@@ -32,6 +33,13 @@ export function renderEffect(cb: () => void): void {
3233
job.id = instance.uid
3334
}
3435

36+
let memos: (() => any[])[] | undefined
37+
let memoCaches: any[][]
38+
if (memoStack.length) {
39+
memos = Array.from(memoStack)
40+
memoCaches = memos.map(memo => memo())
41+
}
42+
3543
const effect = new ReactiveEffect(() =>
3644
callWithAsyncErrorHandling(cb, instance, VaporErrorCodes.RENDER_FUNCTION),
3745
)
@@ -52,6 +60,28 @@ export function renderEffect(cb: () => void): void {
5260
return
5361
}
5462

63+
if (memos) {
64+
let dirty: boolean | undefined
65+
for (let i = 0; i < memos.length; i++) {
66+
const memo = memos[i]
67+
const cache = memoCaches[i]
68+
const value = memo()
69+
70+
for (let j = 0; j < Math.max(value.length, cache.length); j++) {
71+
if (value[j] !== cache[j]) {
72+
dirty = true
73+
break
74+
}
75+
}
76+
77+
memoCaches[i] = value
78+
}
79+
80+
if (!dirty) {
81+
return
82+
}
83+
}
84+
5585
const reset = instance && setCurrentInstance(instance)
5686

5787
if (instance && instance.isMounted && !instance.isUpdating) {

playground/src/for-memo.vue

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<script setup lang="ts">
2+
import { reactive, ref } from 'vue'
3+
4+
const arr = reactive(['foo', 'bar', 'baz', 'qux'])
5+
const selected = ref('foo')
6+
</script>
7+
8+
<template>
9+
<div
10+
v-for="item of arr"
11+
v-memo="[selected === item]"
12+
:class="{ danger: selected === item }"
13+
@click="selected = item"
14+
>
15+
{{ item }}
16+
</div>
17+
</template>
18+
19+
<style>
20+
.danger {
21+
color: red;
22+
}
23+
</style>

0 commit comments

Comments
 (0)