Browse Source

refactor(compiler-vapor): avoid mutating v-for render effects in codegen

Keep v-for selector pattern matching as a codegen optimization, but stop
removing matched effects from the render block IR during generation.

Instead, pass the matched effect indexes to block generation so they are skipped
only in emitted code. This preserves the original render IR and avoids the
extra effect boundary rewrites previously needed after mutating render.effect.
daiwei 3 days ago
parent
commit
f8719bb3cb

+ 25 - 2
packages/compiler-vapor/src/generators/block.ts

@@ -56,6 +56,7 @@ export function genBlockContent(
   context: CodegenContext,
   root?: boolean,
   genEffectsExtraFrag?: () => CodeFragment[],
+  skippedEffectIndexes?: Set<number>,
 ): CodeFragment[] {
   const [frag, push] = buildCodeFragment()
   const { dynamic, effect, operation, returns } = block
@@ -109,7 +110,7 @@ export function genBlockContent(
     }
 
     if (effectIndex < effectEnd) {
-      push(...genEffects(effect.slice(effectIndex, effectEnd), context))
+      push(...genEffectRange(effectIndex, effectEnd))
       effectIndex = effectEnd
     }
   }
@@ -154,7 +155,7 @@ export function genBlockContent(
     push(...genOperations(operation.slice(operationIndex), context))
   }
   if (effectIndex < effect.length) {
-    push(...genEffects(effect.slice(effectIndex), context, genEffectsExtraFrag))
+    push(...genEffectRange(effectIndex, effect.length, genEffectsExtraFrag))
   } else if (genEffectsExtraFrag) {
     push(...genEffects([], context, genEffectsExtraFrag))
   }
