Переглянути джерело

refactor(compiler-vapor): introduce a dedicated KeyIRNode + generator path (#14413)

zhiyuanzmj 2 місяців тому
батько
коміт
74d08c81b6
21 змінених файлів з 567 додано та 259 видалено
  1. 3 1
      packages/compiler-vapor/__tests__/transforms/TransformTransition.spec.ts
  2. 6 5
      packages/compiler-vapor/__tests__/transforms/__snapshots__/TransformTransition.spec.ts.snap
  3. 49 0
      packages/compiler-vapor/__tests__/transforms/__snapshots__/logicalIndex.spec.ts.snap
  4. 0 90
      packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap
  5. 202 0
      packages/compiler-vapor/__tests__/transforms/__snapshots__/transformKey.spec.ts.snap
  6. 10 0
      packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap
  7. 38 0
      packages/compiler-vapor/__tests__/transforms/logicalIndex.spec.ts
  8. 0 61
      packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts
  9. 152 0
      packages/compiler-vapor/__tests__/transforms/transformKey.spec.ts
  10. 6 0
      packages/compiler-vapor/__tests__/transforms/vOnce.spec.ts
  11. 2 0
      packages/compiler-vapor/src/compile.ts
  12. 2 14
      packages/compiler-vapor/src/generate.ts
  13. 1 36
      packages/compiler-vapor/src/generators/block.ts
  14. 26 0
      packages/compiler-vapor/src/generators/key.ts
  15. 3 0
      packages/compiler-vapor/src/generators/operation.ts
  16. 1 0
      packages/compiler-vapor/src/index.ts
  17. 16 3
      packages/compiler-vapor/src/ir/index.ts
  18. 1 47
      packages/compiler-vapor/src/transforms/transformElement.ts
  19. 39 0
      packages/compiler-vapor/src/transforms/transformKey.ts
  20. 9 1
      packages/compiler-vapor/src/transforms/utils.ts
  21. 1 1
      packages/compiler-vapor/src/transforms/vFor.ts

+ 3 - 1
packages/compiler-vapor/__tests__/transforms/TransformTransition.spec.ts

@@ -2,6 +2,7 @@ import { makeCompile } from './_utils'
 import {
   transformChildren,
   transformElement,
+  transformKey,
   transformText,
   transformVBind,
   transformVIf,
@@ -15,6 +16,7 @@ const compileWithElementTransform = makeCompile({
   nodeTransforms: [
     transformText,
     transformVIf,
+    transformKey,
     transformElement,
     transformVSlot,
     transformChildren,
@@ -57,7 +59,7 @@ describe('compiler: transition', () => {
     )
 
     expect(code).toMatchSnapshot()
-    expect(code).contains('_createKeyedFragment(() => _ctx.key')
+    expect(code).contains('_createKeyedFragment(() => (_ctx.key)')
   })
 
   function checkWarning(template: string, shouldWarn = true) {

+ 6 - 5
packages/compiler-vapor/__tests__/transforms/__snapshots__/TransformTransition.spec.ts.snap

@@ -92,15 +92,16 @@ exports[`compiler: transition > work with dynamic keyed children 1`] = `
 const t0 = _template("<h1>foo")
 
 export function render(_ctx) {
-  const n1 = _createComponent(_VaporTransition, null, {
+  const n2 = _createComponent(_VaporTransition, null, {
     "default": () => {
-      return _createKeyedFragment(() => _ctx.key, () => {
-        const n0 = t0()
-        return n0
+      const n0 = _createKeyedFragment(() => (_ctx.key), () => {
+        const n1 = t0()
+        return n1
       })
+      return n0
     }
   }, true)
-  return n1
+  return n2
 }"
 `;
 

+ 49 - 0
packages/compiler-vapor/__tests__/transforms/__snapshots__/logicalIndex.spec.ts.snap

@@ -151,6 +151,55 @@ export function render(_ctx) {
 }"
 `;
 
+exports[`compiler: logicalIndex > setInsertionState scenarios > key scenarios > key append 1`] = `
+"import { setInsertionState as _setInsertionState, createKeyedFragment as _createKeyedFragment, template as _template } from 'vue';
+const t0 = _template("<div>")
+const t1 = _template("<div><span>A</span>", true)
+
+export function render(_ctx) {
+  const n2 = t1()
+  _setInsertionState(n2, null, 1, true)
+  const n0 = _createKeyedFragment(() => (_ctx.id), () => {
+    const n1 = t0()
+    return n1
+  })
+  return n2
+}"
+`;
+
+exports[`compiler: logicalIndex > setInsertionState scenarios > key scenarios > key in middle 1`] = `
+"import { child as _child, next as _next, setInsertionState as _setInsertionState, createKeyedFragment as _createKeyedFragment, template as _template } from 'vue';
+const t0 = _template("<div>")
+const t1 = _template("<div><span>A</span><!><span>B", true)
+
+export function render(_ctx) {
+  const n3 = t1()
+  const n2 = _next(_child(n3), 1)
+  _setInsertionState(n3, n2, 1, true)
+  const n0 = _createKeyedFragment(() => (_ctx.id), () => {
+    const n1 = t0()
+    return n1
+  })
+  return n3
+}"
+`;
+
+exports[`compiler: logicalIndex > setInsertionState scenarios > key scenarios > key prepend 1`] = `
+"import { setInsertionState as _setInsertionState, createKeyedFragment as _createKeyedFragment, template as _template } from 'vue';
+const t0 = _template("<div>")
+const t1 = _template("<div><span>A", true)
+
+export function render(_ctx) {
+  const n2 = t1()
+  _setInsertionState(n2, 0, 0, true)
+  const n0 = _createKeyedFragment(() => (_ctx.id), () => {
+    const n1 = t0()
+    return n1
+  })
+  return n2
+}"
+`;
+
 exports[`compiler: logicalIndex > setInsertionState scenarios > mixed scenarios > prepend + append 1`] = `
 "import { resolveComponent as _resolveComponent, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
 const t0 = _template("<div><span>A", true)

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

@@ -738,93 +738,3 @@ 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
-}"
-`;

+ 202 - 0
packages/compiler-vapor/__tests__/transforms/__snapshots__/transformKey.spec.ts.snap

@@ -0,0 +1,202 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`compiler: key > with dynamic key > <component is/> + key 1`] = `
+"import { createDynamicComponent as _createDynamicComponent, createKeyedFragment as _createKeyedFragment } from 'vue';
+
+export function render(_ctx) {
+  const n0 = _createKeyedFragment(() => (_ctx.id), () => {
+    const n1 = _createDynamicComponent(() => (_ctx.view), null, null, true)
+    return n1
+  })
+  return n0
+}"
+`;
+
+exports[`compiler: key > with dynamic key > basic key 1`] = `
+"import { txt as _txt, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createKeyedFragment as _createKeyedFragment, template as _template } from 'vue';
+const t0 = _template("<div> ", true)
+
+export function render(_ctx) {
+  const n0 = _createKeyedFragment(() => (_ctx.ok), () => {
+    const n1 = t0()
+    const x1 = _txt(n1)
+    _renderEffect(() => _setText(x1, _toDisplayString(_ctx.msg)))
+    return n1
+  })
+  return n0
+}"
+`;
+
+exports[`compiler: key > with dynamic key > complex key 1`] = `
+"import { createKeyedFragment as _createKeyedFragment, template as _template } from 'vue';
+const t0 = _template("<div>", true)
+
+export function render(_ctx) {
+  const n0 = _createKeyedFragment(() => (_ctx.a,_ctx.b), () => {
+    const n1 = t0()
+    return n1
+  })
+  return n0
+}"
+`;
+
+exports[`compiler: key > with dynamic key > component + key 1`] = `
+"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, createKeyedFragment as _createKeyedFragment } from 'vue';
+
+export function render(_ctx) {
+  const _component_Foo = _resolveComponent("Foo")
+  const n0 = _createKeyedFragment(() => (_ctx.id), () => {
+    const n1 = _createComponentWithFallback(_component_Foo, null, null, true)
+    return n1
+  })
+  return n0
+}"
+`;
+
+exports[`compiler: key > with dynamic key > component slot + key 1`] = `
+"import { resolveComponent as _resolveComponent, createKeyedFragment as _createKeyedFragment, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
+const t0 = _template("<div>foo")
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n2 = _createComponentWithFallback(_component_Comp, null, {
+    "default": () => {
+      const n0 = _createKeyedFragment(() => (_ctx.id), () => {
+        const n1 = t0()
+        return n1
+      })
+      return n0
+    }
+  }, true)
+  return n2
+}"
+`;
+
+exports[`compiler: key > with dynamic key > element + key 1`] = `
+"import { createKeyedFragment as _createKeyedFragment, template as _template } from 'vue';
+const t0 = _template("<div>", true)
+
+export function render(_ctx) {
+  const n0 = _createKeyedFragment(() => (_ctx.id), () => {
+    const n1 = t0()
+    return n1
+  })
+  return n0
+}"
+`;
+
+exports[`compiler: key > with dynamic key > nested elements + key 1`] = `
+"import { resolveComponent as _resolveComponent, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, createKeyedFragment as _createKeyedFragment, template as _template } from 'vue';
+const t0 = _template("<div>", true)
+
+export function render(_ctx) {
+  const _component_Foo = _resolveComponent("Foo")
+  const n2 = t0()
+  _setInsertionState(n2, null, 0, true)
+  const n0 = _createKeyedFragment(() => (_ctx.id), () => {
+    const n1 = _createComponentWithFallback(_component_Foo)
+    return n1
+  })
+  return n2
+}"
+`;
+
+exports[`compiler: key > with dynamic key > shortbind key 1`] = `
+"import { createKeyedFragment as _createKeyedFragment, template as _template } from 'vue';
+const t0 = _template("<div>", true)
+
+export function render(_ctx) {
+  const n0 = _createKeyedFragment(() => (_ctx.key), () => {
+    const n1 = t0()
+    return n1
+  })
+  return n0
+}"
+`;
+
+exports[`compiler: key > with dynamic key > v-else-if + key 1`] = `
+"import { createKeyedFragment as _createKeyedFragment, 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
+  }, () => _createIf(() => (_ctx.foo), () => {
+    const n4 = _createKeyedFragment(() => (_ctx.id), () => {
+      const n5 = t0()
+      return n5
+    })
+    return n4
+  }), null, 0)
+  return n0
+}"
+`;
+
+exports[`compiler: key > 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: key > with dynamic key > v-if + key 1`] = `
+"import { createKeyedFragment as _createKeyedFragment, createIf as _createIf, template as _template } from 'vue';
+const t0 = _template("<div>", true)
+
+export function render(_ctx) {
+  const n0 = _createIf(() => (_ctx.ok), () => {
+    const n2 = _createKeyedFragment(() => (_ctx.id), () => {
+      const n3 = t0()
+      return n3
+    })
+    return n2
+  })
+  return n0
+}"
+`;
+
+exports[`compiler: key > 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: key > 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: key > 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
+}"
+`;
+
+exports[`compiler: key > with static key > static expression key 1`] = `
+"import { template as _template } from 'vue';
+const t0 = _template("<div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  return n0
+}"
+`;

+ 10 - 0
packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap

@@ -72,6 +72,16 @@ export function render(_ctx) {
 }"
 `;
 
