vue3学习--KeepAlive原理

670 阅读5分钟
<keep-alive>
  <comp-a v-if="flag"></comp-a>
  <comp-b v-else></comp-b>
  <button @click="flag=!flag">toggle</button>
</keep-alive>

编译之后的render函数

import { resolveComponent as _resolveComponent, createVNode as _createVNode, createCommentVNode as _createCommentVNode, KeepAlive as _KeepAlive, openBlock as _openBlock, createBlock as _createBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
  const _component_comp_a = _resolveComponent("comp-a")
  const _component_comp_b = _resolveComponent("comp-b")
  return (_openBlock(), _createBlock(_KeepAlive, null, [
    (_ctx.flag)
      ? _createVNode(_component_comp_a, { key: 0 })
      : _createVNode(_component_comp_b, { key: 1 }),
    _createVNode("button", {
      onClick: $event => (_ctx.flag=!_ctx.flag)
    }, "toggle", 8 /* PROPS */, ["onClick"])
  ], 1024 /* DYNAMIC_SLOTS */))
}

可见使用了KeepAlive组件对组件做了封装,他是一个抽象组件,并不会渲染成一个真实的DOM,只会渲染内部包裹的子节点,并且让内部的子组件在切换的时候,不会走一整套递归卸载和挂载DOM的流程。

KeepAlive的实现分成四部分:组件的渲染、缓存的设计、props设计、组件的卸载。

KeepAlive 的缓存设计,KeepAlive 包裹的子组件在其渲染后,下一次 KeepAlive 组件更新前会被缓存,缓存后的子组件在下一次渲染的时候直接从缓存中拿到子树 vnode 以及对应的 DOM 元素,直接渲染即可。
props设计: include: [String, RegExp, Array],
exclude: [String, RegExp, Array],
max: [String, Number]
组价卸载:
切换按钮引起的keepalive内部组件的卸载,keepalive所在的组件卸载导致的 keepalive组件整个被卸载

