feat: `v-memo` for `v-for` (#276)
This commit is contained in:
parent
cc58f651e1
commit
884c190f08
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
')',
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -77,6 +77,7 @@ export interface IRFor {
|
|||
value?: SimpleExpressionNode
|
||||
key?: SimpleExpressionNode
|
||||
index?: SimpleExpressionNode
|
||||
memo?: SimpleExpressionNode
|
||||
}
|
||||
|
||||
export interface ForIRNode extends BaseIRNode, IRFor {
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
Loading…
Reference in New Issue