]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat: keep-alive
authorEvan You <yyx990803@gmail.com>
Wed, 26 Sep 2018 21:10:34 +0000 (17:10 -0400)
committerEvan You <yyx990803@gmail.com>
Wed, 26 Sep 2018 21:10:34 +0000 (17:10 -0400)
packages/core/src/component.ts
packages/core/src/componentUtils.ts
packages/core/src/createRenderer.ts
packages/core/src/flags.ts
packages/core/src/index.ts
packages/core/src/optional/keepAlive.ts
packages/core/src/vdom.ts

index e1667b10e667211de7a861e36ebba4ea2aaadbda..2995956f01daa5387891876276eba46bf216b60a 100644 (file)
@@ -89,7 +89,7 @@ class InternalComponent {
   public _computedGetters: Record<string, ComputedGetter> | null = null
   public _watchHandles: Set<Autorun> | null = null
   public _mounted: boolean = false
-  public _destroyed: boolean = false
+  public _unmounted: boolean = false
   public _events: { [event: string]: Function[] | null } | null = null
   public _updateHandle: Autorun | null = null
   public _queueJob: ((fn: () => void) => void) | null = null
index e8535a28cf72a78f39c14be6ba86d016ef2ae650..1630e8ae93746250fa8692f0eede3cc88f95bc64 100644 (file)
@@ -81,8 +81,11 @@ export function renderInstanceRoot(instance: MountedComponent) {
 }
 
 export function teardownComponentInstance(instance: MountedComponent) {
+  if (instance._unmounted) {
+    return
+  }
   const parentComponent = instance.$parent && instance.$parent._self
-  if (parentComponent && !parentComponent._destroyed) {
+  if (parentComponent && !parentComponent._unmounted) {
     parentComponent.$children.splice(
       parentComponent.$children.indexOf(instance.$proxy),
       1
@@ -114,24 +117,28 @@ export function normalizeComponentRoot(
       vnode = createFragment(vnode)
     }
   } else {
-    const { flags } = vnode
+    const { el, flags } = vnode
     if (
       componentVNode &&
       (flags & VNodeFlags.COMPONENT || flags & VNodeFlags.ELEMENT)
     ) {
+      const isKeepAlive = (flags & VNodeFlags.COMPONENT_STATEFUL_KEPT_ALIVE) > 0
       if (
         inheritAttrs !== false &&
         attrs !== void 0 &&
         Object.keys(attrs).length > 0
       ) {
         vnode = cloneVNode(vnode, attrs)
-      } else if (vnode.el) {
+        if (isKeepAlive) {
+          vnode.el = el
+        }
+      } else if (el && !isKeepAlive) {
         vnode = cloneVNode(vnode)
       }
       if (flags & VNodeFlags.COMPONENT) {
         vnode.parentVNode = componentVNode
       }
-    } else if (vnode.el) {
+    } else if (el) {
       vnode = cloneVNode(vnode)
     }
   }
index 6e8a09a8c84892fad0b26df9cefa8a6768556096..a33f5f441d71dbbac955a6b5801a083d657e7472 100644 (file)
@@ -26,6 +26,7 @@ import {
   normalizeComponentRoot,
   shouldUpdateFunctionalComponent
 } from './componentUtils'
+import { KeepAliveSymbol } from './optional/keepAlive'
 
 interface NodeOps {
   createElement: (tag: string, isSVG?: boolean) => any
@@ -271,15 +272,22 @@ export function createRenderer(options: RendererOptions) {
     let el: RenderNode | RenderFragment
     const { flags, tag, data, slots } = vnode
     if (flags & VNodeFlags.COMPONENT_STATEFUL) {
-      el = mountComponentInstance(
-        vnode,
-        tag as ComponentClass,
-        null,
-        parentComponent,
-        isSVG,
-        endNode
-      )
+      if (flags & VNodeFlags.COMPONENT_STATEFUL_KEPT_ALIVE) {
+        // kept-alive
+        el = vnode.el as RenderNode
+        // TODO activated hook
+      } else {
+        el = mountComponentInstance(
+          vnode,
+          tag as ComponentClass,
+          null,
+          parentComponent,
+          isSVG,
+          endNode
+        )
+      }
     } else {
+      debugger
       // functional component
       const render = tag as FunctionalComponent
       const { props, attrs } = resolveProps(data, render.props, render)
@@ -1098,7 +1106,9 @@ export function createRenderer(options: RendererOptions) {
       }
     } else if (flags & VNodeFlags.COMPONENT) {
       if (flags & VNodeFlags.COMPONENT_STATEFUL) {
-        unmountComponentInstance(children as MountedComponent)
+        if ((flags & VNodeFlags.COMPONENT_STATEFUL_SHOULD_KEEP_ALIVE) === 0) {
+          unmountComponentInstance(children as MountedComponent)
+        }
       } else {
         unmount(children as VNode)
       }
@@ -1161,12 +1171,17 @@ export function createRenderer(options: RendererOptions) {
     isSVG: boolean,
     endNode: RenderNode | RenderFragment | null
   ): RenderNode {
-    // a vnode may already have an instance if this is a compat call
-    // with new Vue()
+    // a vnode may already have an instance if this is a compat call with
+    // new Vue()
     const instance =
       (__COMPAT__ && (parentVNode.children as MountedComponent)) ||
       createComponentInstance(parentVNode, Component, parentComponent)
 
+    // inject platform-specific unmount to keep-alive container
+    if ((Component as any)[KeepAliveSymbol] === true) {
+      ;(instance as any).$unmount = unmountComponentInstance
+    }
+
     if (instance.beforeMount) {
       instance.beforeMount.call(instance.$proxy)
     }
@@ -1177,7 +1192,7 @@ export function createRenderer(options: RendererOptions) {
 
     instance._updateHandle = autorun(
       () => {
-        if (instance._destroyed) {
+        if (instance._unmounted) {
           return
         }
         if (instance._mounted) {
@@ -1271,6 +1286,9 @@ export function createRenderer(options: RendererOptions) {
   }
 
   function unmountComponentInstance(instance: MountedComponent) {
+    if (instance._unmounted) {
+      return
+    }
     if (instance.beforeUnmount) {
       instance.beforeUnmount.call(instance.$proxy)
     }
@@ -1279,7 +1297,7 @@ export function createRenderer(options: RendererOptions) {
     }
     stop(instance._updateHandle)
     teardownComponentInstance(instance)
-    instance._destroyed = true
+    instance._unmounted = true
     if (instance.unmounted) {
       instance.unmounted.call(instance.$proxy)
     }
index c248c4e5a7c842ea48ea5734ea4dcabcd30bf8bc..3322e09fb9f89b0b405244795ad0130e1602816c 100644 (file)
@@ -5,17 +5,18 @@ export const enum VNodeFlags {
   ELEMENT = ELEMENT_HTML | ELEMENT_SVG,
 
   COMPONENT_UNKNOWN = 1 << 2,
-  COMPONENT_STATEFUL = 1 << 3,
-  COMPONENT_FUNCTIONAL = 1 << 4,
-  COMPONENT_ASYNC = 1 << 5,
-  COMPONENT = COMPONENT_UNKNOWN |
-    COMPONENT_STATEFUL |
-    COMPONENT_FUNCTIONAL |
-    COMPONENT_ASYNC,
+  COMPONENT_STATEFUL_NORMAL = 1 << 3,
+  COMPONENT_STATEFUL_SHOULD_KEEP_ALIVE = 1 << 4,
+  COMPONENT_STATEFUL_KEPT_ALIVE = 1 << 5,
+  COMPONENT_STATEFUL = COMPONENT_STATEFUL_NORMAL |
+    COMPONENT_STATEFUL_SHOULD_KEEP_ALIVE |
+    COMPONENT_STATEFUL_KEPT_ALIVE,
+  COMPONENT_FUNCTIONAL = 1 << 6,
+  COMPONENT = COMPONENT_UNKNOWN | COMPONENT_STATEFUL | COMPONENT_FUNCTIONAL,
 
-  TEXT = 1 << 6,
-  FRAGMENT = 1 << 7,
-  PORTAL = 1 << 8
+  TEXT = 1 << 7,
+  FRAGMENT = 1 << 8,
+  PORTAL = 1 << 9
 }
 
 export const enum ChildrenFlags {
index fdafe97c37950b7beac8cb70158463ef3971218f..aeb14ca5a1d13a4e4eefb7d4340fa17a0aaf69b8 100644 (file)
@@ -18,6 +18,7 @@ export { createComponentInstance } from './componentUtils'
 export * from './optional/directive'
 export * from './optional/context'
 export * from './optional/asyncComponent'
+export * from './optional/keepAlive'
 
 // flags & types
 export { ComponentType, ComponentClass, FunctionalComponent } from './component'
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..cb045cfcb33e96ae6b33dd00b7543a9fa0a8355a 100644 (file)
@@ -0,0 +1,130 @@
+import { Component, ComponentClass, MountedComponent } from '../component'
+import { VNode, Slots } from '../vdom'
+import { VNodeFlags } from '../flags'
+
+type MatchPattern = string | RegExp | string[] | RegExp[]
+
+interface KeepAliveProps {
+  include?: MatchPattern
+  exclude?: MatchPattern
+  max?: number | string
+}
+
+type CacheKey = string | number | ComponentClass
+type Cache = Map<CacheKey, VNode>
+
+export const KeepAliveSymbol = Symbol()
+
+export class KeepAlive extends Component<{}, KeepAliveProps> {
+  cache: Cache
+  keys: Set<CacheKey>
+
+  // to be set in createRenderer when instance is created
+  $unmount: (instance: MountedComponent) => void
+
+  created() {
+    this.cache = new Map()
+    // keys represents the "freshness" of cached components
+    // oldest cached ones will be pruned first when cache count exceeds max
+    this.keys = new Set()
+  }
+
+  unmounted() {
+    this.cache.forEach(vnode => {
+      // change flag so it can be properly unmounted
+      vnode.flags = VNodeFlags.COMPONENT_STATEFUL_NORMAL
+      this.$unmount(vnode.children as MountedComponent)
+    })
+  }
+
+  pruneCache(filter?: (name: string) => boolean) {
+    this.cache.forEach((vnode, key) => {
+      const name = getName(vnode.tag as ComponentClass)
+      if (name && (!filter || !filter(name))) {
+        this.pruneCacheEntry(key)
+      }
+    })
+  }
+
+  pruneCacheEntry(key: CacheKey) {
+    const cached = this.cache.get(key) as VNode
+    const current = this.$vnode
+    if (!current || cached.tag !== current.tag) {
+      this.$unmount(cached.children as MountedComponent)
+    }
+    this.cache.delete(key)
+    this.keys.delete(key)
+  }
+
+  render(props: any, slots: Slots) {
+    if (!slots.default) {
+      return
+    }
+    const children = slots.default()
+    let vnode = children[0]
+    if (children.length > 1) {
+      if (__DEV__) {
+        console.warn(`KeepAlive can only have a single child.`)
+      }
+      return children
+    } else if ((vnode.flags & VNodeFlags.COMPONENT_STATEFUL) === 0) {
+      if (__DEV__) {
+        console.warn(`KeepAlive child must be a stateful component.`)
+      }
+      return children
+    }
+
+    const comp = vnode.tag as ComponentClass
+    const name = getName(comp)
+    const { include, exclude, max } = props
+
+    if (
+      (include && (!name || !matches(include, name))) ||
+      (exclude && name && matches(exclude, name))
+    ) {
+      return vnode
+    }
+
+    const { cache, keys } = this
+    const key = vnode.key == null ? comp : vnode.key
+    const cached = cache.get(key)
+    if (cached) {
+      vnode.children = cached.children
+      vnode.el = cached.el
+      vnode.flags |= VNodeFlags.COMPONENT_STATEFUL_KEPT_ALIVE
+      // make this key the freshest
+      keys.delete(key)
+      keys.add(key)
+    } else {
+      cache.set(key, vnode)
+      keys.add(key)
+      // prune oldest entry
+      if (max && keys.size > parseInt(max, 10)) {
+        this.pruneCacheEntry(Array.from(this.keys)[0])
+      }
+    }
+    vnode.flags |= VNodeFlags.COMPONENT_STATEFUL_SHOULD_KEEP_ALIVE
+    return vnode
+  }
+}
+
+// mark constructor
+// we use a symbol instead of comparing to the constructor itself
+// so that the implementation can be tree-shaken
+;(KeepAlive as any)[KeepAliveSymbol] = true
+
+function getName(comp: ComponentClass): string | void {
+  return comp.options && comp.options.name
+}
+
+function matches(pattern: MatchPattern, name: string): boolean {
+  if (Array.isArray(pattern)) {
+    return (pattern as any).some((p: string | RegExp) => matches(p, name))
+  } else if (typeof pattern === 'string') {
+    return pattern.split(',').indexOf(name) > -1
+  } else if (pattern.test) {
+    return pattern.test(name)
+  }
+  /* istanbul ignore next */
+  return false
+}
index 578cff95de546c6c54897be08f102fef149989b9..c586dc4e3dc554dc63d8bd36949bc52103a8d209 100644 (file)
@@ -146,7 +146,7 @@ export function createComponentVNode(
       comp = render
     } else {
       // object literal stateful
-      flags = VNodeFlags.COMPONENT_STATEFUL
+      flags = VNodeFlags.COMPONENT_STATEFUL_NORMAL
       comp =
         comp._normalized ||
         (comp._normalized = createComponentClassFromOptions(comp))
@@ -157,7 +157,7 @@ export function createComponentVNode(
       // TODO warn invalid comp value in dev
     }
     if (comp.prototype && comp.prototype.render) {
-      flags = VNodeFlags.COMPONENT_STATEFUL
+      flags = VNodeFlags.COMPONENT_STATEFUL_NORMAL
     } else {
       flags = VNodeFlags.COMPONENT_FUNCTIONAL
     }