const KeepAliveImpl = {
  name: `KeepAlive`,
  __isKeepAlive: true,
  inheritRef: true,
  props: {
    include: [String, RegExp, Array],
    exclude: [String, RegExp, Array],
    max: [String, Number]
  },
  setup(props, { slots }) {
    const cache = new Map()
    const keys = new Set()
    let current = null
    const instance = getCurrentInstance()
    const parentSuspense = instance.suspense
    const sharedContext = instance.ctx
    const { renderer: { p: patch, m: move, um: _unmount, o: { createElement } } } = sharedContext
    const storageContainer = createElement('div')
    //当从A切换到B时,B组件activate
    sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
      const instance = vnode.component
      move(vnode, container, anchor, 0 /* ENTER */, parentSuspense)
      patch(instance.vnode, vnode, container, anchor, instance, parentSuspense, isSVG, optimized)
      queuePostRenderEffect(() => { //在组件渲染完毕后,执行子节点组件定义的 activated 钩子函数。
        instance.isDeactivated = false
        if (instance.a) {
          invokeArrayFns(instance.a)
        }
        const vnodeHook = vnode.props && vnode.props.onVnodeMounted
        if (vnodeHook) {
          invokeVNodeHook(vnodeHook, instance.parent, vnode)
        }
      }, parentSuspense)
    }
    //当从A切换到B时,A组件被卸载
    sharedContext.deactivate = (vnode) => {
      const instance = vnode.component
      //只是移出了DOM,没哟真正意义上执行子组件的整套卸载流程
      move(vnode, storageContainer, null, 1 /* LEAVE */, parentSuspense)
      queuePostRenderEffect(() => { //执行自定义的deactivated 钩子函数
        if (instance.da) {
          invokeArrayFns(instance.da)
        }
        const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted
        if (vnodeHook) {
          invokeVNodeHook(vnodeHook, instance.parent, vnode)
        }
        instance.isDeactivated = true
      }, parentSuspense)
    }
    function unmount(vnode) {
      resetShapeFlag(vnode)
      _unmount(vnode, instance, parentSuspense)
    }
    function pruneCache(filter) {
      cache.forEach((vnode, key) => {
        const name = getName(vnode.type)
        if (name && (!filter || !filter(name))) {
          pruneCacheEntry(key)
        }
      })
    }
    function pruneCacheEntry(key) {
      const cached = cache.get(key)
      if (!current || cached.type !== current.type) {
        unmount(cached)
      }
      else if (current) {
        resetShapeFlag(current)
      }
      cache.delete(key)
      keys.delete(key)
    }
    //监听props的变化。
    //当 include 发生变化的时候,从缓存中删除那些 name 不匹配 include 的 vnode 节点;
    //当 exclude 发生变化的时候,从缓存中删除那些 name 匹配 exclude 的 vnode 节点。
    watch(() => [props.include, props.exclude], ([include, exclude]) => {
      include && pruneCache(name => matches(include, name))
      exclude && !pruneCache(name => matches(exclude, name))
    })
    let pendingCacheKey = null
    const cacheSubtree = () => {
      if (pendingCacheKey != null) {
        cache.set(pendingCacheKey, instance.subTree)
      }
    }
    
    //缓存的设计:初次渲染的时候会赋值key给pendingCacheKey
    onBeforeMount(cacheSubtree)
    onBeforeUpdate(cacheSubtree) //beforeUpdated的时候把instance.subTree和key存起来
    onBeforeUnmount(() => { //keepAlive所在的组件卸载。keepAlive也会被卸载
      cache.forEach(cached => {//遍历所有缓存的vnode
        const { subTree, suspense } = instance
        if (cached.type === subTree.type) {//缓存的vnode是不是当前keepalive组件渲染的vnode
          resetShapeFlag(subTree) //修改shapeFla,不在被当做一个keepalive的vnode了,就可以走正常的卸载流程了
          const da = subTree.component.da
          da && queuePostRenderEffect(da, suspense) //通过 queuePostRenderEffect 的方式执行子组件的 deactivated 钩子函数。
          return
        }
        unmount(cached)  //重置 shapeFlag 以及执行缓存 vnode 的整套卸载流程
      })
    })
    
    //当setup函数返回的是一个函数,那么这个函数就是组件的渲染函数。
    return () => {
      pendingCacheKey = null
      if (!slots.default) {
        return null
      }
      const children = slots.default()
      let vnode = children[0]
      if (children.length > 1) {//keepalive只能渲染单个子节点。
        if ((process.env.NODE_ENV !== 'production')) {
          warn(`KeepAlive should contain exactly one component child.`)
        }
        current = null
        return children
      }
      else if (!isVNode(vnode) ||
        !(vnode.shapeFlag & 4 /* STATEFUL_COMPONENT */)) {
        current = null
        return vnode
      }
      const comp = vnode.type
      const name = getName(comp)
      const { include, exclude, max } = props
      if ((include && (!name || !matches(include, name))) ||  
        (exclude && name && matches(exclude, name))) {
        return (current = vnode)  //props条件不匹配的直接返回节点
      }
      const key = vnode.key == null ? comp : vnode.key
      const cachedVNode = cache.get(key)
      if (vnode.el) {
        vnode = cloneVNode(vnode)
      }
      pendingCacheKey = key //初次渲染的时候会赋值key,编译出来的render函数可以看到会加个key
      //当再次切回来的时候可以拿到之后缓存的节点
      if (cachedVNode) {
        vnode.el = cachedVNode.el
        vnode.component = cachedVNode.component
        vnode.shapeFlag |= 512 /* COMPONENT_KEPT_ALIVE */ //避免vnode节点作为新节点被挂载
        keys.delete(key) //保证key的新鲜
        keys.add(key)
      }
      else { //添加key.如果有最大值或者超过10个,末尾添加首位移出。按照最近最久未使用的LRU算法
        keys.add(key)
        if (max && keys.size > parseInt(max, 10)) {
          pruneCacheEntry(keys.values().next().value)
        }
      }
      vnode.shapeFlag |= 256 /* COMPONENT_SHOULD_KEEP_ALIVE */
      current = vnode
      return vnode
    }
  }
}

有缓存和没有缓存在patch阶段有什么区别?

const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
  if (n1 == null) {
    // 处理 KeepAlive 组件
    //有缓存之后被标记shapeFlag,所以会走下边的逻辑。会调用keepalive中定义的sharedContext.activate
    if (n2.shapeFlag & 512 /* COMPONENT_KEPT_ALIVE */) {
      parentComponent.ctx.activate(n2, container, anchor, isSVG, optimized)
    }
    else {
      // 挂载组件
      mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
    }
  }
  else {
    // 更新组件
  }
}

当 flag 为 true 的时候,渲染 A 组件,然后我们点击按钮修改 flag 的值,会触发 KeepAlive 组件的重新渲染,会先执行 BeforeUpdate 钩子函数缓存 A 组件对应的渲染子树 vnode,然后再执行 patch 更新子组件。
这个时候会执行 B 组件的渲染,以及 A 组件的卸载,我们知道组件的卸载会执行 unmount 方法

const unmount = (vnode, parentComponent, parentSuspense, doRemove = false) => {
  const { shapeFlag  } = vnode
  if (shapeFlag & 256 /* COMPONENT_SHOULD_KEEP_ALIVE */) {
    parentComponent.ctx.deactivate(vnode)
    return
  }
  // 卸载组件
}

总结: 1,KeepAlive是一个内置组件,只渲染内部节点,且children只能有1个 2,初始化渲染的时候,会经过一些判断,如果children个数大于1,或者不是vnode,或者不在include再exclude的,直接返回子节点。 没有缓存添加key到keys,并且判断max,做新鲜化处理。 最后设置shapeFlag,返回vnode 3,当有切换的时候会触发更新,A切到B,触发beforeUpdate钩子函数,此时会通过cache保存key和instance.subtree. A组件unmount,deactivate,B组件activate,然后patch更新子组件