diff --git a/benchmark/client/App.vue b/benchmark/client/App.vue
index 0757a527c..3ca56bdfb 100644
--- a/benchmark/client/App.vue
+++ b/benchmark/client/App.vue
@@ -113,6 +113,7 @@ const isSelected = createSelector(selected)
v-for="row of rows"
:key="row.id"
:class="{ danger: isSelected(row.id) }"
+ v-memo="[row.label, row.id === selected]"
>
{{ row.id }} |
diff --git a/packages/compiler-vapor/src/generators/for.ts b/packages/compiler-vapor/src/generators/for.ts
index 6fece136e..21fb2ed02 100644
--- a/packages/compiler-vapor/src/generators/for.ts
+++ b/packages/compiler-vapor/src/generators/for.ts
@@ -1,4 +1,4 @@
-import { walkIdentifiers } from '@vue/compiler-dom'
+import { type SimpleExpressionNode, walkIdentifiers } from '@vue/compiler-dom'
import { genBlock } from './block'
import { genExpression } from './expression'
import type { CodegenContext } from '../generate'
@@ -16,7 +16,7 @@ export function genFor(
context: CodegenContext,
): CodeFragment[] {
const { vaporHelper } = context
- const { source, value, key, index, render, keyProp, once, id } = oper
+ const { source, value, key, index, render, keyProp, once, id, memo } = oper
let isDestructureAssignment = false
let rawValue: string | null = null
@@ -24,67 +24,13 @@ export function genFor(
const rawIndex = index && index.content
const sourceExpr = ['() => (', ...genExpression(source, context), ')']
-
- const idsOfValue = new Set()
- if (value) {
- rawValue = value && value.content
- if ((isDestructureAssignment = !!value.ast)) {
- walkIdentifiers(
- value.ast,
- (id, _, __, ___, isLocal) => {
- if (isLocal) idsOfValue.add(id.name)
- },
- true,
- )
- } else {
- idsOfValue.add(rawValue)
- }
- }
-
- const [depth, exitScope] = context.enterScope()
- let propsName: string
- const idMap: Record = {}
- if (context.options.prefixIdentifiers) {
- propsName = `_ctx${depth}`
- Array.from(idsOfValue).forEach(
- (id, idIndex) => (idMap[id] = `${propsName}[${idIndex}].value`),
- )
- if (rawKey) idMap[rawKey] = `${propsName}[${idsOfValue.size}].value`
- if (rawIndex) idMap[rawIndex] = `${propsName}[${idsOfValue.size + 1}].value`
- } else {
- propsName = `[${[rawValue || ((rawKey || rawIndex) && '_'), rawKey || (rawIndex && '__'), rawIndex].filter(Boolean).join(', ')}]`
- }
-
- let blockFn = context.withId(
- () => genBlock(render, context, [propsName]),
- idMap,
- )
- exitScope()
-
- let getKeyFn: CodeFragment[] | false = false
- if (keyProp) {
- const idMap: Record = {}
- if (rawKey) idMap[rawKey] = null
- if (rawIndex) idMap[rawIndex] = null
- idsOfValue.forEach(id => (idMap[id] = null))
-
- const expr = context.withId(() => genExpression(keyProp, context), idMap)
- getKeyFn = [
- ...genMulti(
- ['(', ')', ', '],
- rawValue ? rawValue : rawKey || rawIndex ? '_' : undefined,
- rawKey ? rawKey : rawIndex ? '__' : undefined,
- rawIndex,
- ),
- ' => (',
- ...expr,
- ')',
- ]
- }
+ const idsInValue = getIdsInValue()
+ let blockFn = genBlockFn()
+ const simpleIdMap: Record = genSimpleIdMap()
if (isDestructureAssignment) {
const idMap: Record = {}
- idsOfValue.forEach(id => (idMap[id] = null))
+ idsInValue.forEach(id => (idMap[id] = null))
if (rawKey) idMap[rawKey] = null
if (rawIndex) idMap[rawIndex] = null
const destructureAssignmentFn: CodeFragment[] = [
@@ -96,7 +42,7 @@ export function genFor(
rawIndex,
),
') => ',
- ...genMulti(DELIMITERS_ARRAY, ...idsOfValue, rawKey, rawIndex),
+ ...genMulti(DELIMITERS_ARRAY, ...idsInValue, rawKey, rawIndex),
]
blockFn = genCall(
@@ -113,10 +59,77 @@ export function genFor(
vaporHelper('createFor'),
sourceExpr,
blockFn,
- getKeyFn,
- false, // todo: getMemo
+ genCallback(keyProp),
+ genCallback(memo),
false, // todo: hydrationNode
once && 'true',
),
]
+
+ function getIdsInValue() {
+ const idsInValue = new Set()
+ if (value) {
+ rawValue = value && value.content
+ if ((isDestructureAssignment = !!value.ast)) {
+ walkIdentifiers(
+ value.ast,
+ (id, _, __, ___, isLocal) => {
+ if (isLocal) idsInValue.add(id.name)
+ },
+ true,
+ )
+ } else {
+ idsInValue.add(rawValue)
+ }
+ }
+ return idsInValue
+ }
+
+ function genBlockFn() {
+ const [depth, exitScope] = context.enterScope()
+ let propsName: string
+ const idMap: Record = {}
+ if (context.options.prefixIdentifiers) {
+ propsName = `_ctx${depth}`
+ Array.from(idsInValue).forEach(
+ (id, idIndex) => (idMap[id] = `${propsName}[${idIndex}].value`),
+ )
+ if (rawKey) idMap[rawKey] = `${propsName}[${idsInValue.size}].value`
+ if (rawIndex)
+ idMap[rawIndex] = `${propsName}[${idsInValue.size + 1}].value`
+ } else {
+ propsName = `[${[rawValue || ((rawKey || rawIndex) && '_'), rawKey || (rawIndex && '__'), rawIndex].filter(Boolean).join(', ')}]`
+ }
+
+ const blockFn = context.withId(
+ () => genBlock(render, context, [propsName]),
+ idMap,
+ )
+ exitScope()
+ return blockFn
+ }
+
+ function genSimpleIdMap() {
+ const idMap: Record = {}
+ if (rawKey) idMap[rawKey] = null
+ if (rawIndex) idMap[rawIndex] = null
+ idsInValue.forEach(id => (idMap[id] = null))
+ return idMap
+ }
+
+ function genCallback(expr: SimpleExpressionNode | undefined) {
+ if (!expr) return false
+ const res = context.withId(() => genExpression(expr, context), simpleIdMap)
+ return [
+ ...genMulti(
+ ['(', ')', ', '],
+ rawValue ? rawValue : rawKey || rawIndex ? '_' : undefined,
+ rawKey ? rawKey : rawIndex ? '__' : undefined,
+ rawIndex,
+ ),
+ ' => (',
+ ...res,
+ ')',
+ ]
+ }
}
diff --git a/packages/compiler-vapor/src/ir/index.ts b/packages/compiler-vapor/src/ir/index.ts
index 02318b634..f4157a516 100644
--- a/packages/compiler-vapor/src/ir/index.ts
+++ b/packages/compiler-vapor/src/ir/index.ts
@@ -77,6 +77,7 @@ export interface IRFor {
value?: SimpleExpressionNode
key?: SimpleExpressionNode
index?: SimpleExpressionNode
+ memo?: SimpleExpressionNode
}
export interface ForIRNode extends BaseIRNode, IRFor {
diff --git a/packages/compiler-vapor/src/transforms/vFor.ts b/packages/compiler-vapor/src/transforms/vFor.ts
index 4997c696b..a5ed245c2 100644
--- a/packages/compiler-vapor/src/transforms/vFor.ts
+++ b/packages/compiler-vapor/src/transforms/vFor.ts
@@ -15,7 +15,7 @@ import {
IRNodeTypes,
type VaporDirectiveNode,
} from '../ir'
-import { findProp, propToExpression } from '../utils'
+import { findDir, findProp, propToExpression } from '../utils'
import { newBlock, wrapTemplate } from './utils'
export const transformVFor: NodeTransform = createStructuralDirectiveTransform(
@@ -45,6 +45,7 @@ export function processFor(
const { source, value, key, index } = parseResult
const keyProp = findProp(node, 'key')
+ const memo = findDir(node, 'memo')
const keyProperty = keyProp && propToExpression(keyProp)
context.node = node = wrapTemplate(node, ['for'])
context.dynamic.flags |= DynamicFlag.NON_TEMPLATE | DynamicFlag.INSERT
@@ -65,6 +66,7 @@ export function processFor(
keyProp: keyProperty,
render,
once: context.inVOnce,
+ memo: memo && memo.exp,
})
}
}
diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts
index def382b24..b27c69ec5 100644
--- a/packages/runtime-vapor/src/apiCreateFor.ts
+++ b/packages/runtime-vapor/src/apiCreateFor.ts
@@ -19,6 +19,7 @@ import { currentInstance } from './component'
import { componentKey } from './component'
import type { DynamicSlot } from './componentSlots'
import { renderEffect } from './renderEffect'
+import { withMemo } from './memo'
interface ForBlock extends Fragment {
scope: EffectScope
@@ -264,7 +265,15 @@ export const createFor = (
memo: getMemo && getMemo(item, key, index),
[fragmentKey]: true,
})
- block.nodes = scope.run(() => renderItem(state))!
+ block.nodes = scope.run(() => {
+ if (getMemo) {
+ return withMemo(
+ () => block.memo!,
+ () => renderItem(state),
+ )
+ }
+ return renderItem(state)
+ })!
// TODO v-memo
// if (getMemo) block.update()
@@ -306,7 +315,7 @@ export const createFor = (
}
}
- if (needsUpdate) setState(block, newItem, newKey, newIndex)
+ if (needsUpdate) updateState(block, newItem, newKey, newIndex)
}
function updateWithoutMemo(
@@ -321,9 +330,8 @@ export const createFor = (
newKey !== key.value ||
newIndex !== index.value ||
// shallowRef list
- (!isReactive(newItem) && isObject(newItem))
-
- if (needsUpdate) setState(block, newItem, newKey, newIndex)
+ (isObject(newItem) && !isReactive(newItem))
+ if (needsUpdate) updateState(block, newItem, newKey, newIndex)
}
function unmount({ nodes, scope }: ForBlock) {
@@ -332,7 +340,7 @@ export const createFor = (
}
}
-function setState(
+function updateState(
block: ForBlock,
newItem: any,
newKey: any,
diff --git a/packages/runtime-vapor/src/componentMetadata.ts b/packages/runtime-vapor/src/componentMetadata.ts
index 8bfe2237f..ab2ad0bc6 100644
--- a/packages/runtime-vapor/src/componentMetadata.ts
+++ b/packages/runtime-vapor/src/componentMetadata.ts
@@ -21,7 +21,7 @@ export function getMetadata(
export function recordPropMetadata(el: Node, key: string, value: any): any {
const metadata = getMetadata(el)[MetadataKind.prop]
const prev = metadata[key]
- metadata[key] = value
+ if (prev !== value) metadata[key] = value
return prev
}
diff --git a/packages/runtime-vapor/src/memo.ts b/packages/runtime-vapor/src/memo.ts
new file mode 100644
index 000000000..28892b36a
--- /dev/null
+++ b/packages/runtime-vapor/src/memo.ts
@@ -0,0 +1,8 @@
+export const memoStack: Array<() => any[]> = []
+
+export function withMemo(memo: () => any[], callback: () => T): T {
+ memoStack.push(memo)
+ const res = callback()
+ memoStack.pop()
+ return res
+}
diff --git a/packages/runtime-vapor/src/renderEffect.ts b/packages/runtime-vapor/src/renderEffect.ts
index 807145681..9e4c38723 100644
--- a/packages/runtime-vapor/src/renderEffect.ts
+++ b/packages/runtime-vapor/src/renderEffect.ts
@@ -12,6 +12,7 @@ import {
queuePostFlushCb,
} from './scheduler'
import { VaporErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
+import { memoStack } from './memo'
export function renderEffect(cb: () => void): void {
const instance = getCurrentInstance()
@@ -32,6 +33,13 @@ export function renderEffect(cb: () => void): void {
job.id = instance.uid
}
+ let memos: (() => any[])[] | undefined
+ let memoCaches: any[][]
+ if (memoStack.length) {
+ memos = Array.from(memoStack)
+ memoCaches = memos.map(memo => memo())
+ }
+
const effect = new ReactiveEffect(() =>
callWithAsyncErrorHandling(cb, instance, VaporErrorCodes.RENDER_FUNCTION),
)
@@ -52,6 +60,28 @@ export function renderEffect(cb: () => void): void {
return
}
+ if (memos) {
+ let dirty: boolean | undefined
+ for (let i = 0; i < memos.length; i++) {
+ const memo = memos[i]
+ const cache = memoCaches[i]
+ const value = memo()
+
+ for (let j = 0; j < Math.max(value.length, cache.length); j++) {
+ if (value[j] !== cache[j]) {
+ dirty = true
+ break
+ }
+ }
+
+ memoCaches[i] = value
+ }
+
+ if (!dirty) {
+ return
+ }
+ }
+
const reset = instance && setCurrentInstance(instance)
if (instance && instance.isMounted && !instance.isUpdating) {
diff --git a/playground/src/for-memo.vue b/playground/src/for-memo.vue
new file mode 100644
index 000000000..3c3ba90f5
--- /dev/null
+++ b/playground/src/for-memo.vue
@@ -0,0 +1,23 @@
+
+
+
+
+ {{ item }}
+
+
+
+
|