Просмотр исходного кода

fix(slots): ensure different branches of dynamic slots have different keys

fix #6202
Evan You 3 лет назад
Родитель
Сommit
00036bb52c

+ 2 - 1
packages/compiler-core/__tests__/__snapshots__/scopeId.spec.ts.snap

@@ -43,7 +43,8 @@ export function render(_ctx, _cache) {
           name: \\"foo\\",
           fn: _withCtx(() => [
             _createElementVNode(\\"div\\")
-          ])
+          ]),
+          key: \\"0\\"
         }
       : undefined,
     _renderList(_ctx.list, (i) => {

+ 10 - 5
packages/compiler-core/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap

@@ -56,7 +56,8 @@ return function render(_ctx, _cache) {
     (_ctx.ok)
       ? {
           name: \\"one\\",
-          fn: _withCtx((props) => [_toDisplayString(props)])
+          fn: _withCtx((props) => [_toDisplayString(props)]),
+          key: \\"0\\"
         }
       : undefined
   ]), 1024 /* DYNAMIC_SLOTS */))
@@ -76,16 +77,19 @@ return function render(_ctx, _cache) {
       ok
         ? {
             name: \\"one\\",
-            fn: _withCtx(() => [\\"foo\\"])
+            fn: _withCtx(() => [\\"foo\\"]),
+            key: \\"0\\"
           }
         : orNot
           ? {
               name: \\"two\\",
-              fn: _withCtx((props) => [\\"bar\\"])
+              fn: _withCtx((props) => [\\"bar\\"]),
+              key: \\"1\\"
             }
           : {
               name: \\"one\\",
-              fn: _withCtx(() => [\\"baz\\"])
+              fn: _withCtx(() => [\\"baz\\"]),
+              key: \\"2\\"
             }
     ]), 1024 /* DYNAMIC_SLOTS */))
   }
@@ -105,7 +109,8 @@ return function render(_ctx, _cache) {
       ok
         ? {
             name: \\"one\\",
-            fn: _withCtx(() => [\\"hello\\"])
+            fn: _withCtx(() => [\\"hello\\"]),
+            key: \\"0\\"
           }
         : undefined
     ]), 1024 /* DYNAMIC_SLOTS */))

+ 10 - 5
packages/compiler-core/__tests__/transforms/vSlot.spec.ts

@@ -568,7 +568,8 @@ describe('compiler: transform component slots', () => {
                 fn: {
                   type: NodeTypes.JS_FUNCTION_EXPRESSION,
                   returns: [{ type: NodeTypes.TEXT, content: `hello` }]
-                }
+                },
+                key: `0`
               }),
               alternate: {
                 content: `undefined`,
@@ -616,7 +617,8 @@ describe('compiler: transform component slots', () => {
                       content: { content: `props` }
                     }
                   ]
-                }
+                },
+                key: `0`
               }),
               alternate: {
                 content: `undefined`,
@@ -660,7 +662,8 @@ describe('compiler: transform component slots', () => {
                   type: NodeTypes.JS_FUNCTION_EXPRESSION,
                   params: undefined,
                   returns: [{ type: NodeTypes.TEXT, content: `foo` }]
-                }
+                },
+                key: `0`
               }),
               alternate: {
                 type: NodeTypes.JS_CONDITIONAL_EXPRESSION,
@@ -671,7 +674,8 @@ describe('compiler: transform component slots', () => {
                     type: NodeTypes.JS_FUNCTION_EXPRESSION,
                     params: { content: `props` },
                     returns: [{ type: NodeTypes.TEXT, content: `bar` }]
-                  }
+                  },
+                  key: `1`
                 }),
                 alternate: createObjectMatcher({
                   name: `one`,
@@ -679,7 +683,8 @@ describe('compiler: transform component slots', () => {
                     type: NodeTypes.JS_FUNCTION_EXPRESSION,
                     params: undefined,
                     returns: [{ type: NodeTypes.TEXT, content: `baz` }]
-                  }
+                  },
+                  key: `2`
                 })
               }
             }

+ 18 - 6
packages/compiler-core/src/transforms/vSlot.ts

