]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat: `v-memo` for `v-for` (#276)
authorKevin Deng 三咲智子 <sxzz@sxzz.moe>
Thu, 19 Sep 2024 07:40:20 +0000 (15:40 +0800)
committerGitHub <noreply@github.com>
Thu, 19 Sep 2024 07:40:20 +0000 (15:40 +0800)
benchmark/client/App.vue
packages/compiler-vapor/src/generators/for.ts
packages/compiler-vapor/src/ir/index.ts
packages/compiler-vapor/src/transforms/vFor.ts
packages/runtime-vapor/src/apiCreateFor.ts
packages/runtime-vapor/src/componentMetadata.ts
packages/runtime-vapor/src/memo.ts [new file with mode: 0644]
packages/runtime-vapor/src/renderEffect.ts
playground/src/for-memo.vue [new file with mode: 0644]

index 0757a527ce3f0cb68085ea8cd626c17b4dc6704f..3ca56bdfb1ae183fa621b5dc5e8a3a9f97d0d7ba 100644 (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>
index 6fece136ec4f64c1174d2eac73c26764dc9b08d3..21fb2ed02ab8d6593f63c823fe726cb285ff6435 100644 (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,
+      ')',
+    ]
+  }
 }
index 02318b634bf0993dd70ceacf0ace411454fa537e..f4157a516ee19088d81613a0352edc83e5ee279f 100644 (file)
@@ -77,6 +77,7 @@ export interface IRFor {
   value?: SimpleExpressionNode
   key?: SimpleExpressionNode
   index?: SimpleExpressionNode
+  memo?: SimpleExpressionNode
 }
 
 export interface ForIRNode extends BaseIRNode, IRFor {
index 4997c696b7e1bbb9c37d893ee87ef51a4c3a2f08..a5ed245c201d29808bf8122505a42144e1d8b501 100644 (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,
     })
   }
 }
index def382b24c8f4ca827b726ea79c0c93b52a48f2f..b27c69ec5ce085692319afc1f7e37b97a84068b6 100644 (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,
index 8bfe2237fb1c2831b7b4799dc20fe1dca8e0b9e8..ab2ad0bc65031c2c9c2d8df82e4cd22a7771ca1d 100644 (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
 }
 
diff --git a/packages/runtime-vapor/src/memo.ts b/packages/runtime-vapor/src/memo.ts
new file mode 100644 (file)
index 0000000..28892b3
--- /dev/null
@@ -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
+}
index 807145681455c3f9e51ea23fc21540f59317a6f3..9e4c3872381a371fdead1980e0f5e7a8fe1e1b6a 100644 (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) {
diff --git a/playground/src/for-memo.vue b/playground/src/for-memo.vue
new file mode 100644 (file)
index 0000000..3c3ba90
--- /dev/null
@@ -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>