+exports[`compiler: v-once > with key 1`] = `
+"import { template as _template } from 'vue';
+const t0 = _template("<div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  return n0
+}"
+`;
+
 exports[`compiler: v-once > with v-for 1`] = `
 "import { createFor as _createFor, template as _template } from 'vue';
 const t0 = _template("<div>")

+ 38 - 0
packages/compiler-vapor/__tests__/transforms/logicalIndex.spec.ts

@@ -2,6 +2,7 @@ import { makeCompile } from './_utils'
 import {
   transformChildren,
   transformElement,
+  transformKey,
   transformSlotOutlet,
   transformText,
   transformVFor,
@@ -14,6 +15,7 @@ const compileWithTransforms = makeCompile({
     transformText,
     transformVIf,
     transformVFor,
+    transformKey,
     transformVSlot,
     transformSlotOutlet,
     transformElement,
@@ -423,5 +425,41 @@ describe('compiler: logicalIndex', () => {
         expect(code).toMatchSnapshot()
       })
     })
+
+    describe('key scenarios', () => {
+      test('key prepend', () => {
+        const { code } = compileWithTransforms(`
+          <div>
+            <div :key="id" />
+            <span>A</span>
+          </div>
+        `)
+        expect(code).toContain('_setInsertionState(n2, 0, 0, true)')
+        expect(code).toMatchSnapshot()
+      })
+
+      test('key append', () => {
+        const { code } = compileWithTransforms(`
+          <div>
+            <span>A</span>
+            <div :key="id" />
+          </div>
+        `)
+        expect(code).toMatchSnapshot()
+        expect(code).toContain('_setInsertionState(n2, null, 1, true)')
+      })
+
+      test('key in middle', () => {
+        const { code } = compileWithTransforms(`
+          <div>
+            <span>A</span>
+            <div :key="id" />
+            <span>B</span>
+          </div>
+        `)
+        expect(code).toContain('_setInsertionState(n3, n2, 1, true)')
+        expect(code).toMatchSnapshot()
+      })
+    })
   })
 })

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

@@ -1242,65 +1242,4 @@ 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(')
-    })
-  })
 })

+ 152 - 0
packages/compiler-vapor/__tests__/transforms/transformKey.spec.ts

@@ -0,0 +1,152 @@
+import { makeCompile } from './_utils'
+import {
+  IRNodeTypes,
+  transformChildren,
+  transformElement,
+  transformKey,
+  transformText,
+  transformVFor,
+  transformVIf,
+  transformVSlot,
+} from '../../src'
+import { NodeTypes } from '@vue/compiler-dom'
+
+const compileWithKey = makeCompile({
+  nodeTransforms: [
+    transformVIf,
+    transformKey,
+    transformVFor,
+    transformText,
+    transformElement,
+    transformVSlot,
+    transformChildren,
+  ],
+})
+
+describe('compiler: key', () => {
+  describe('with dynamic key', () => {
+    test('basic key', () => {
+      const { code, helpers, ir } = compileWithKey(
+        `<div :key="ok">{{msg}}</div>`,
+      )
+      expect(code).toMatchSnapshot()
+
+      expect(helpers).contains('createKeyedFragment')
+
+      expect([...ir.template.keys()]).toEqual(['<div> '])
+
+      const op = ir.block.dynamic.children[0].operation
+      expect(op).toMatchObject({
+        type: IRNodeTypes.KEY,
+        id: 0,
+        value: {
+          type: NodeTypes.SIMPLE_EXPRESSION,
+          content: 'ok',
+          isStatic: false,
+        },
+        block: {
+          type: IRNodeTypes.BLOCK,
+          dynamic: {
+            children: [{ template: 0 }],
+          },
+        },
+      })
+      expect(ir.block.returns).toEqual([0])
+
+      expect(ir.block.dynamic).toMatchObject({
+        children: [{ id: 0 }],
+      })
+
+      expect(ir.block.effect).toEqual([])
+    })
+
+    test('complex key', () => {
+      const { code } = compileWithKey(`<div :key="a,b" />`)
+      expect(code).toMatchSnapshot()
+      expect(code).contains('_createKeyedFragment(() => (_ctx.a,_ctx.b)')
+    })
+
+    test('shortbind key', () => {
+      const { code } = compileWithKey(`<div :key />`)
+      expect(code).toMatchSnapshot()
+      expect(code).contains('_createKeyedFragment(() => (_ctx.key)')
+    })
+
+    test('component + key', () => {
+      const { code } = compileWithKey(`<Foo :key="id" />`)
+      expect(code).toMatchSnapshot()
+      expect(code).contains('_createKeyedFragment(() => (_ctx.id)')
+    })
+
+    test('component slot + key', () => {
+      const { code } = compileWithKey(`<Comp><div :key="id">foo</div></Comp>`)
+      expect(code).toMatchSnapshot()
+      expect(code).contains('_createKeyedFragment(() => (_ctx.id)')
+    })
+
+    test('element + key', () => {
+      const { code } = compileWithKey(`<div :key="id"></div>`)
+      expect(code).toMatchSnapshot()
+      expect(code).contains('_createKeyedFragment(() => (_ctx.id)')
+    })
+
+    test('nested elements + key', () => {
+      const { code } = compileWithKey(`<div><Foo :key="id" /></div>`)
+      expect(code).toMatchSnapshot()
+      expect(code).contains('_createKeyedFragment(() => (_ctx.id)')
+    })
+
+    test('<component is/> + key', () => {
+      const { code } = compileWithKey(`<component :is="view" :key="id" />`)
+      expect(code).toMatchSnapshot()
+      expect(code).contains('_createKeyedFragment(() => (_ctx.id)')
+    })
+
+    test('v-if + key', () => {
+      const { code } = compileWithKey(`<div v-if="ok" :key="id"></div>`)
+      expect(code).toMatchSnapshot()
+      expect(code).contains('_createKeyedFragment(')
+    })
+
+    test('v-else-if + key', () => {
+      const { code } = compileWithKey(
+        `<div v-if="ok" /><div v-else-if="foo" :key="id"></div>`,
+      )
+      expect(code).toMatchSnapshot()
+      expect(code).contains('_createKeyedFragment(')
+    })
+
+    test('v-for + key', () => {
+      const { code } = compileWithKey(`<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 } = compileWithKey(`<Foo key="1" />`)
+      expect(code).toMatchSnapshot()
+      expect(code).not.contains('_createKeyedFragment(')
+    })
+
+    test('element + key', () => {
+      const { code } = compileWithKey(`<div key="1"></div>`)
+      expect(code).toMatchSnapshot()
+      expect(code).not.contains('_createKeyedFragment(')
+    })
+
+    test('<component is/> + key', () => {
+      const { code } = compileWithKey(`<component :is="view" key="1" />`)
+      expect(code).toMatchSnapshot()
+      expect(code).not.contains('_createKeyedFragment(')
+    })
+
+    test('static expression key', () => {
+      const { code } = compileWithKey(`<div :key="1" />`)
+      expect(code).toMatchSnapshot()
+      expect(code).not.contains('_createKeyedFragment(')
+    })
+  })
+})