@@ -160,6 +160,7 @@ export function buildSlots(
   let hasNamedDefaultSlot = false
   const implicitDefaultChildren: TemplateChildNode[] = []
   const seenSlotNames = new Set<string>()
+  let conditionalBranchIndex = 0
 
   for (let i = 0; i < children.length; i++) {
     const slotElement = children[i]
@@ -210,7 +211,7 @@ export function buildSlots(
       dynamicSlots.push(
         createConditionalExpression(
           vIf.exp!,
-          buildDynamicSlot(slotName, slotFunction),
+          buildDynamicSlot(slotName, slotFunction, conditionalBranchIndex++),
           defaultFallback
         )
       )
@@ -243,10 +244,14 @@ export function buildSlots(
         conditional.alternate = vElse.exp
           ? createConditionalExpression(
               vElse.exp,
-              buildDynamicSlot(slotName, slotFunction),
+              buildDynamicSlot(
+                slotName,
+                slotFunction,
+                conditionalBranchIndex++
+              ),
               defaultFallback
             )
-          : buildDynamicSlot(slotName, slotFunction)
+          : buildDynamicSlot(slotName, slotFunction, conditionalBranchIndex++)
       } else {
         context.onError(
           createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, vElse.loc)
@@ -369,12 +374,19 @@ export function buildSlots(
 
 function buildDynamicSlot(
   name: ExpressionNode,
-  fn: FunctionExpression
+  fn: FunctionExpression,
+  index?: number
 ): ObjectExpression {
-  return createObjectExpression([
+  const props = [
     createObjectProperty(`name`, name),
     createObjectProperty(`fn`, fn)
-  ])
+  ]
+  if (index != null) {
+    props.push(
+      createObjectProperty(`key`, createSimpleExpression(String(index), true))
+    )
+  }
+  return createObjectExpression(props)
 }
 
 function hasForwardedSlots(children: TemplateChildNode[]): boolean {

+ 2 - 1
packages/compiler-ssr/__tests__/ssrComponent.spec.ts

@@ -166,7 +166,8 @@ describe('ssr: components', () => {
                         _createTextVNode(\\"foo\\")
                       ]
                     }
-                  })
+                  }),
+                  key: \\"0\\"
                 }
               : undefined
           ]), _parent))

+ 9 - 0
packages/runtime-core/__tests__/helpers/createSlots.spec.ts

@@ -17,6 +17,15 @@ describe('createSlot', () => {
     expect(actual).toEqual({ descriptor: slot })
   })
 
+  it('should attach key', () => {
+    const dynamicSlot = [{ name: 'descriptor', fn: slot, key: '1' }]
+
+    const actual = createSlots(record, dynamicSlot)
+    const ret = actual.descriptor()
+    // @ts-ignore
+    expect(ret.key).toBe('1')
+  })
+
   it('should add all slots to the record', () => {
     const dynamicSlot = [
       { name: 'descriptor', fn: slot },

+ 10 - 1
packages/runtime-core/src/helpers/createSlots.ts

@@ -4,6 +4,7 @@ import { isArray } from '@vue/shared'
 interface CompiledSlotDescriptor {
   name: string
   fn: Slot
+  key?: string
 }
 
 /**
@@ -27,7 +28,15 @@ export function createSlots(
       }
     } else if (slot) {
       // conditional single slot generated by <template v-if="..." #foo>
-      slots[slot.name] = slot.fn
+      slots[slot.name] = slot.key
+        ? (...args: any[]) => {
+            const res = slot.fn(...args)
+            // attach branch key so each conditional branch is considered a
+            // different fragment
+            ;(res as any).key = slot.key
+            return res
+          }
+        : slot.fn
     }
   }
   return slots

+ 8 - 1
packages/runtime-core/src/helpers/renderSlot.ts

@@ -66,7 +66,14 @@ export function renderSlot(
   const validSlotContent = slot && ensureValidVNode(slot(props))
   const rendered = createBlock(
     Fragment,
-    { key: props.key || `_${name}` },
+    {
+      key:
+        props.key ||
+        // slot content array of a dynamic conditional slot may have a branch
+        // key attached in the `createSlots` helper, respect that
+        (validSlotContent && (validSlotContent as any).key) ||
+        `_${name}`
+    },
     validSlotContent || (fallback ? fallback() : []),
     validSlotContent && (slots as RawSlots)._ === SlotFlags.STABLE
       ? PatchFlags.STABLE_FRAGMENT