Sfoglia il codice sorgente

feat(compiler-vapor): add keyed block handling for dynamic keys

daiwei 2 mesi fa
parent
commit
862ab176e3

+ 90 - 0
packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap

@@ -738,3 +738,93 @@ export function render(_ctx) {
   return n0
 }"
 `;
+
+exports[`compiler: element transform > with dynamic key > <component is/> + key 1`] = `
+"import { createDynamicComponent as _createDynamicComponent, createKeyedFragment as _createKeyedFragment } from 'vue';
+
+export function render(_ctx) {
+  return _createKeyedFragment(() => _ctx.id, () => {
+    const n0 = _createDynamicComponent(() => (_ctx.view), null, null, true)
+    return n0
+  })
+}"
+`;
+
+exports[`compiler: element transform > with dynamic key > component + key 1`] = `
+"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, createKeyedFragment as _createKeyedFragment } from 'vue';
+
+export function render(_ctx) {
+  return _createKeyedFragment(() => _ctx.id, () => {
+    const _component_Foo = _resolveComponent("Foo")
+    const n0 = _createComponentWithFallback(_component_Foo, null, null, true)
+    return n0
+  })
+}"
+`;
+
+exports[`compiler: element transform > with dynamic key > element + key 1`] = `
+"import { createKeyedFragment as _createKeyedFragment, template as _template } from 'vue';
+const t0 = _template("<div>", true)
+
+export function render(_ctx) {
+  return _createKeyedFragment(() => _ctx.id, () => {
+    const n0 = t0()
+    return n0
+  })
+}"
+`;
+
+exports[`compiler: element transform > with dynamic key > v-for + key 1`] = `
+"import { createFor as _createFor, template as _template } from 'vue';
+const t0 = _template("<div>")
+
+export function render(_ctx) {
+  const n0 = _createFor(() => (_ctx.list), (_for_item0) => {
+    const n2 = t0()
+    return n2
+  }, (i) => (i))
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > with dynamic key > v-if + key 1`] = `
+"import { createIf as _createIf, template as _template } from 'vue';
+const t0 = _template("<div>", true)
+
+export function render(_ctx) {
+  const n0 = _createIf(() => (_ctx.ok), () => {
+    const n2 = t0()
+    return n2
+  })
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > with static key > <component is/> + key 1`] = `
+"import { createDynamicComponent as _createDynamicComponent } from 'vue';
+
+export function render(_ctx) {
+  const n0 = _createDynamicComponent(() => (_ctx.view), null, null, true)
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > with static key > component + key 1`] = `
+"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
+
+export function render(_ctx) {
+  const _component_Foo = _resolveComponent("Foo")
+  const n0 = _createComponentWithFallback(_component_Foo, null, null, true)
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > with static key > element + key 1`] = `
+"import { template as _template } from 'vue';
+const t0 = _template("<div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  return n0
+}"
+`;

+ 63 - 0
packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts

@@ -7,6 +7,7 @@ import {
   transformText,
   transformVBind,
   transformVFor,
+  transformVIf,
   transformVOn,
 } from '../../src'
 import {
@@ -17,6 +18,7 @@ import {
 
 const compileWithElementTransform = makeCompile({
   nodeTransforms: [
+    transformVIf,
     transformVFor,
     transformElement,
     transformText,
@@ -1240,4 +1242,65 @@ describe('compiler: element transform', () => {
       expect([...ir.template.keys()]).toMatchObject([template])
     })
   })
+
+  describe('with dynamic key', () => {
+    test('component + key', () => {
+      const { code } = compileWithElementTransform(`<Foo :key="id" />`)
+      expect(code).toMatchSnapshot()
+      expect(code).contains('_createKeyedFragment(() => _ctx.id')
+    })
+
+    test('element + key', () => {
+      const { code } = compileWithElementTransform(`<div :key="id"></div>`)
+      expect(code).toMatchSnapshot()
+      expect(code).contains('_createKeyedFragment(() => _ctx.id')
+    })
+
+    test('<component is/> + key', () => {
+      const { code } = compileWithElementTransform(
+        `<component :is="view" :key="id" />`,
+      )
+      expect(code).toMatchSnapshot()
+      expect(code).contains('_createKeyedFragment(() => _ctx.id')
+    })
+
+    test('v-if + key', () => {
+      const { code } = compileWithElementTransform(
+        `<div v-if="ok" :key="id"></div>`,
+      )
+      expect(code).toMatchSnapshot()
+      expect(code).not.contains('_createKeyedFragment(')
+    })
+
+    test('v-for + key', () => {
+      const { code } = compileWithElementTransform(
+        `<div v-for="i in list" :key="i"></div>`,
+      )
+      expect(code).toMatchSnapshot()
+      expect(code).not.contains('_createKeyedFragment(')
+    })
+  })
+
+  // static keys will be ignored
+  describe('with static key', () => {
+    test('component + key', () => {
+      const { code } = compileWithElementTransform(`<Foo key="1" />`)
+      expect(code).toMatchSnapshot()
+      expect(code).not.contains('_createKeyedFragment(')
+    })
+
+    test('element + key', () => {
+      const { code } = compileWithElementTransform(`<div key="1"></div>`)
+      expect(code).toMatchSnapshot()
+      expect(code).not.contains('_createKeyedFragment(')
+    })
+
+    test('<component is/> + key', () => {
+      const { code } = compileWithElementTransform(
+        `<component :is="view" key="1" />`,
+      )
+      expect(code).toMatchSnapshot()
+      expect(code).not.contains('_createKeyedFragment(')
+    })
+  })
 })

+ 14 - 2
packages/compiler-vapor/src/generate.ts

@@ -5,7 +5,7 @@ import type {
 } from '@vue/compiler-dom'
 import type { BlockIRNode, CoreHelper, RootIRNode, VaporHelper } from './ir'
 import { extend, remove } from '@vue/shared'
-import { genBlockContent } from './generators/block'
+import { genBlock, genBlockContent, genKeyedFragment } from './generators/block'
 import { genTemplates } from './generators/template'
 import {
   type CodeFragment,
@@ -204,7 +204,19 @@ export function generate(
   if (ir.hasDeferredVShow) {
     push(NEWLINE, `const deferredApplyVShows = []`)
   }
-  push(...genBlockContent(ir.block, context, true))
+  if (ir.block.keyed && ir.block.keyExpr) {
+    push(
+      NEWLINE,
+      `return `,
+      ...genKeyedFragment(
+        genBlock(ir.block, context, [], true, true),
+        ir.block.keyExpr,
+        context,
+      ),
+    )
+  } else {
+    push(...genBlockContent(ir.block, context, true))
+  }
   push(INDENT_END, NEWLINE)
 
   if (!inline) {

+ 36 - 1
packages/compiler-vapor/src/generators/block.ts

@@ -13,14 +13,16 @@ import type { CodegenContext } from '../generate'
 import { genEffects, genOperations } from './operation'
 import { genChildren, genSelf } from './template'
 import { toValidAssetId } from '@vue/compiler-dom'
+import { genExpression } from './expression'
 
 export function genBlock(
   oper: BlockIRNode,
   context: CodegenContext,
   args: CodeFragment[] = [],
   root?: boolean,
+  ignoreKeyed: boolean = false,
 ): CodeFragment[] {
-  return [
+  const blockFn: CodeFragment[] = [
     '(',
     ...args,
     ') => {',
@@ -30,6 +32,39 @@ export function genBlock(
     NEWLINE,
     '}',
   ]
+  if (!ignoreKeyed && oper.keyed && oper.keyExpr) {
+    return wrapWithKeyedFragment(blockFn, oper.keyExpr, context)
+  }
+  return blockFn
+}
+
+export function genKeyedFragment(
+  blockFn: CodeFragment[],
+  keyExpr: BlockIRNode['keyExpr'],
+  context: CodegenContext,
+): CodeFragment[] {
+  return genCall(
+    context.helper('createKeyedFragment'),
+    [`() => `, ...genExpression(keyExpr!, context)],
+    blockFn,
+  )
+}
+
+export function wrapWithKeyedFragment(
+  blockFn: CodeFragment[],
+  keyExpr: BlockIRNode['keyExpr'],
+  context: CodegenContext,
+): CodeFragment[] {
+  return [
+    `() => {`,
+    INDENT_START,
+    NEWLINE,
+    `return `,
+    ...genKeyedFragment(blockFn, keyExpr, context),
+    INDENT_END,
+    NEWLINE,
+    `}`,
+  ]
 }
 
 export function genBlockContent(

+ 1 - 18
packages/compiler-vapor/src/generators/component.ts

@@ -566,7 +566,7 @@ function genSlotBlockWithProps(oper: SlotBlockIRNode, context: CodegenContext) {
   let propsName: string | undefined
   let exitScope: (() => void) | undefined
   let depth: number | undefined
-  const { props, key, node } = oper
+  const { props, node } = oper
   const idToPathMap: DestructureMap = props
     ? parseValueDestructure(props, context)
     : new Map<string, DestructureMapValue | null>()
@@ -598,23 +598,6 @@ function genSlotBlockWithProps(oper: SlotBlockIRNode, context: CodegenContext) {
   )
   exitScope && exitScope()
 
-  if (key) {
-    blockFn = [
-      `() => {`,
-      INDENT_START,
-      NEWLINE,
-      `return `,
-      ...genCall(
-        context.helper('createKeyedFragment'),
-        [`() => `, ...genExpression(key, context)],
-        blockFn,
-      ),
-      INDENT_END,
-      NEWLINE,
-      `}`,
-    ]
-  }
-
   if (node.type === NodeTypes.ELEMENT) {
     // wrap with withVaporCtx to track slot owner for:
     // 1. createSlot to get correct rawSlots in forwarded slots

+ 2 - 0
packages/compiler-vapor/src/ir/index.ts

@@ -53,6 +53,8 @@ export interface BlockIRNode extends BaseIRNode {
   effect: IREffect[]
   operation: OperationNode[]
   returns: number[]
+  keyed?: boolean
+  keyExpr?: SimpleExpressionNode
 }
 
 export interface RootIRNode {

+ 48 - 1
packages/compiler-vapor/src/transforms/transformElement.ts

@@ -44,7 +44,13 @@ import {
   type VaporDirectiveNode,
 } from '../ir'
 import { EMPTY_EXPRESSION } from './utils'
-import { findProp, isBuiltInComponent } from '../utils'
+import {
+  findDir,
+  findProp,
+  isBuiltInComponent,
+  isStaticExpression,
+  propToExpression,
+} from '../utils'
 import { IMPORT_EXP_END, IMPORT_EXP_START } from '../generators/utils'
 
 export const isReservedProp: (key: string) => boolean = /*#__PURE__*/ makeMap(
@@ -87,6 +93,8 @@ export const transformElement: NodeTransform = (node, context) => {
       node.tagType === ElementTypes.COMPONENT || isCustomElement
 
     const isDynamicComponent = isComponentTag(node.tag)
+    maybeMarkKeyedBlock(node, context)
+
     const propsResult = buildProps(
       node,
       context as TransformContext<ElementNode>,
@@ -195,6 +203,45 @@ function isSingleRoot(
   return context.root === parent
 }
 
+// Dynamic key should become a keyed block (handled in genBlock).
+// Only apply to plain elements/components; skip v-for and v-if branches
+function maybeMarkKeyedBlock(
+  node: ElementNode,
+  context: TransformContext<RootNode | TemplateChildNode>,
+): void {
+  const keyProp = findProp(node, 'key')
+  const hasIf =
+    !!findDir(node, 'if') ||
+    !!findDir(node, 'else-if') ||
+    !!findDir(node, 'else', true)
+  const parent = context.parent?.node
+  const hasParentIf =
+    parent &&
+    parent.type === NodeTypes.ELEMENT &&
+    parent.tagType === ElementTypes.TEMPLATE &&
+    (!!findDir(parent, 'if') ||
+      !!findDir(parent, 'else-if') ||
+      !!findDir(parent, 'else', true))
+  if (
+    keyProp &&
+    keyProp.type === NodeTypes.DIRECTIVE &&
+    keyProp.exp &&
+    !context.inVFor &&
+    !hasIf &&
+    !hasParentIf &&
+    !context.block.keyed
+  ) {
+    const keyExpr = propToExpression(keyProp)
+    if (
+      keyExpr &&
+      !isStaticExpression(keyExpr, context.options.bindingMetadata)
+    ) {
+      context.block.keyed = true
+      context.block.keyExpr = keyExpr
+    }
+  }
+}
+
 function transformComponentElement(
   node: ComponentNode,
   propsResult: PropsResult,

+ 1 - 23
packages/compiler-vapor/src/transforms/vSlot.ts

@@ -6,7 +6,6 @@ import {
   type SimpleExpressionNode,
   type TemplateChildNode,
   createCompilerError,
-  isCommentOrWhitespace,
   isTemplateNode,
   isVSlot,
 } from '@vue/compiler-dom'
@@ -24,12 +23,7 @@ import {
   type SlotBlockIRNode,
   type VaporDirectiveNode,
 } from '../ir'
-import {
-  findDir,
-  findProp,
-  isTransitionNode,
-  resolveExpression,
-} from '../utils'
+import { findDir, resolveExpression } from '../utils'
 import { markNonTemplate } from './transformText'
 
 export const transformVSlot: NodeTransform = (node, context) => {
@@ -91,22 +85,6 @@ function transformComponentSlot(
 
   const [block, onExit] = createSlotBlock(node, dir, context)
 
-  // only add key for slot content inside Transition
-  if (isTransitionNode(node) && nonSlotTemplateChildren.length) {
-    const nonCommentChild = nonSlotTemplateChildren.find(
-      n => !isCommentOrWhitespace(n),
-    )
-    if (nonCommentChild) {
-      const keyProp = findProp(
-        nonCommentChild as ElementNode,
-        'key',
-      ) as VaporDirectiveNode
-      if (keyProp) {
-        block.key = keyProp.exp
-      }
-    }
-  }
-
   const { slots } = context
 
   return () => {