@@ -172,6 +173,28 @@ export function genBlockContent(
   context.singleUseAssetComponentNames = prevSingleUseAssetComponentNames
   return frag
 
+  function genEffectRange(
+    start: number,
+    end: number,
+    genExtraFrag?: () => CodeFragment[],
+  ): CodeFragment[] {
+    if (!skippedEffectIndexes) {
+      return genEffects(effect.slice(start, end), context, genExtraFrag)
+    }
+
+    const effects: typeof effect = []
+    for (let i = start; i < end; i++) {
+      if (!skippedEffectIndexes.has(i)) {
+        effects.push(effect[i])
+      }
+    }
+
+    if (effects.length || genExtraFrag) {
+      return genEffects(effects, context, genExtraFrag)
+    }
+    return []
+  }
+
   function genResolveAssets(
     kind: 'component' | 'directive',
     helper: CoreHelper,

+ 61 - 78
packages/compiler-vapor/src/generators/for.ts

@@ -12,7 +12,6 @@ import {
   type IRDynamicInfo,
   type IREffect,
   IRNodeTypes,
-  isBlockOperation,
 } from '../ir'
 import {
   type CodeFragment,
@@ -78,12 +77,8 @@ export function genFor(
     idMap[indexVar] = null
   }
 
-  const { selectorPatterns, keyOnlyBindingPatterns } = matchPatterns(
-    render,
-    keyProp,
-    idMap,
-    context,
-  )
+  const { selectorPatterns, keyOnlyBindingPatterns, skippedEffectIndexes } =
+    matchPatterns(render, keyProp, idMap, context)
   const selectorDeclarations: CodeFragment[] = []
   const selectorName = (i: number) =>
     selectorPatterns.length > 1 ? `_selector${id}_${i}` : `_selector${id}`
@@ -105,32 +100,38 @@ export function genFor(
     frag.push('(', ...args, ') => {', INDENT_START)
     if (selectorPatterns.length || keyOnlyBindingPatterns.length) {
       frag.push(
-        ...genBlockContent(render, context, false, () => {
-          const patternFrag: CodeFragment[] = []
-
-          for (let i = 0; i < selectorPatterns.length; i++) {
-            const { effect } = selectorPatterns[i]
-            patternFrag.push(
-              NEWLINE,
-              `${selectorName(i)}(`,
-              ...genExpression(keyProp!, context),
-              `, () => {`,
-              INDENT_START,
-            )
-            for (const oper of effect.operations) {
-              patternFrag.push(...genOperation(oper, context))
+        ...genBlockContent(
+          render,
+          context,
+          false,
+          () => {
+            const patternFrag: CodeFragment[] = []
+
+            for (let i = 0; i < selectorPatterns.length; i++) {
+              const { effect } = selectorPatterns[i]
+              patternFrag.push(
+                NEWLINE,
+                `${selectorName(i)}(`,
+                ...genExpression(keyProp!, context),
+                `, () => {`,
+                INDENT_START,
+              )
+              for (const oper of effect.operations) {
+                patternFrag.push(...genOperation(oper, context))
+              }
+              patternFrag.push(INDENT_END, NEWLINE, `})`)
             }
-            patternFrag.push(INDENT_END, NEWLINE, `})`)
-          }
 
-          for (const { effect } of keyOnlyBindingPatterns) {
-            for (const oper of effect.operations) {
-              patternFrag.push(...genOperation(oper, context))
+            for (const { effect } of keyOnlyBindingPatterns) {
+              for (const oper of effect.operations) {
+                patternFrag.push(...genOperation(oper, context))
+              }
             }
-          }
 
-          return patternFrag
-        }),
+            return patternFrag
+          },
+          skippedEffectIndexes,
+        ),
       )
     } else {
       frag.push(...genBlockContent(render, context))
@@ -383,65 +384,47 @@ function matchPatterns(
   const keyOnlyBindingPatterns: NonNullable<
     ReturnType<typeof matchKeyOnlyBindingPattern>
   >[] = []
-  const removedEffectIndexes: number[] = []
-
-  render.effect = render.effect.filter((effect, index) => {
-    if (keyProp !== undefined) {
-      const selector = matchSelectorPattern(
-        effect,
-        keyProp.content,
-        idMap,
-        context,
-      )
-      if (selector) {
-        selectorPatterns.push(selector)
-        removedEffectIndexes.push(index)
-        return false
-      }
-      const keyOnly = matchKeyOnlyBindingPattern(effect, keyProp.content)
-      if (keyOnly) {
-        keyOnlyBindingPatterns.push(keyOnly)
-        removedEffectIndexes.push(index)
-        return false
-      }
-    }
+  let skippedEffectIndexes: Set<number> | undefined
 
-    return true
-  })
+  if (keyProp === undefined) {
+    return {
+      keyOnlyBindingPatterns,
+      selectorPatterns,
+      skippedEffectIndexes,
+    }
+  }
 
-  if (removedEffectIndexes.length) {
-    shiftEffectBoundaries(render.dynamic, removedEffectIndexes)
+  for (let index = 0; index < render.effect.length; index++) {
+    const effect = render.effect[index]
+    const selector = matchSelectorPattern(
+      effect,
+      keyProp.content,
+      idMap,
+      context,
+    )
+    if (selector) {
+      selectorPatterns.push(selector)
+      skipEffect(index)
+      continue
+    }
+    const keyOnly = matchKeyOnlyBindingPattern(effect, keyProp.content)
+    if (keyOnly) {
+      keyOnlyBindingPatterns.push(keyOnly)
+      skipEffect(index)
+    }
   }
 
   return {
     keyOnlyBindingPatterns,
     selectorPatterns,
+    skippedEffectIndexes,
   }
-}
 
-function shiftEffectBoundaries(
-  dynamic: IRDynamicInfo,
-  removedEffectIndexes: number[],
-): void {
-  const operation = dynamic.operation
-  if (
-    operation &&
-    isBlockOperation(operation) &&
-    operation.effectIndex !== undefined
-  ) {
-    let offset = 0
-    for (const removedIndex of removedEffectIndexes) {
-      if (removedIndex < operation.effectIndex) {
-        offset++
-      } else {
-        break
-      }
+  function skipEffect(index: number): void {
+    if (!skippedEffectIndexes) {
+      skippedEffectIndexes = new Set()
     }
-    operation.effectIndex -= offset
-  }
-
-  for (const child of dynamic.children) {
-    shiftEffectBoundaries(child, removedEffectIndexes)
+    skippedEffectIndexes.add(index)
   }
 }