]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(runtime-core): should not track dynamic children when the user calls a compiled...
authorHcySunYang <HcySunYang@outlook.com>
Tue, 25 May 2021 17:33:41 +0000 (01:33 +0800)
committerGitHub <noreply@github.com>
Tue, 25 May 2021 17:33:41 +0000 (13:33 -0400)
fix #3548, partial fix for #3569

packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts
packages/runtime-core/src/compat/instance.ts
packages/runtime-core/src/compat/renderFn.ts
packages/runtime-core/src/componentRenderContext.ts
packages/runtime-core/src/componentSlots.ts
packages/runtime-core/src/helpers/renderSlot.ts
packages/runtime-core/src/vnode.ts

index 3c653281f1d8156628e76617ca6173ed0ff860d3..82f6f02fb1e51c6a94e0f19616aa5eb4b8096454 100644 (file)
@@ -2,6 +2,7 @@ import {
   h,
   Fragment,
   createVNode,
+  createCommentVNode,
   openBlock,
   createBlock,
   render,
@@ -576,4 +577,119 @@ describe('renderer: optimized mode', () => {
     await nextTick()
     expect(inner(root)).toBe('<div>World</div>')
   })
+
+  // #3548
+  test('should not track dynamic children when the user calls a compiled slot inside template expression', () => {
+    const Comp = {
+      setup(props: any, { slots }: SetupContext) {
+        return () => {
+          return (
+            openBlock(),
+            (block = createBlock('section', null, [
+              renderSlot(slots, 'default')
+            ]))
+          )
+        }
+      }
+    }
+
+    let dynamicVNode: VNode
+    const Wrapper = {
+      setup(props: any, { slots }: SetupContext) {
+        return () => {
+          return (
+            openBlock(),
+            createBlock(Comp, null, {
+              default: withCtx(() => {
+                return [
+                  (dynamicVNode = createVNode(
+                    'div',
+                    {
+                      class: {
+                        foo: !!slots.default!()
+                      }
+                    },
+                    null,
+                    PatchFlags.CLASS
+                  ))
+                ]
+              }),
+              _: 1
+            })
+          )
+        }
+      }
+    }
+    const app = createApp({
+      render() {
+        return (
+          openBlock(),
+          createBlock(Wrapper, null, {
+            default: withCtx(() => {
+              return [createVNode({}) /* component */]
+            }),
+            _: 1
+          })
+        )
+      }
+    })
+
+    app.mount(root)
+    expect(inner(root)).toBe('<section><div class="foo"></div></section>')
+    /**
+     * Block Tree:
+     *  - block(div)
+     *   - block(Fragment): renderSlots()
+     *    - dynamicVNode
+     */
+    expect(block!.dynamicChildren!.length).toBe(1)
+    expect(block!.dynamicChildren![0].dynamicChildren!.length).toBe(1)
+    expect(block!.dynamicChildren![0].dynamicChildren![0]).toEqual(
+      dynamicVNode!
+    )
+  })
+
+  // 3569
+  test('should force bailout when the user manually calls the slot function', async () => {
+    const index = ref(0)
+    const Foo = {
+      setup(props: any, { slots }: SetupContext) {
+        return () => {
+          return slots.default!()[index.value]
+        }
+      }
+    }
+
+    const app = createApp({
+      setup() {
+        return () => {
+          return (
+            openBlock(),
+            createBlock(Foo, null, {
+              default: withCtx(() => [
+                true
+                  ? (openBlock(), createBlock('p', { key: 0 }, '1'))
+                  : createCommentVNode('v-if', true),
+                true
+                  ? (openBlock(), createBlock('p', { key: 0 }, '2'))
+                  : createCommentVNode('v-if', true)
+              ]),
+              _: 1 /* STABLE */
+            })
+          )
+        }
+      }
+    })
+
+    app.mount(root)
+    expect(inner(root)).toBe('<p>1</p>')
+
+    index.value = 1
+    await nextTick()
+    expect(inner(root)).toBe('<p>2</p>')
+
+    index.value = 0
+    await nextTick()
+    expect(inner(root)).toBe('<p>1</p>')
+  })
 })
