]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
test: tests for keep-alive
authorEvan You <yyx990803@gmail.com>
Thu, 31 Oct 2019 01:41:28 +0000 (21:41 -0400)
committerEvan You <yyx990803@gmail.com>
Thu, 31 Oct 2019 01:41:28 +0000 (21:41 -0400)
packages/runtime-core/__tests__/keepAlive.spec.ts [new file with mode: 0644]
packages/runtime-core/src/apiLifecycle.ts
packages/runtime-core/src/createRenderer.ts
packages/runtime-core/src/keepAlive.ts
packages/runtime-core/src/scheduler.ts
packages/runtime-core/src/suspense.ts

diff --git a/packages/runtime-core/__tests__/keepAlive.spec.ts b/packages/runtime-core/__tests__/keepAlive.spec.ts
new file mode 100644 (file)
index 0000000..54d8ce5
--- /dev/null
@@ -0,0 +1,233 @@
+import { ComponentOptions } from '../src/component'
+import {
+  h,
+  TestElement,
+  nodeOps,
+  render,
+  ref,
+  KeepAlive,
+  serializeInner,
+  nextTick
+} from '@vue/runtime-test'
+
+describe('keep-alive', () => {
+  let one: ComponentOptions
+  let two: ComponentOptions
+  let root: TestElement
+
+  beforeEach(() => {
+    root = nodeOps.createElement('div')
+    one = {
+      data: () => ({ msg: 'one' }),
+      render() {
+        return h('div', this.msg)
+      },
+      created: jest.fn(),
+      mounted: jest.fn(),
+      activated: jest.fn(),
+      deactivated: jest.fn(),
+      unmounted: jest.fn()
+    }
+    two = {
+      data: () => ({ msg: 'two' }),
+      render() {
+        return h('div', this.msg)
+      },
+      created: jest.fn(),
+      mounted: jest.fn(),
+      activated: jest.fn(),
+      deactivated: jest.fn(),
+      unmounted: jest.fn()
+    }
+  })
+
+  function assertHookCalls(component: any, callCounts: number[]) {
+    expect([
+      component.created.mock.calls.length,
+      component.mounted.mock.calls.length,
+      component.activated.mock.calls.length,
+      component.deactivated.mock.calls.length,
+      component.unmounted.mock.calls.length
+    ]).toEqual(callCounts)
+  }
+
+  test('should preserve state', async () => {
+    const toggle = ref(true)
+    const instanceRef = ref<any>(null)
+    const App = {
+      render() {
+        return h(KeepAlive, null, {
+          default: () => h(toggle.value ? one : two, { ref: instanceRef })
+        })
+      }
+    }
+    render(h(App), root)
+    expect(serializeInner(root)).toBe(`<div>one</div>`)
+    instanceRef.value.msg = 'changed'
+    await nextTick()
+    expect(serializeInner(root)).toBe(`<div>changed</div>`)
+    toggle.value = false
+    await nextTick()
+    expect(serializeInner(root)).toBe(`<div>two</div>`)
+    toggle.value = true
+    await nextTick()
+    expect(serializeInner(root)).toBe(`<div>changed</div>`)
+  })
+
+  test('should call correct lifecycle hooks', async () => {
+    const toggle1 = ref(true)
+    const toggle2 = ref(true)
+    const App = {
+      render() {
+        return toggle1.value
+          ? h(KeepAlive, () => h(toggle2.value ? one : two))
+          : null
+      }
+    }
+    render(h(App), root)
+
+    expect(serializeInner(root)).toBe(`<div>one</div>`)
+    assertHookCalls(one, [1, 1, 1, 0, 0])
+    assertHookCalls(two, [0, 0, 0, 0, 0])
+
+    // toggle kept-alive component
+    toggle2.value = false
+    await nextTick()
+    expect(serializeInner(root)).toBe(`<div>two</div>`)
+    assertHookCalls(one, [1, 1, 1, 1, 0])
+    assertHookCalls(two, [1, 1, 1, 0, 0])
+
+    toggle2.value = true
+    await nextTick()
+    expect(serializeInner(root)).toBe(`<div>one</div>`)
+    assertHookCalls(one, [1, 1, 2, 1, 0])
+    assertHookCalls(two, [1, 1, 1, 1, 0])
+
+    toggle2.value = false
+    await nextTick()
+    expect(serializeInner(root)).toBe(`<div>two</div>`)
+    assertHookCalls(one, [1, 1, 2, 2, 0])
+    assertHookCalls(two, [1, 1, 2, 1, 0])
+
+    // teardown keep-alive, should unmount all components including cached
+    toggle1.value = false
+    await nextTick()
+    expect(serializeInner(root)).toBe(`<!---->`)
+    assertHookCalls(one, [1, 1, 2, 2, 1])
+    assertHookCalls(two, [1, 1, 2, 2, 1])
+  })
+
+  test('should call lifecycle hooks on nested components', async () => {
+    one.render = () => h(two)
+
+    const toggle = ref(true)
+    const App = {
+      render() {
+        return h(KeepAlive, () => (toggle.value ? h(one) : null))
+      }
+    }
+    render(h(App), root)
+
+    expect(serializeInner(root)).toBe(`<div>two</div>`)
+    assertHookCalls(one, [1, 1, 1, 0, 0])
+    assertHookCalls(two, [1, 1, 1, 0, 0])
+
+    toggle.value = false
+    await nextTick()
+    expect(serializeInner(root)).toBe(`<!---->`)
+    assertHookCalls(one, [1, 1, 1, 1, 0])
+    assertHookCalls(two, [1, 1, 1, 1, 0])
+
+    toggle.value = true
+    await nextTick()
+    expect(serializeInner(root)).toBe(`<div>two</div>`)
+    assertHookCalls(one, [1, 1, 2, 1, 0])
+    assertHookCalls(two, [1, 1, 2, 1, 0])
+
+    toggle.value = false
+    await nextTick()
+    expect(serializeInner(root)).toBe(`<!---->`)
+    assertHookCalls(one, [1, 1, 2, 2, 0])
+    assertHookCalls(two, [1, 1, 2, 2, 0])
+  })
+
+  test('should call correct hooks for nested keep-alive', async () => {
+    const toggle2 = ref(true)
+    one.render = () => h(KeepAlive, () => (toggle2.value ? h(two) : null))
+
+    const toggle1 = ref(true)
+    const App = {
+      render() {
+        return h(KeepAlive, () => (toggle1.value ? h(one) : null))
+      }
+    }
+    render(h(App), root)
+
+    expect(serializeInner(root)).toBe(`<div>two</div>`)
+    assertHookCalls(one, [1, 1, 1, 0, 0])
+    assertHookCalls(two, [1, 1, 1, 0, 0])
+
+    toggle1.value = false
+    await nextTick()
+    expect(serializeInner(root)).toBe(`<!---->`)
+    assertHookCalls(one, [1, 1, 1, 1, 0])
+    assertHookCalls(two, [1, 1, 1, 1, 0])
+
+    toggle1.value = true
+    await nextTick()
+    expect(serializeInner(root)).toBe(`<div>two</div>`)
+    assertHookCalls(one, [1, 1, 2, 1, 0])
+    assertHookCalls(two, [1, 1, 2, 1, 0])
+
+    // toggle nested instance
+    toggle2.value = false
+    await nextTick()
+    expect(serializeInner(root)).toBe(`<!---->`)
+    assertHookCalls(one, [1, 1, 2, 1, 0])
+    assertHookCalls(two, [1, 1, 2, 2, 0])
+
+    toggle2.value = true
+    await nextTick()
+    expect(serializeInner(root)).toBe(`<div>two</div>`)
+    assertHookCalls(one, [1, 1, 2, 1, 0])
+    assertHookCalls(two, [1, 1, 3, 2, 0])
+
+    toggle1.value = false
+    await nextTick()
+    expect(serializeInner(root)).toBe(`<!---->`)
+    assertHookCalls(one, [1, 1, 2, 2, 0])
+    assertHookCalls(two, [1, 1, 3, 3, 0])
+
+    // toggle nested instance when parent is deactivated
+    toggle2.value = false
+    await nextTick()
+    expect(serializeInner(root)).toBe(`<!---->`)
+    assertHookCalls(one, [1, 1, 2, 2, 0])
+    assertHookCalls(two, [1, 1, 3, 3, 0]) // should not be affected
+
+    toggle2.value = true
+    await nextTick()
+    expect(serializeInner(root)).toBe(`<!---->`)
+    assertHookCalls(one, [1, 1, 2, 2, 0])
+    assertHookCalls(two, [1, 1, 3, 3, 0]) // should not be affected
+
+    toggle1.value = true
+    await nextTick()
+    expect(serializeInner(root)).toBe(`<div>two</div>`)
+    assertHookCalls(one, [1, 1, 3, 2, 0])
+    assertHookCalls(two, [1, 1, 4, 3, 0])
+
+    toggle1.value = false
+    toggle2.value = false
+    await nextTick()
+    expect(serializeInner(root)).toBe(`<!---->`)
+    assertHookCalls(one, [1, 1, 3, 3, 0])
+    assertHookCalls(two, [1, 1, 4, 4, 0])
+
+    toggle1.value = true
+    await nextTick()
+    expect(serializeInner(root)).toBe(`<!---->`)
+    assertHookCalls(one, [1, 1, 4, 3, 0])
+    assertHookCalls(two, [1, 1, 4, 4, 0]) // should remain inactive
+  })
+})
index 50f796f5d6097fba354d748c2a64988b6392e7f6..18de3e2cc5af81eef18033a1f6367c6e3ccff241 100644 (file)
@@ -9,32 +9,38 @@ import { callWithAsyncErrorHandling, ErrorTypeStrings } from './errorHandling'
 import { warn } from './warning'
 import { capitalize } from '@vue/shared'
 import { pauseTracking, resumeTracking, DebuggerEvent } from '@vue/reactivity'