+ 6 - 0
packages/compiler-vapor/__tests__/transforms/vOnce.spec.ts

@@ -216,4 +216,10 @@ describe('compiler: v-once', () => {
       once: true,
     })
   })
+
+  test('with key', () => {
+    const { ir, code } = compileWithOnce(`<div :key="foo" v-once />`)
+    expect(code).toMatchSnapshot()
+    expect(ir.block.dynamic.operation).toBe(undefined)
+  })
 })

+ 2 - 0
packages/compiler-vapor/src/compile.ts

@@ -28,6 +28,7 @@ import { transformSlotOutlet } from './transforms/transformSlotOutlet'
 import { transformVSlot } from './transforms/vSlot'
 import { transformTransition } from './transforms/transformTransition'
 import type { HackOptions } from './ir'
+import { transformKey } from './transforms/transformKey'
 
 export { wrapTemplate } from './transforms/utils'
 
@@ -81,6 +82,7 @@ export function getBaseTransformPreset(): TransformPreset {
       transformVOnce,
       transformVIf,
       transformVFor,
+      transformKey,
       transformSlotOutlet,
       transformTemplateRef,
       transformElement,

+ 2 - 14
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 { genBlock, genBlockContent, genKeyedFragment } from './generators/block'
+import { genBlockContent } from './generators/block'
 import { genTemplates } from './generators/template'
 import {
   type CodeFragment,
@@ -204,19 +204,7 @@ export function generate(
   if (ir.hasDeferredVShow) {
     push(NEWLINE, `const deferredApplyVShows = []`)
   }
-  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(...genBlockContent(ir.block, context, true))
   push(INDENT_END, NEWLINE)
 
   if (!inline) {

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

@@ -13,16 +13,14 @@ 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[] {
-  const blockFn: CodeFragment[] = [
+  return [
     '(',
     ...args,
     ') => {',
@@ -32,39 +30,6 @@ 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(

+ 26 - 0
packages/compiler-vapor/src/generators/key.ts

@@ -0,0 +1,26 @@
+import type { CodegenContext } from '../generate'
+import type { KeyIRNode } from '../ir'
+import { genBlock } from './block'
+import { genExpression } from './expression'
+import { type CodeFragment, NEWLINE, buildCodeFragment, genCall } from './utils'
+
+export function genKey(
+  oper: KeyIRNode,
+  context: CodegenContext,
+): CodeFragment[] {
+  const { id, value, block } = oper
+  const [frag, push] = buildCodeFragment()
+  const blockFn = genBlock(block, context)
+
+  push(
+    NEWLINE,
+    `const n${id} = `,
+    ...genCall(
+      context.helper('createKeyedFragment'),
+      [`() => (`, ...genExpression(value, context), ')'],
+      blockFn,
+    ),
+  )
+
+  return frag
+}

+ 3 - 0
packages/compiler-vapor/src/generators/operation.ts

@@ -26,6 +26,7 @@ import { genCreateComponent } from './component'
 import { genSlotOutlet } from './slotOutlet'
 import { processExpressions } from './expression'
 import { genBuiltinDirective } from './directive'
+import { genKey } from './key'
 
 export function genOperations(
   opers: OperationNode[],
@@ -77,6 +78,8 @@ export function genOperation(
       return genIf(oper, context)
     case IRNodeTypes.FOR:
       return genFor(oper, context)
+    case IRNodeTypes.KEY:
+      return genKey(oper, context)
     case IRNodeTypes.CREATE_COMPONENT_NODE:
       return genCreateComponent(oper, context)
     case IRNodeTypes.SLOT_OUTLET_NODE:

+ 1 - 0
packages/compiler-vapor/src/index.ts

@@ -46,6 +46,7 @@ export { transformVShow } from './transforms/vShow'
 export { transformVText } from './transforms/vText'
 export { transformVIf } from './transforms/vIf'
 export { transformVFor } from './transforms/vFor'
+export { transformKey } from './transforms/transformKey'
 export { transformVModel } from './transforms/vModel'
 export { transformComment } from './transforms/transformComment'
 export { transformSlotOutlet } from './transforms/transformSlotOutlet'

+ 16 - 3
packages/compiler-vapor/src/ir/index.ts

@@ -32,13 +32,13 @@ export enum IRNodeTypes {
 
   IF,
   FOR,
+  KEY,
 
   GET_TEXT_CHILD,
 }
 
 export interface BaseIRNode {
   type: IRNodeTypes
-  key?: SimpleExpressionNode | undefined
 }
 
 export type CoreHelper = keyof typeof import('packages/runtime-dom/src')
@@ -53,8 +53,6 @@ export interface BlockIRNode extends BaseIRNode {
   effect: IREffect[]
   operation: OperationNode[]
   returns: number[]
-  keyed?: boolean
-  keyExpr?: SimpleExpressionNode
 }
 
 export interface RootIRNode {
@@ -108,6 +106,18 @@ export interface ForIRNode extends BaseIRNode, IRFor {
   last?: boolean
 }
 
+export interface KeyIRNode extends BaseIRNode {
+  type: IRNodeTypes.KEY
+  id: number
+  value: SimpleExpressionNode
+  block: BlockIRNode
+  parent?: number
+  anchor?: number
+  logicalIndex?: number
+  append?: boolean
+  last?: boolean
+}
+
 export interface SetPropIRNode extends BaseIRNode {
   type: IRNodeTypes.SET_PROP
   element: number
@@ -247,6 +257,7 @@ export type OperationNode =
   | DirectiveIRNode
   | IfIRNode
   | ForIRNode
+  | KeyIRNode
   | CreateComponentIRNode
   | SlotOutletIRNode
   | GetTextChildIRNode
@@ -309,6 +320,7 @@ export type VaporDirectiveNode = Overwrite<
 export type InsertionStateTypes =
   | IfIRNode
   | ForIRNode
+  | KeyIRNode
   | SlotOutletIRNode
   | CreateComponentIRNode
 
@@ -318,6 +330,7 @@ export function isBlockOperation(op: OperationNode): op is InsertionStateTypes {
     type === IRNodeTypes.CREATE_COMPONENT_NODE ||
     type === IRNodeTypes.SLOT_OUTLET_NODE ||
     type === IRNodeTypes.IF ||
+    type === IRNodeTypes.KEY ||
     type === IRNodeTypes.FOR
   )
 }

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

@@ -44,13 +44,7 @@ import {
   type VaporDirectiveNode,
 } from '../ir'
 import { EMPTY_EXPRESSION } from './utils'
-import {
-  findDir,
-  findProp,
-  isBuiltInComponent,
-  isStaticExpression,
-  propToExpression,
-} from '../utils'
+import { findProp, isBuiltInComponent } from '../utils'
 import { IMPORT_EXP_END, IMPORT_EXP_START } from '../generators/utils'
 
 export const isReservedProp: (key: string) => boolean = /*#__PURE__*/ makeMap(
@@ -93,7 +87,6 @@ export const transformElement: NodeTransform = (node, context) => {
       node.tagType === ElementTypes.COMPONENT || isCustomElement
 
     const isDynamicComponent = isComponentTag(node.tag)
-    maybeMarkKeyedBlock(node, context)
 
     const propsResult = buildProps(
       node,
@@ -203,45 +196,6 @@ 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,

+ 39 - 0
packages/compiler-vapor/src/transforms/transformKey.ts

@@ -0,0 +1,39 @@
+import { NodeTypes, type SimpleExpressionNode } from '@vue/compiler-dom'
+import type { NodeTransform } from '../transform'
+import { DynamicFlag, IRNodeTypes } from '../ir'
+import { normalizeBindShorthand } from './vBind'
+import { findDir, findProp, isStaticExpression } from '../utils'
+import { newBlock, wrapTemplate } from './utils'
+
+export const transformKey: NodeTransform = (node, context) => {
+  if (
+    node.type !== NodeTypes.ELEMENT ||
+    context.inVOnce ||
+    findDir(node, 'for')
+  )
+    return
+
+  const dir = findProp(node, 'key', true, true)
+  if (!dir || dir.type === NodeTypes.ATTRIBUTE) return
+
+  let value: SimpleExpressionNode
+  value = dir.exp || normalizeBindShorthand(dir.arg!, context)
+  if (isStaticExpression(value, context.options.bindingMetadata)) return
+
+  let id = context.reference()
+  context.dynamic.flags |= DynamicFlag.NON_TEMPLATE | DynamicFlag.INSERT
+
+  context.node = node = wrapTemplate(node, ['key'])
+  const block = newBlock(node)
+  const exitBlock = context.enterBlock(block)
+
+  return () => {
+    exitBlock()
+    context.dynamic.operation = {
+      type: IRNodeTypes.KEY,
+      id,
+      value,
+      block,
+    }
+  }
+}

+ 9 - 1
packages/compiler-vapor/src/transforms/utils.ts

@@ -74,7 +74,15 @@ export function wrapTemplate(node: ElementNode, dirs: string[]): TemplateNode {
   const reserved: Array<AttributeNode | DirectiveNode> = []
   const pass: Array<AttributeNode | DirectiveNode> = []
   node.props.forEach(prop => {
-    if (prop.type === NodeTypes.DIRECTIVE && dirs.includes(prop.name)) {
+    if (
+      prop.type === NodeTypes.DIRECTIVE &&
+      (dirs.includes(prop.name) ||
+        (prop.name === 'bind' &&
+          prop.arg &&
+          prop.arg.type === NodeTypes.SIMPLE_EXPRESSION &&
+          prop.arg.content === 'key' &&
+          dirs.includes('key')))
+    ) {
       reserved.push(prop)
     } else {
       pass.push(prop)

+ 1 - 1
packages/compiler-vapor/src/transforms/vFor.ts

@@ -52,7 +52,7 @@ export function processFor(
     node.tagType === ElementTypes.COMPONENT ||
     // template v-for with a single component child
     isTemplateWithSingleComponent(node)
-  context.node = node = wrapTemplate(node, ['for'])
+  context.node = node = wrapTemplate(node, ['for', 'key'])
   context.dynamic.flags |= DynamicFlag.NON_TEMPLATE | DynamicFlag.INSERT
   const id = context.reference()
   const render: BlockIRNode = newBlock(node)