index 5ed4e19f0ecd36499b452ca6998e1a9eb9973765..966a9a1a655eb54605f3e86ebf4b3519d4bd02ce 100644 (file)
@@ -37,6 +37,7 @@ import {
 import { resolveFilter } from '../helpers/resolveAssets'
 import { resolveMergedOptions } from '../componentOptions'
 import { InternalSlots, Slots } from '../componentSlots'
+import { ContextualRenderFn } from '../componentRenderContext'
 
 export type LegacyPublicInstance = ComponentPublicInstance &
   LegacyPublicProperties
@@ -106,7 +107,7 @@ export function installCompatInstanceProperties(map: PublicPropertiesMap) {
       const res: InternalSlots = {}
       for (const key in i.slots) {
         const fn = i.slots[key]!
-        if (!(fn as any)._nonScoped) {
+        if (!(fn as ContextualRenderFn)._ns /* non-scoped slot */) {
           res[key] = fn
         }
       }
index c65cc8d79df039b663a49ee1caa5a1efb6f28a6b..480bf029a0fcd9666d7072c281a43700c8cd3cc9 100644 (file)
@@ -281,7 +281,7 @@ function convertLegacySlots(vnode: VNode): VNode {
       for (const key in slots) {
         const slotChildren = slots[key]
         slots[key] = () => slotChildren
-        slots[key]._nonScoped = true
+        slots[key]._ns = true /* non-scoped slot */
       }
     }
   }
index a25756864e9c7dd252bb7543ee62061623200da2..8f49ec251d5963eada2b46dacf135475df4e82ea 100644 (file)
@@ -1,7 +1,6 @@
 import { ComponentInternalInstance } from './component'
 import { devtoolsComponentUpdated } from './devtools'
-import { isRenderingCompiledSlot } from './helpers/renderSlot'
-import { closeBlock, openBlock } from './vnode'
+import { setBlockTracking } from './vnode'
 
 /**
  * mark the current rendering instance for asset resolution (e.g.
@@ -56,6 +55,14 @@ export function popScopeId() {
  */
 export const withScopeId = (_id: string) => withCtx
 
+export type ContextualRenderFn = {
+  (...args: any[]): any
+  _n: boolean /* already normalized */
+  _c: boolean /* compiled */
+  _d: boolean /* disableTracking */
+  _ns: boolean /* nonScoped */
+}
+
 /**
  * Wrap a slot function to memoize current rendering instance
  * @private compiler helper
@@ -66,18 +73,26 @@ export function withCtx(
   isNonScopedSlot?: boolean // __COMPAT__ only
 ) {
   if (!ctx) return fn
-  const renderFnWithContext = (...args: any[]) => {
+
+  // already normalized
+  if ((fn as ContextualRenderFn)._n) {
+    return fn
+  }
+
+  const renderFnWithContext: ContextualRenderFn = (...args: any[]) => {
     // If a user calls a compiled slot inside a template expression (#1745), it
-    // can mess up block tracking, so by default we need to push a null block to
-    // avoid that. This isn't necessary if rendering a compiled `<slot>`.
-    if (!isRenderingCompiledSlot) {
-      openBlock(true /* null block that disables tracking */)
+    // can mess up block tracking, so by default we disable block tracking and
+    // force bail out when invoking a compiled slot (indicated by the ._d flag).
+    // This isn't necessary if rendering a compiled `<slot>`, so we flip the
+    // ._d flag off when invoking the wrapped fn inside `renderSlot`.
+    if (renderFnWithContext._d) {
+      setBlockTracking(-1)
     }
     const prevInstance = setCurrentRenderingInstance(ctx)
     const res = fn(...args)
     setCurrentRenderingInstance(prevInstance)
-    if (!isRenderingCompiledSlot) {
-      closeBlock()
+    if (renderFnWithContext._d) {
+      setBlockTracking(1)
     }
 
     if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
@@ -86,13 +101,18 @@ export function withCtx(
 
     return res
   }
-  // mark this as a compiled slot function.
+
+  // mark normalized to avoid duplicated wrapping
+  renderFnWithContext._n = true
+  // mark this as compiled by default
   // this is used in vnode.ts -> normalizeChildren() to set the slot
   // rendering flag.
-  // also used to cache the normalized results to avoid repeated normalization
-  renderFnWithContext._c = renderFnWithContext
+  renderFnWithContext._c = true
+  // disable block tracking by default
+  renderFnWithContext._d = true
+  // compat build only flag to distinguish scoped slots from non-scoped ones
   if (__COMPAT__ && isNonScopedSlot) {
-    renderFnWithContext._nonScoped = true
+    renderFnWithContext._ns = true
   }
   return renderFnWithContext
 }
index 7e0fe3cb61b77381d80a4a9a90538a96f5901898..8c19cec3fa340706ddb53ef040752e8fc044f153 100644 (file)
@@ -17,7 +17,7 @@ import {
 } from '@vue/shared'
 import { warn } from './warning'
 import { isKeepAlive } from './components/KeepAlive'
-import { withCtx } from './componentRenderContext'
+import { ContextualRenderFn, withCtx } from './componentRenderContext'
 import { isHmrUpdating } from './hmr'
 import { DeprecationTypes, isCompatEnabled } from './compat/compatConfig'
 import { toRaw } from '@vue/reactivity'
@@ -62,9 +62,8 @@ const normalizeSlot = (
   key: string,
   rawSlot: Function,
   ctx: ComponentInternalInstance | null | undefined
-): Slot =>
-  (rawSlot as any)._c ||
-  (withCtx((props: any) => {
+): Slot => {
+  const normalized = withCtx((props: any) => {
     if (__DEV__ && currentInstance) {
       warn(
         `Slot "${key}" invoked outside of the render function: ` +
@@ -73,7 +72,11 @@ const normalizeSlot = (
       )
     }
     return normalizeSlotValue(rawSlot(props))
-  }, ctx) as Slot)
+  }, ctx) as Slot
+  // NOT a compiled slot
+  ;(normalized as ContextualRenderFn)._c = false
+  return normalized
+}
 
 const normalizeObjectSlots = (
   rawSlots: RawSlots,
index 26e7b82500878c9b9e536eb9b362db8c4a04e6aa..181d49a547cfe840e592caa6dcf914d72d6282d0 100644 (file)
@@ -1,5 +1,6 @@
 import { Data } from '../component'
 import { Slots, RawSlots } from '../componentSlots'
+import { ContextualRenderFn } from '../componentRenderContext'
 import { Comment, isVNode } from '../vnode'
 import {
   VNodeArrayChildren,
@@ -11,10 +12,6 @@ import {
 import { PatchFlags, SlotFlags } from '@vue/shared'
 import { warn } from '../warning'
 
-export let isRenderingCompiledSlot = 0
-export const setCompiledSlotRendering = (n: number) =>
-  (isRenderingCompiledSlot += n)
-
 /**
  * Compiler runtime helper for rendering `<slot/>`
  * @private
@@ -43,7 +40,9 @@ export function renderSlot(
   // invocation interfering with template-based block tracking, but in
   // `renderSlot` we can be sure that it's template-based so we can force
   // enable it.
-  isRenderingCompiledSlot++
+  if (slot && (slot as ContextualRenderFn)._c) {
+    ;(slot as ContextualRenderFn)._d = false
+  }
   openBlock()
   const validSlotContent = slot && ensureValidVNode(slot(props))
   const rendered = createBlock(
@@ -57,7 +56,9 @@ export function renderSlot(
   if (!noSlotted && rendered.scopeId) {
     rendered.slotScopeIds = [rendered.scopeId + '-s']
   }
-  isRenderingCompiledSlot--
+  if (slot && (slot as ContextualRenderFn)._c) {
+    ;(slot as ContextualRenderFn)._d = true
+  }
   return rendered
 }
 
index 930db771823ba4be567124199d492ccf309efd46..7e5976e2f7beac85cab34d393c9e2f1fa0219802 100644 (file)
@@ -40,7 +40,6 @@ import {
 import { RendererNode, RendererElement } from './renderer'
 import { NULL_DYNAMIC_COMPONENT } from './helpers/resolveAssets'
 import { hmrDirtyComponents } from './hmr'
-import { setCompiledSlotRendering } from './helpers/renderSlot'
 import { convertLegacyComponent } from './compat/component'
 import { convertLegacyVModelProps } from './compat/componentVModel'
 import { defineLegacyVNodeProperties } from './compat/renderFn'
@@ -218,7 +217,7 @@ export function closeBlock() {
 // Only tracks when this value is > 0
 // We are not using a simple boolean because this value may need to be
 // incremented/decremented by nested usage of v-once (see below)
-let shouldTrack = 1
+let isBlockTreeEnabled = 1
 
 /**
  * Block tracking sometimes needs to be disabled, for example during the
@@ -237,7 +236,7 @@ let shouldTrack = 1
  * @private
  */
 export function setBlockTracking(value: number) {
-  shouldTrack += value
+  isBlockTreeEnabled += value
 }
 
 /**
@@ -263,12 +262,13 @@ export function createBlock(
     true /* isBlock: prevent a block from tracking itself */
   )
   // save current block children on the block vnode
-  vnode.dynamicChildren = currentBlock || (EMPTY_ARR as any)
+  vnode.dynamicChildren =
+    isBlockTreeEnabled > 0 ? currentBlock || (EMPTY_ARR as any) : null
   // close block
   closeBlock()
   // a block is always going to be patched, so track it as a child of its
   // parent block
-  if (shouldTrack > 0 && currentBlock) {
+  if (isBlockTreeEnabled > 0 && currentBlock) {
     currentBlock.push(vnode)
   }
   return vnode
@@ -458,7 +458,7 @@ function _createVNode(
   }
 
   if (
-    shouldTrack > 0 &&
+    isBlockTreeEnabled > 0 &&
     // avoid a block node from tracking itself
     !isBlockNode &&
     // has current parent block
@@ -635,9 +635,9 @@ export function normalizeChildren(vnode: VNode, children: unknown) {
       const slot = (children as any).default
       if (slot) {
         // _c marker is added by withCtx() indicating this is a compiled slot
-        slot._c && setCompiledSlotRendering(1)
+        slot._c && (slot._d = false)
         normalizeChildren(vnode, slot())
-        slot._c && setCompiledSlotRendering(-1)
+        slot._c && (slot._d = true)
       }
       return
     } else {