-import { registerKeepAliveHook } from './keepAlive'
+
+export { onActivated, onDeactivated } from './keepAlive'
 
 export function injectHook(
   type: LifecycleHooks,
-  hook: Function,
+  hook: Function & { __weh?: Function },
   target: ComponentInternalInstance | null = currentInstance,
   prepend: boolean = false
 ) {
   if (target) {
     const hooks = target[type] || (target[type] = [])
-    const wrappedHook = (...args: unknown[]) => {
-      if (target.isUnmounted) {
-        return
-      }
-      // disable tracking inside all lifecycle hooks
-      // since they can potentially be called inside effects.
-      pauseTracking()
-      // Set currentInstance during hook invocation.
-      // This assumes the hook does not synchronously trigger other hooks, which
-      // can only be false when the user does something really funky.
-      setCurrentInstance(target)
-      const res = callWithAsyncErrorHandling(hook, target, type, args)
-      setCurrentInstance(null)
-      resumeTracking()
-      return res
-    }
+    // cache the error handling wrapper for injected hooks so the same hook
+    // can be properly deduped by the scheduler. "__weh" stands for "with error
+    // handling".
+    const wrappedHook =
+      hook.__weh ||
+      (hook.__weh = (...args: unknown[]) => {
+        if (target.isUnmounted) {
+          return
+        }
+        // disable tracking inside all lifecycle hooks
+        // since they can potentially be called inside effects.
+        pauseTracking()
+        // Set currentInstance during hook invocation.
+        // This assumes the hook does not synchronously trigger other hooks, which
+        // can only be false when the user does something really funky.
+        setCurrentInstance(target)
+        const res = callWithAsyncErrorHandling(hook, target, type, args)
+        setCurrentInstance(null)
+        resumeTracking()
+        return res
+      })
     if (prepend) {
       hooks.unshift(wrappedHook)
     } else {
@@ -84,17 +90,3 @@ export type ErrorCapturedHook = (
 export const onErrorCaptured = createHook<ErrorCapturedHook>(
   LifecycleHooks.ERROR_CAPTURED
 )
-
-export function onActivated(
-  hook: Function,
-  target?: ComponentInternalInstance | null
-) {
-  registerKeepAliveHook(hook, LifecycleHooks.ACTIVATED, target)
-}
-
-export function onDeactivated(
-  hook: Function,
-  target?: ComponentInternalInstance | null
-) {
-  registerKeepAliveHook(hook, LifecycleHooks.DEACTIVATED, target)
-}
index e49728e862cc426698cd4e2ef1c27bfd0bcf1927..25d6e3adf7d0a300fc95f0c4cd4c33811fc00983 100644 (file)
@@ -863,6 +863,7 @@ export function createRenderer<
 
     setupRenderEffect(
       instance,
+      parentComponent,
       parentSuspense,
       initialVNode,
       container,
@@ -877,6 +878,7 @@ export function createRenderer<
 
   function setupRenderEffect(
     instance: ComponentInternalInstance,
+    parentComponent: ComponentInternalInstance | null,
     parentSuspense: HostSuspenseBoundary | null,
     initialVNode: HostVNode,
     container: HostElement,
@@ -898,6 +900,10 @@ export function createRenderer<
         if (instance.m !== null) {
           queuePostRenderEffect(instance.m, parentSuspense)
         }
+        // activated hook for keep-alive roots.
+        if (instance.a !== null) {
+          queuePostRenderEffect(instance.a, parentSuspense)
+        }
         mounted = true
       } else {
         // updateComponent
@@ -1450,7 +1456,7 @@ export function createRenderer<
     parentSuspense: HostSuspenseBoundary | null,
     doRemove?: boolean
   ) {
-    const { bum, effects, update, subTree, um } = instance
+    const { bum, effects, update, subTree, um, da, isDeactivated } = instance
     // beforeUnmount hook
     if (bum !== null) {
       invokeHooks(bum)
@@ -1470,6 +1476,10 @@ export function createRenderer<
     if (um !== null) {
       queuePostRenderEffect(um, parentSuspense)
     }
+    // deactivated hook
+    if (da !== null && !isDeactivated) {
+      queuePostRenderEffect(da, parentSuspense)
+    }
     queuePostFlushCb(() => {
       instance.isUnmounted = true
     })
index 93d8fa470a9082e8a46fcba2c6a6dba5808789e7..5704a0b31f8e80ddfd93445e117ede53f9fd7e27 100644 (file)
@@ -9,7 +9,7 @@ import {
 } from './component'
 import { VNode, cloneVNode, isVNode } from './vnode'
 import { warn } from './warning'
-import { onBeforeUnmount, injectHook } from './apiLifecycle'
+import { onBeforeUnmount, injectHook, onUnmounted } from './apiLifecycle'
 import { isString, isArray } from '@vue/shared'
 import { watch } from './apiWatch'
 import { ShapeFlags } from './shapeFlags'
@@ -203,47 +203,67 @@ function matches(pattern: MatchPattern, name: string): boolean {
   return false
 }
 
-export function registerKeepAliveHook(
+export function onActivated(
   hook: Function,
+  target?: ComponentInternalInstance | null
+) {
+  registerKeepAliveHook(hook, LifecycleHooks.ACTIVATED, target)
+}
+
+export function onDeactivated(
+  hook: Function,
+  target?: ComponentInternalInstance | null
+) {
+  registerKeepAliveHook(hook, LifecycleHooks.DEACTIVATED, target)
+}
+
+function registerKeepAliveHook(
+  hook: Function & { __wdc?: Function },
   type: LifecycleHooks,
   target: ComponentInternalInstance | null = currentInstance
 ) {
-  // When registering an activated/deactivated hook, instead of registering it
-  // on the target instance, we walk up the parent chain and register it on
-  // every ancestor instance that is a keep-alive root. This avoids the need
-  // to walk the entire component tree when invoking these hooks, and more
-  // importantly, avoids the need to track child components in arrays.
+  // cache the deactivate branch check wrapper for injected hooks so the same
+  // hook can be properly deduped by the scheduler. "__wdc" stands for "with
+  // deactivation check".
+  const wrappedHook =
+    hook.__wdc ||
+    (hook.__wdc = () => {
+      // only fire the hook if the target instance is NOT in a deactivated branch.
+      let current: ComponentInternalInstance | null = target
+      while (current) {
+        if (current.isDeactivated) {
+          return
+        }
+        current = current.parent
+      }
+      hook()
+    })
+  injectHook(type, wrappedHook, target)
+  // In addition to registering it on the target instance, we walk up the parent
+  // chain and register it on all ancestor instances that are keep-alive roots.
+  // This avoids the need to walk the entire component tree when invoking these
+  // hooks, and more importantly, avoids the need to track child components in
+  // arrays.
   if (target) {
-    let current = target
-    while (current.parent) {
+    let current = target.parent
+    while (current && current.parent) {
       if (current.parent.type === KeepAlive) {
-        register(hook, type, target, current)
+        injectToKeepAliveRoot(wrappedHook, type, target, current)
       }
       current = current.parent
     }
   }
 }
 
-function register(
+function injectToKeepAliveRoot(
   hook: Function,
   type: LifecycleHooks,
   target: ComponentInternalInstance,
   keepAliveRoot: ComponentInternalInstance
 ) {
-  const wrappedHook = () => {
-    // only fire the hook if the target instance is NOT in a deactivated branch.
-    let current: ComponentInternalInstance | null = target
-    while (current) {
-      if (current.isDeactivated) {
-        return
-      }
-      current = current.parent
-    }
-    hook()
-  }
-  injectHook(type, wrappedHook, keepAliveRoot, true)
-  onBeforeUnmount(() => {
+  injectHook(type, hook, keepAliveRoot, true /* prepend */)
+  onUnmounted(() => {
     const hooks = keepAliveRoot[type]!
-    hooks.splice(hooks.indexOf(wrappedHook), 1)
+    hooks.splice(hooks.indexOf(hook), 1)
   }, target)
 }
index 9a0c40810f52203d18ac8616902f68b7f31face8..e291d412a6d56ff51362a2ff65047a6a34f0f73b 100644 (file)
@@ -6,6 +6,7 @@ const postFlushCbs: Function[] = []
 const p = Promise.resolve()
 
 let isFlushing = false
+let isFlushPending = false
 
 export function nextTick(fn?: () => void): Promise<void> {
   return fn ? p.then(fn) : p
@@ -14,9 +15,7 @@ export function nextTick(fn?: () => void): Promise<void> {
 export function queueJob(job: () => void) {
   if (!queue.includes(job)) {
     queue.push(job)
-    if (!isFlushing) {
-      nextTick(flushJobs)
-    }
+    queueFlush()
   }
 }
 
@@ -26,8 +25,12 @@ export function queuePostFlushCb(cb: Function | Function[]) {
   } else {
     postFlushCbs.push(...cb)
   }
+  queueFlush()
+}
 
-  if (!isFlushing) {
+function queueFlush() {
+  if (!isFlushing && !isFlushPending) {
+    isFlushPending = true
     nextTick(flushJobs)
   }
 }
@@ -48,6 +51,7 @@ const RECURSION_LIMIT = 100
 type JobCountMap = Map<Function, number>
 
 function flushJobs(seenJobs?: JobCountMap) {
+  isFlushPending = false
   isFlushing = true
   let job
   if (__DEV__) {
@@ -77,7 +81,7 @@ function flushJobs(seenJobs?: JobCountMap) {
   isFlushing = false
   // some postFlushCb queued jobs!
   // keep flushing until it drains.
-  if (queue.length) {
+  if (queue.length || postFlushCbs.length) {
     flushJobs(seenJobs)
   }
 }
index bda5ae075ca2eb1ac49c90b072ef86e47759f751..a8d323c0dd28783c1d862089cb0460468850508d 100644 (file)
@@ -203,6 +203,7 @@ export interface SuspenseBoundary<
     instance: ComponentInternalInstance,
     setupRenderEffect: (
       instance: ComponentInternalInstance,
+      parentComponent: ComponentInternalInstance | null,
       parentSuspense: SuspenseBoundary<HostNode, HostElement> | null,
       initialVNode: VNode<HostNode, HostElement>,
       container: HostElement,
@@ -402,6 +403,7 @@ function createSuspenseBoundary<HostNode, HostElement>(
           handleSetupResult(instance, asyncSetupResult, suspense)
           setupRenderEffect(
             instance,
+            parentComponent,
             suspense,
             vnode,
             // component may have been moved before resolve