From 884c190f08b8dfc4cd2258173d16d3b4e324a72a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Deng=20=E4=B8=89=E5=92=B2=E6=99=BA=E5=AD=90?= Date: Thu, 19 Sep 2024 15:40:20 +0800 Subject: [PATCH] feat: `v-memo` for `v-for` (#276) --- benchmark/client/App.vue | 1 + packages/compiler-vapor/src/generators/for.ts | 139 ++++++++++-------- packages/compiler-vapor/src/ir/index.ts | 1 + .../compiler-vapor/src/transforms/vFor.ts | 4 +- packages/runtime-vapor/src/apiCreateFor.ts | 20 ++- .../runtime-vapor/src/componentMetadata.ts | 2 +- packages/runtime-vapor/src/memo.ts | 8 + packages/runtime-vapor/src/renderEffect.ts | 30 ++++ playground/src/for-memo.vue | 23 +++ 9 files changed, 157 insertions(+), 71 deletions(-) create mode 100644 packages/runtime-vapor/src/memo.ts create mode 100644 playground/src/for-memo.vue 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 @@ + + + + +