]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(runtime-core): avoid manual slot invocation in template expressions interfering...
authorEvan You <yyx990803@gmail.com>
Thu, 6 Aug 2020 14:16:13 +0000 (10:16 -0400)
committerEvan You <yyx990803@gmail.com>
Thu, 6 Aug 2020 14:16:13 +0000 (10:16 -0400)
fix #1745

packages/runtime-core/__tests__/helpers/renderSlot.spec.ts
packages/runtime-core/src/helpers/renderSlot.ts
packages/runtime-core/src/helpers/withRenderContext.ts
packages/runtime-core/src/vnode.ts

index a2c39f5b53457f34befe23fb8dcd6098ec3223f8..e14f4b75025164bb1b5ea77114be8624c9fa37f7 100644 (file)
@@ -1,5 +1,13 @@
 import { renderSlot } from '../../src/helpers/renderSlot'
-import { h } from '../../src/h'
+import {
+  h,
+  withCtx,
+  createVNode,
+  openBlock,
+  createBlock,
+  Fragment
+} from '../../src'
+import { PatchFlags } from '@vue/shared/src'
 
 describe('renderSlot', () => {
   it('should render slot', () => {
@@ -20,4 +28,23 @@ describe('renderSlot', () => {
     renderSlot({ default: (_a, _b, _c) => [h('child')] }, 'default')
     expect('SSR-optimized slot function detected').toHaveBeenWarned()
   })
+
+  // #1745
+  it('should force enable tracking', () => {
+    const slot = withCtx(
+      () => {
+        return [createVNode('div', null, 'foo', PatchFlags.TEXT)]
+      },
+      // mock instance
+      {} as any
+    )
+
+    // manual invocation should not track
+    const manual = (openBlock(), createBlock(Fragment, null, slot()))
+    expect(manual.dynamicChildren!.length).toBe(0)
+
+    // renderSlot should track
+    const templateRendered = renderSlot({ default: slot }, 'default')
+    expect(templateRendered.dynamicChildren!.length).toBe(1)
+  })
 })
index fd19b3a95ff004197d1cf72ef50cbde45d1a5240..0e4f09a3435edc173ea43269f6c6d0ed6499ab68 100644 (file)
@@ -10,6 +10,8 @@ import {
 import { PatchFlags, SlotFlags } from '@vue/shared'
 import { warn } from '../warning'
 
+export let isRenderingTemplateSlot = false
+
 /**
  * Compiler runtime helper for rendering `<slot/>`
  * @private
@@ -33,15 +35,20 @@ export function renderSlot(
     slot = () => []
   }
 
-  return (
-    openBlock(),
-    createBlock(
-      Fragment,
-      { key: props.key },
-      slot ? slot(props) : fallback ? fallback() : [],
-      (slots as RawSlots)._ === SlotFlags.STABLE
-        ? PatchFlags.STABLE_FRAGMENT
-        : PatchFlags.BAIL
-    )
-  )
+  // a compiled slot disables block tracking by default to avoid manual
+  // 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.
+  isRenderingTemplateSlot = true
+  const rendered = (openBlock(),
+  createBlock(
+    Fragment,
+    { key: props.key },
+    slot ? slot(props) : fallback ? fallback() : [],
+    (slots as RawSlots)._ === SlotFlags.STABLE
+      ? PatchFlags.STABLE_FRAGMENT
+      : PatchFlags.BAIL
+  ))
+  isRenderingTemplateSlot = false
+  return rendered
 }
index a8f326d081e524b1b2f57005c25416d6b3c02bf1..bf1541fa11ab16e79e70e3bd1b29d4c4abdb3b01 100644 (file)
@@ -4,6 +4,7 @@ import {
   currentRenderingInstance
 } from '../componentRenderUtils'
 import { ComponentInternalInstance } from '../component'
+import { setBlockTracking } from '../vnode'
 
 /**
  * Wrap a slot function to memoize current rendering instance
@@ -15,10 +16,15 @@ export function withCtx(
 ) {
   if (!ctx) return fn
   return function renderFnWithContext() {
+    // By default, compiled slots disables block tracking since the user may
+    // call it inside a template expression (#1745). It should only track when
+    // it's called by a template `<slot>`.
+    setBlockTracking(-1)
     const owner = currentRenderingInstance
     setCurrentRenderingInstance(ctx)
     const res = fn.apply(null, arguments as any)
     setCurrentRenderingInstance(owner)
+    setBlockTracking(1)
     return res
   }
 }
index 353ed863b05dfb0fa35c6eaf61131827fa2fa433..e2bb819528ea8b80574b091502b6eae48bae06b0 100644 (file)
@@ -35,6 +35,7 @@ import { currentRenderingInstance } from './componentRenderUtils'
 import { RendererNode, RendererElement } from './renderer'
 import { NULL_DYNAMIC_COMPONENT } from './helpers/resolveAssets'
 import { hmrDirtyComponents } from './hmr'
+import { isRenderingTemplateSlot } from './helpers/renderSlot'
 
 export const Fragment = (Symbol(__DEV__ ? 'Fragment' : undefined) as any) as {
   __isFragment: true
@@ -400,18 +401,20 @@ function _createVNode(
 
   normalizeChildren(vnode, children)
 
-  // presence of a patch flag indicates this node needs patching on updates.
-  // component nodes also should always be patched, because even if the
-  // component doesn't need to update, it needs to persist the instance on to
-  // the next vnode so that it can be properly unmounted later.
   if (
-    shouldTrack > 0 &&
+    (shouldTrack > 0 || isRenderingTemplateSlot) &&
+    // avoid a block node from tracking itself
     !isBlockNode &&
+    // has current parent block
     currentBlock &&
+    // presence of a patch flag indicates this node needs patching on updates.
+    // component nodes also should always be patched, because even if the
+    // component doesn't need to update, it needs to persist the instance on to
+    // the next vnode so that it can be properly unmounted later.
+    (patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) &&
     // the EVENTS flag is only for hydration and if it is the only flag, the
     // vnode should not be considered dynamic due to handler caching.
-    patchFlag !== PatchFlags.HYDRATE_EVENTS &&
-    (patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT)
+    patchFlag !== PatchFlags.HYDRATE_EVENTS
   ) {
     currentBlock.push(vnode)
   }