feat: `v-memo` for `v-for` (#276)

This commit is contained in:
Kevin Deng 三咲智子 2024-09-19 15:40:20 +08:00 committed by GitHub
parent cc58f651e1
commit 884c190f08
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 157 additions and 71 deletions

View File

@ -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]"
>
<td>{{ row.id }}</td>
<td>

View File

@ -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<string>()
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<string, string | null> = {}
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<string, null> = {}
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<string, null> = genSimpleIdMap()
if (isDestructureAssignment) {
const idMap: Record<string, null> = {}
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<string>()
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<string, string | null> = {}
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<string, null> = {}
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,
')',
]
}
}

View File

@ -77,6 +77,7 @@ export interface IRFor {
value?: SimpleExpressionNode
key?: SimpleExpressionNode
index?: SimpleExpressionNode
memo?: SimpleExpressionNode
}
export interface ForIRNode extends BaseIRNode, IRFor {

View File

@ -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,
})
}
}

View File

@ -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,

View File

@ -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
}

View File

@ -0,0 +1,8 @@
export const memoStack: Array<() => any[]> = []
export function withMemo<T>(memo: () => any[], callback: () => T): T {
memoStack.push(memo)
const res = callback()
memoStack.pop()
return res
}

View File

@ -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) {

View File

@ -0,0 +1,23 @@
<script setup lang="ts">
import { reactive, ref } from 'vue'
const arr = reactive(['foo', 'bar', 'baz', 'qux'])
const selected = ref('foo')
</script>
<template>
<div
v-for="item of arr"
v-memo="[selected === item]"
:class="{ danger: selected === item }"
@click="selected = item"
>
{{ item }}
</div>
</template>
<style>
.danger {
color: red;
}
</style>