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"
|
v-for="row of rows"
|
||||||
:key="row.id"
|
:key="row.id"
|
||||||
:class="{ danger: isSelected(row.id) }"
|
:class="{ danger: isSelected(row.id) }"
|
||||||
|
v-memo="[row.label, row.id === selected]"
|
||||||
>
|
>
|
||||||
<td>{{ row.id }}</td>
|
<td>{{ row.id }}</td>
|
||||||
<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 { genBlock } from './block'
|
||||||
import { genExpression } from './expression'
|
import { genExpression } from './expression'
|
||||||
import type { CodegenContext } from '../generate'
|
import type { CodegenContext } from '../generate'
|
||||||
|
@ -16,7 +16,7 @@ export function genFor(
|
||||||
context: CodegenContext,
|
context: CodegenContext,
|
||||||
): CodeFragment[] {
|
): CodeFragment[] {
|
||||||
const { vaporHelper } = context
|
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 isDestructureAssignment = false
|
||||||
let rawValue: string | null = null
|
let rawValue: string | null = null
|
||||||
|
@ -24,67 +24,13 @@ export function genFor(
|
||||||
const rawIndex = index && index.content
|
const rawIndex = index && index.content
|
||||||
|
|
||||||
const sourceExpr = ['() => (', ...genExpression(source, context), ')']
|
const sourceExpr = ['() => (', ...genExpression(source, context), ')']
|
||||||
|
const idsInValue = getIdsInValue()
|
||||||
const idsOfValue = new Set<string>()
|
let blockFn = genBlockFn()
|
||||||
if (value) {
|
const simpleIdMap: Record<string, null> = genSimpleIdMap()
|
||||||
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,
|
|
||||||
')',
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDestructureAssignment) {
|
if (isDestructureAssignment) {
|
||||||
const idMap: Record<string, null> = {}
|
const idMap: Record<string, null> = {}
|
||||||
idsOfValue.forEach(id => (idMap[id] = null))
|
idsInValue.forEach(id => (idMap[id] = null))
|
||||||
if (rawKey) idMap[rawKey] = null
|
if (rawKey) idMap[rawKey] = null
|
||||||
if (rawIndex) idMap[rawIndex] = null
|
if (rawIndex) idMap[rawIndex] = null
|
||||||
const destructureAssignmentFn: CodeFragment[] = [
|
const destructureAssignmentFn: CodeFragment[] = [
|
||||||
|
@ -96,7 +42,7 @@ export function genFor(
|
||||||
rawIndex,
|
rawIndex,
|
||||||
),
|
),
|
||||||
') => ',
|
') => ',
|
||||||
...genMulti(DELIMITERS_ARRAY, ...idsOfValue, rawKey, rawIndex),
|
...genMulti(DELIMITERS_ARRAY, ...idsInValue, rawKey, rawIndex),
|
||||||
]
|
]
|
||||||
|
|
||||||
blockFn = genCall(
|
blockFn = genCall(
|
||||||
|
@ -113,10 +59,77 @@ export function genFor(
|
||||||
vaporHelper('createFor'),
|
vaporHelper('createFor'),
|
||||||
sourceExpr,
|
sourceExpr,
|
||||||
blockFn,
|
blockFn,
|
||||||
getKeyFn,
|
genCallback(keyProp),
|
||||||
false, // todo: getMemo
|
genCallback(memo),
|
||||||
false, // todo: hydrationNode
|
false, // todo: hydrationNode
|
||||||
once && 'true',
|
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
|
value?: SimpleExpressionNode
|
||||||
key?: SimpleExpressionNode
|
key?: SimpleExpressionNode
|
||||||
index?: SimpleExpressionNode
|
index?: SimpleExpressionNode
|
||||||
|
memo?: SimpleExpressionNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ForIRNode extends BaseIRNode, IRFor {
|
export interface ForIRNode extends BaseIRNode, IRFor {
|
||||||
|
|
|
@ -15,7 +15,7 @@ import {
|
||||||
IRNodeTypes,
|
IRNodeTypes,
|
||||||
type VaporDirectiveNode,
|
type VaporDirectiveNode,
|
||||||
} from '../ir'
|
} from '../ir'
|
||||||
import { findProp, propToExpression } from '../utils'
|
import { findDir, findProp, propToExpression } from '../utils'
|
||||||
import { newBlock, wrapTemplate } from './utils'
|
import { newBlock, wrapTemplate } from './utils'
|
||||||
|
|
||||||
export const transformVFor: NodeTransform = createStructuralDirectiveTransform(
|
export const transformVFor: NodeTransform = createStructuralDirectiveTransform(
|
||||||
|
@ -45,6 +45,7 @@ export function processFor(
|
||||||
const { source, value, key, index } = parseResult
|
const { source, value, key, index } = parseResult
|
||||||
|
|
||||||
const keyProp = findProp(node, 'key')
|
const keyProp = findProp(node, 'key')
|
||||||
|
const memo = findDir(node, 'memo')
|
||||||
const keyProperty = keyProp && propToExpression(keyProp)
|
const keyProperty = keyProp && propToExpression(keyProp)
|
||||||
context.node = node = wrapTemplate(node, ['for'])
|
context.node = node = wrapTemplate(node, ['for'])
|
||||||
context.dynamic.flags |= DynamicFlag.NON_TEMPLATE | DynamicFlag.INSERT
|
context.dynamic.flags |= DynamicFlag.NON_TEMPLATE | DynamicFlag.INSERT
|
||||||
|
@ -65,6 +66,7 @@ export function processFor(
|
||||||
keyProp: keyProperty,
|
keyProp: keyProperty,
|
||||||
render,
|
render,
|
||||||
once: context.inVOnce,
|
once: context.inVOnce,
|
||||||
|
memo: memo && memo.exp,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { currentInstance } from './component'
|
||||||
import { componentKey } from './component'
|
import { componentKey } from './component'
|
||||||
import type { DynamicSlot } from './componentSlots'
|
import type { DynamicSlot } from './componentSlots'
|
||||||
import { renderEffect } from './renderEffect'
|
import { renderEffect } from './renderEffect'
|
||||||
|
import { withMemo } from './memo'
|
||||||
|
|
||||||
interface ForBlock extends Fragment {
|
interface ForBlock extends Fragment {
|
||||||
scope: EffectScope
|
scope: EffectScope
|
||||||
|
@ -264,7 +265,15 @@ export const createFor = (
|
||||||
memo: getMemo && getMemo(item, key, index),
|
memo: getMemo && getMemo(item, key, index),
|
||||||
[fragmentKey]: true,
|
[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
|
// TODO v-memo
|
||||||
// if (getMemo) block.update()
|
// 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(
|
function updateWithoutMemo(
|
||||||
|
@ -321,9 +330,8 @@ export const createFor = (
|
||||||
newKey !== key.value ||
|
newKey !== key.value ||
|
||||||
newIndex !== index.value ||
|
newIndex !== index.value ||
|
||||||
// shallowRef list
|
// shallowRef list
|
||||||
(!isReactive(newItem) && isObject(newItem))
|
(isObject(newItem) && !isReactive(newItem))
|
||||||
|
if (needsUpdate) updateState(block, newItem, newKey, newIndex)
|
||||||
if (needsUpdate) setState(block, newItem, newKey, newIndex)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function unmount({ nodes, scope }: ForBlock) {
|
function unmount({ nodes, scope }: ForBlock) {
|
||||||
|
@ -332,7 +340,7 @@ export const createFor = (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setState(
|
function updateState(
|
||||||
block: ForBlock,
|
block: ForBlock,
|
||||||
newItem: any,
|
newItem: any,
|
||||||
newKey: any,
|
newKey: any,
|
||||||
|
|
|
@ -21,7 +21,7 @@ export function getMetadata(
|
||||||
export function recordPropMetadata(el: Node, key: string, value: any): any {
|
export function recordPropMetadata(el: Node, key: string, value: any): any {
|
||||||
const metadata = getMetadata(el)[MetadataKind.prop]
|
const metadata = getMetadata(el)[MetadataKind.prop]
|
||||||
const prev = metadata[key]
|
const prev = metadata[key]
|
||||||
metadata[key] = value
|
if (prev !== value) metadata[key] = value
|
||||||
return prev
|
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,
|
queuePostFlushCb,
|
||||||
} from './scheduler'
|
} from './scheduler'
|
||||||
import { VaporErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
|
import { VaporErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
|
||||||
|
import { memoStack } from './memo'
|
||||||
|
|
||||||
export function renderEffect(cb: () => void): void {
|
export function renderEffect(cb: () => void): void {
|
||||||
const instance = getCurrentInstance()
|
const instance = getCurrentInstance()
|
||||||
|
@ -32,6 +33,13 @@ export function renderEffect(cb: () => void): void {
|
||||||
job.id = instance.uid
|
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(() =>
|
const effect = new ReactiveEffect(() =>
|
||||||
callWithAsyncErrorHandling(cb, instance, VaporErrorCodes.RENDER_FUNCTION),
|
callWithAsyncErrorHandling(cb, instance, VaporErrorCodes.RENDER_FUNCTION),
|
||||||
)
|
)
|
||||||
|
@ -52,6 +60,28 @@ export function renderEffect(cb: () => void): void {
|
||||||
return
|
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)
|
const reset = instance && setCurrentInstance(instance)
|
||||||
|
|
||||||
if (instance && instance.isMounted && !instance.isUpdating) {
|
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