Skip to content

Commit

Permalink
feat: v-memo for v-for (#276)
Browse files Browse the repository at this point in the history
  • Loading branch information
sxzz authored Sep 19, 2024
1 parent cc58f65 commit 884c190
Show file tree
Hide file tree
Showing 9 changed files with 157 additions and 71 deletions.
1 change: 1 addition & 0 deletions benchmark/client/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
139 changes: 76 additions & 63 deletions packages/compiler-vapor/src/generators/for.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -16,75 +16,21 @@ 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
const rawKey = key && key.content
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[] = [
Expand All @@ -96,7 +42,7 @@ export function genFor(
rawIndex,
),
') => ',
...genMulti(DELIMITERS_ARRAY, ...idsOfValue, rawKey, rawIndex),
...genMulti(DELIMITERS_ARRAY, ...idsInValue, rawKey, rawIndex),
]

blockFn = genCall(
Expand All @@ -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,
')',
]
}
}
1 change: 1 addition & 0 deletions packages/compiler-vapor/src/ir/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export interface IRFor {
value?: SimpleExpressionNode
key?: SimpleExpressionNode
index?: SimpleExpressionNode
memo?: SimpleExpressionNode
}

export interface ForIRNode extends BaseIRNode, IRFor {
Expand Down
4 changes: 3 additions & 1 deletion packages/compiler-vapor/src/transforms/vFor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand All @@ -65,6 +66,7 @@ export function processFor(
keyProp: keyProperty,
render,
once: context.inVOnce,
memo: memo && memo.exp,
})
}
}
20 changes: 14 additions & 6 deletions packages/runtime-vapor/src/apiCreateFor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -306,7 +315,7 @@ export const createFor = (
}
}

if (needsUpdate) setState(block, newItem, newKey, newIndex)
if (needsUpdate) updateState(block, newItem, newKey, newIndex)
}

function updateWithoutMemo(
Expand All @@ -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) {
Expand All @@ -332,7 +340,7 @@ export const createFor = (
}
}

function setState(
function updateState(
block: ForBlock,
newItem: any,
newKey: any,
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime-vapor/src/componentMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
8 changes: 8 additions & 0 deletions packages/runtime-vapor/src/memo.ts
Original file line number Diff line number Diff line change
@@ -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
}
30 changes: 30 additions & 0 deletions packages/runtime-vapor/src/renderEffect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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),
)
Expand All @@ -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) {
Expand Down
23 changes: 23 additions & 0 deletions playground/src/for-memo.vue
Original file line number Diff line number Diff line change
@@ -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>

0 comments on commit 884c190

Please sign in to comment.