]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(slots): properly force update on forwarded slots
authorEvan You <yyx990803@gmail.com>
Thu, 16 Jul 2020 00:12:49 +0000 (20:12 -0400)
committerEvan You <yyx990803@gmail.com>
Thu, 16 Jul 2020 00:12:49 +0000 (20:12 -0400)
fix #1594

packages/compiler-core/__tests__/transforms/vSlot.spec.ts
packages/compiler-core/src/transforms/vSlot.ts
packages/runtime-core/src/componentSlots.ts
packages/runtime-core/src/helpers/renderSlot.ts
packages/runtime-core/src/vnode.ts
packages/shared/src/index.ts
packages/shared/src/slotFlags.ts [new file with mode: 0644]

index c9a2e3e7cb98a6496505ce9d4b6d1cf4073e8441..0942b007df2c98f52239f700d5a9660d5235d4c0 100644 (file)
@@ -719,6 +719,23 @@ describe('compiler: transform component slots', () => {
     expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
   })
 
+  test('generate flag on forwarded slots', () => {
+    const { slots } = parseWithSlots(`<Comp><slot/></Comp>`)
+    expect(slots).toMatchObject({
+      type: NodeTypes.JS_OBJECT_EXPRESSION,
+      properties: [
+        {
+          key: { content: `default` },
+          value: { type: NodeTypes.JS_FUNCTION_EXPRESSION }
+        },
+        {
+          key: { content: `_` },
+          value: { content: `3` } // forwarded
+        }
+      ]
+    })
+  })
+
   describe('errors', () => {
     test('error on extraneous children w/ named default slot', () => {
       const onError = jest.fn()
index 0cec12745b1974eec4b518fbc6ec6f8cf1ef08fd..fa8e471f674d2db607c0d83f9ddef190a77a29bb 100644 (file)
@@ -33,6 +33,7 @@ import {
 } from '../utils'
 import { CREATE_SLOTS, RENDER_LIST, WITH_CTX } from '../runtimeHelpers'
 import { parseForExpression, createForLoopParams } from './vFor'
+import { SlotFlags } from '@vue/shared/src'
 
 const defaultFallback = createSimpleExpression(`undefined`, false)
 
@@ -321,13 +322,19 @@ export function buildSlots(
     }
   }
 
+  const slotFlag = hasDynamicSlots
+    ? SlotFlags.DYNAMIC
+    : hasForwardedSlots(node.children)
+      ? SlotFlags.FORWARDED
+      : SlotFlags.STABLE
+
   let slots = createObjectExpression(
     slotsProperties.concat(
       createObjectProperty(
         `_`,
         // 2 = compiled but dynamic = can skip normalization, but must run diff
         // 1 = compiled and static = can skip normalization AND diff as optimized
-        createSimpleExpression(hasDynamicSlots ? `2` : `1`, false)
+        createSimpleExpression('' + slotFlag, false)
       )
     ),
     loc
@@ -354,3 +361,19 @@ function buildDynamicSlot(
     createObjectProperty(`fn`, fn)
   ])
 }
+
+function hasForwardedSlots(children: TemplateChildNode[]): boolean {
+  for (let i = 0; i < children.length; i++) {
+    const child = children[i]
+    if (child.type === NodeTypes.ELEMENT) {
+      if (
+        child.tagType === ElementTypes.SLOT ||
+        (child.tagType === ElementTypes.ELEMENT &&
+          hasForwardedSlots(child.children))
+      ) {
+        return true
+      }
+    }
+  }
+  return false
+}
index b0a52b0f50564b8c9ff26823fc5e2ee14b89b4d4..4a6a1b75375932fb394804a66d742ed80471fb48 100644 (file)
@@ -12,7 +12,8 @@ import {
   EMPTY_OBJ,
   ShapeFlags,
   extend,
-  def
+  def,
+  SlotFlags
 } from '@vue/shared'
 import { warn } from './warning'
 import { isKeepAlive } from './components/KeepAlive'
@@ -27,24 +28,25 @@ export type InternalSlots = {
 
 export type Slots = Readonly<InternalSlots>
 
-export const enum CompiledSlotTypes {
-  STATIC = 1,
-  DYNAMIC = 2
-}
-
 export type RawSlots = {
   [name: string]: unknown
   // manual render fn hint to skip forced children updates
   $stable?: boolean
-  // internal, for tracking slot owner instance. This is attached during
-  // normalizeChildren when the component vnode is created.
+  /**
+   * for tracking slot owner instance. This is attached during
+   * normalizeChildren when the component vnode is created.
+   * @internal
+   */
   _ctx?: ComponentInternalInstance | null
-  // internal, indicates compiler generated slots
-  // we use a reserved property instead of a vnode patchFlag because the slots
-  // object may be directly passed down to a child component in a manual
-  // render function, and the optimization hint need to be on the slot object
-  // itself to be preserved.
-  _?: CompiledSlotTypes
+  /**
+   * indicates compiler generated slots
+   * we use a reserved property instead of a vnode patchFlag because the slots
+   * object may be directly passed down to a child component in a manual
+   * render function, and the optimization hint need to be on the slot object
+   * itself to be preserved.
+   * @internal
+   */
+  _?: SlotFlags
 }
 
 const isInternalKey = (key: string) => key[0] === '_' || key === '$stable'
@@ -141,8 +143,8 @@ export const updateSlots = (
         // Parent was HMR updated so slot content may have changed.
         // force update slots and mark instance for hmr as well
         extend(slots, children as Slots)
-      } else if (type === CompiledSlotTypes.STATIC) {
-        // compiled AND static.
+      } else if (type === SlotFlags.STABLE) {
+        // compiled AND stable.
         // no need to update, and skip stale slots removal.
         needDeletionCheck = false
       } else {
index 0cfb6ed694fbc242717dcd154a282a82d3e3eb4d..8744b4c30530fe24d7436ba253dd3d0281f945b4 100644 (file)
@@ -1,5 +1,5 @@
 import { Data } from '../component'
-import { Slots, RawSlots, CompiledSlotTypes } from '../componentSlots'
+import { Slots, RawSlots } from '../componentSlots'
 import {
   VNodeArrayChildren,
   openBlock,
@@ -7,7 +7,7 @@ import {
   Fragment,
   VNode
 } from '../vnode'
-import { PatchFlags } from '@vue/shared'
+import { PatchFlags, SlotFlags } from '@vue/shared'
 import { warn } from '../warning'
 
 /**
@@ -39,7 +39,7 @@ export function renderSlot(
       Fragment,
       { key: props.key },
       slot ? slot(props) : fallback ? fallback() : [],
-      (slots as RawSlots)._ === CompiledSlotTypes.STATIC
+      (slots as RawSlots)._ === SlotFlags.STABLE
         ? PatchFlags.STABLE_FRAGMENT
         : PatchFlags.BAIL
     )
index aa87d440a04d42d25148694cf22618476508c63d..a96519cb94c998bf83c3680e5989980a16402f2d 100644 (file)
@@ -8,7 +8,8 @@ import {
   normalizeClass,
   normalizeStyle,
   PatchFlags,
-  ShapeFlags
+  ShapeFlags,
+  SlotFlags
 } from '@vue/shared'
 import {
   ComponentInternalInstance,
@@ -542,10 +543,22 @@ export function normalizeChildren(vnode: VNode, children: unknown) {
       return
     } else {
       type = ShapeFlags.SLOTS_CHILDREN
-      if (!(children as RawSlots)._ && !(InternalObjectKey in children!)) {
+      const slotFlag = (children as RawSlots)._
+      if (!slotFlag && !(InternalObjectKey in children!)) {
         // if slots are not normalized, attach context instance
         // (compiled / normalized slots already have context)
         ;(children as RawSlots)._ctx = currentRenderingInstance
+      } else if (slotFlag === SlotFlags.FORWARDED && currentRenderingInstance) {
+        // a child component receives forwarded slots from the parent.
+        // its slot type is determined by its parent's slot type.
+        if (
+          currentRenderingInstance.vnode.patchFlag & PatchFlags.DYNAMIC_SLOTS
+        ) {
+          ;(children as RawSlots)._ = SlotFlags.DYNAMIC
+          vnode.patchFlag |= PatchFlags.DYNAMIC_SLOTS
+        } else {
+          ;(children as RawSlots)._ = SlotFlags.STABLE
+        }
       }
     }
   } else if (isFunction(children)) {
index 7caf7d7867969c27448fbc2e4f77f32ef87733b9..d886f0743472ff983467c8ba9bb7c8a6589e26fb 100644 (file)
@@ -3,6 +3,7 @@ import { makeMap } from './makeMap'
 export { makeMap }
 export * from './patchFlags'
 export * from './shapeFlags'
+export * from './slotFlags'
 export * from './globalsWhitelist'
 export * from './codeframe'
 export * from './mockWarn'
diff --git a/packages/shared/src/slotFlags.ts b/packages/shared/src/slotFlags.ts
new file mode 100644 (file)
index 0000000..d111b5e
--- /dev/null
@@ -0,0 +1,21 @@
+export const enum SlotFlags {
+  /**
+   * Stable slots that only reference slot props or context state. The slot
+   * can fully capture its own dependencies so when passed down the parent won't
+   * need to force the child to update.
+   */
+  STABLE = 1,
+  /**
+   * Slots that reference scope variables (v-for or an outer slot prop), or
+   * has conditional structure (v-if, v-for). The parent will need to force
+   * the child to update because the slot does not fully capture its dependencies.
+   */
+  DYNAMIC = 2,
+  /**
+   * <slot/> being forwarded into a child component. Whether the parent needs
+   * to update the child is dependent on what kind of slots the parent itself
+   * received. This has to be refined at runtime, when the child's vnode
+   * is being created (in `normalizeChildren`)
+   */
+  FORWARDED = 3
+}