Vue3 KeepAlive组件原理分析

更新日期: 2021-12-09阅读: 1.4k标签: Vue3

前言

知其然而知其所以然,优秀的工程师不仅要能熟练的使用框架,还要了解其底层是如何实现的。本文主要探究vue3源码中内置KeepAlive组件实现原理。

KeepAlive 是一个抽象组件,它并不会渲染成一个真实的 dom,只会渲染内部包裹的子节点,并且让内部的子组件在切换的时候,不会走一整套递归卸载和挂载 DOM的流程,从而优化了性能。如果你要了解使用方法,官网已经介绍的很详细了。

实现原理

KeepAlive 组件在源码实现的是一个对象,其实现主要是组件的渲染、组件缓存的处理、props三个参数的处理和组件卸载过程。

// packages/runtime-core/src/components/KeepAlive.ts
const KeepAliveImpl: ComponentOptions = {
  name: `KeepAlive`,
  // keep-alive 组件接收的三个参数
  props: {
    include: [String, RegExp, Array],
    exclude: [String, RegExp, Array],
    max: [String, Number]
  },
  ...

  setup(props: KeepAliveProps, { slots }: SetupContext) {
    // 组件卸载逻辑
    ...
    // 返回渲染函数
    return () => {
      ...
       // 缓存逻辑处理
       // props 参数处理逻辑
       // 组件初始化逻辑
    }
  }
}

上面示例中,当 setup 函数返回一个函数,这个函数就是组件的渲染函数。

组件渲染

直接看下面源码:

// packages/runtime-core/src/components/KeepAlive.ts
const KeepAliveImpl: ComponentOptions = {
  name: `KeepAlive`,
  ...
  setup(props: KeepAliveProps, { slots }: SetupContext) {
    ...
    // 返回渲染函数
    return () => {
      pendingCacheKey = null

      if (!slots.default) {
        return null
      }
      // 获取keep-alive 包裹的 children 元素
      const children = slots.default()
      const rawVNode = children[0]
      if (children.length > 1) {
        // keep-alive 只渲染单个子节点,大于1 报错
        if (__DEV__) {
          warn(`KeepAlive should contain exactly one component child.`)
        }
        current = null
        return children
      } else if (
        !isVNode(rawVNode) ||
        (!(rawVNode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) &&
          !(rawVNode.shapeFlag & ShapeFlags.SUSPENSE))
      ) {
        // 不是vnode节点 或者自定义组件和SUSPENSE 则返回
        current = null
        return rawVNode
      }
      ...
      current = vnode
      return rawVNode
    }
  }
}

从上面可以看出,KeepAlive 渲染的 vnode 就是子节点 children 的第一个元素,它是函数的返回值。因此我们说 KeepAlive 是抽象组件,它本身不渲染成实体节点,而是渲染它的第一个子节点。

缓存处理

KeepAlive 组件缓存的东西是 DOM,因为渲染 DOM 是 patch 递归的过程,也是最损耗性能的。 KeepAlive 组件注入了两个钩子函数,onMounted 和 onUpdated,在这两个钩子函数内部都执行了 cacheSubtree 函数来做缓存:

...
// packages/runtime-core/src/components/KeepAlive.ts
const cacheSubtree = () => {
  // fix #1621, the pendingCacheKey could be 0
  if (pendingCacheKey != null) {
    cache.set(pendingCacheKey, getInnerChild(instance.subTree))
  }
}
onMounted(cacheSubtree)
onUpdated(cacheSubtree)

pendingCacheKey 是在 KeepAlive 的 render 函数中才会被赋值,所以 KeepAlive 首次进入 onMounted 钩子函数的时候是不会缓存的。然后 KeepAlive 执行 render 的时候,pendingCacheKey 会被赋值为 vnode.key。

所以当组件切换会触发组件的重新渲染,进而也触发了 KeepAlvie 组件的重新渲染,在组件重新渲染前,会执行 onUpdate 对应的钩子函数,也就再次执行到 cacheSubtree 函数中。

这个时候 pendingCacheKey 对应的是 A 组件 vnode 的 key,instance.subTree 对应的也是 A 组件的渲染子树,所以 KeepAlive 每次在更新前,会缓存前一个组件的渲染子树。

当我们再次切换回原来组件,会再次触发KeepAlvie 组件的重新渲染,当然此时执行 onUpdate 钩子函数缓存的就是 B 组件的渲染子树了。

接着再次执行 KeepAlive 组件的 render 函数,此时就可以从缓存中根据 A 组件的 key 拿到对应的渲染子树 cachedVNode 的了,然后执行如下逻辑:

if (cachedVNode) {
    // copy over mounted state
    vnode.el = cachedVNode.el
    vnode.component = cachedVNode.component
    if (vnode.transition) {
      // recursively update transition hooks on subTree
      setTransitionHooks(vnode, vnode.transition!)
    }
    // 避免 vnode 节点作为新节点被挂载
    vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
    // 让这个 key 始终新鲜
    keys.delete(key)
    keys.add(key)
} else {
    keys.add(key)
    // 删除最久不用的 key
    if (max && keys.size > parseInt(max as string, 10)) {
      pruneCacheEntry(keys.values().next().value)
    }
}

有了缓存的渲染子树后,我们就可以直接拿到它对应的 DOM 以及组件实例 component,赋值给 KeepAlive 的 vnode,并更新 vnode.shapeFlag,以便后续 patch 阶段使用。patch 主要由 processComponent 方法实现:源码如下:

  // packages/runtime-core/src/renderer.ts
  const processComponent = (n1: VNode | null,n2: VNode, ...) => {
    n2.slotScopeIds = slotScopeIds
    if (n1 == null) {
      // 处理 keep-alive
      if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
        ;(parentComponent!.ctx as KeepAliveContext).activate(
          n2,
          container,
          anchor,
          isSVG,
          optimized
        )
      } else {
        // 挂载组件
        mountComponent(n2,...)
      }
    } else {
      updateComponent(n1, n2, optimized)
    }
  }

KeepAlive 首次渲染某一个子节点时,和正常的组件节点渲染没有区别,但是有缓存后,由于标记了 shapeFlag,所以在执行processComponent函数时会走到处理 KeepAlive 组件的逻辑中,执行 KeepAlive 组件实例上下文中的 activate 函数,我们来看它的实现:

// packages/runtime-core/src/components/KeepAlive.ts
sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
  const instance = vnode.component!
  move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
  // in case props have changed
  patch(instance.vnode,vnode,container,anchor,instance,parentSuspense,isSVG,
    vnode.slotScopeIds,optimized
  )
  queuePostRenderEffect(() => {
    instance.isDeactivated = false
    if (instance.a) {
      invokeArrayFns(instance.a)
    }
    const vnodeHook = vnode.props && vnode.props.onVnodeMounted
    if (vnodeHook) {
      invokeVNodeHook(vnodeHook, instance.parent, vnode)
    }
  }, parentSuspense)

  if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
    // Update components tree
    devtoolsComponentAdded(instance)
  }
}

可以看到,由于此时已经能从 vnode.el 中拿到缓存的 DOM 了,所以可以直接调用 move 方法挂载节点,然后执行 patch 方法更新组件,以防止 props 发生变化的情况。接下来,就是通过 queuePostRenderEffect 的方式,在组件渲染完毕后,执行子节点组件定义的 activated 钩子函数。

props 参数处理

KeepAlive 一共支持了三个 Props,分别是 include、exclude 和 max。

// packages/runtime-core/src/components/KeepAlive.ts
props: {
    include: [String, RegExp, Array],
    exclude: [String, RegExp, Array],
    max: [String, Number]
}

include 和 exclude 对应的实现逻辑如下:

// packages/runtime-core/src/components/KeepAlive.ts
const { include, exclude, max } = props

if (
    (include && (!name || !matches(include, name))) ||
    (exclude && name && matches(exclude, name))
) {
    current = vnode
    return rawVNode
}

如果子组件名称不匹配 include 的 vnode ,以及子组件名称匹配 exclude 的 vnode 都不应该被缓存,而应该直接返回。

由于 props 是响应式的,在 include 和 exclude props 发生变化的时候也应该有相关的处理逻辑,如下:

// packages/runtime-core/src/components/KeepAlive.ts
watch(
  () => [props.include, props.exclude],
  ([include, exclude]) => {
    include && pruneCache(name => matches(include, name))
    exclude && pruneCache(name => !matches(exclude, name))
  },
  // prune post-render after `current` has been updated
  { flush: 'post', deep: true }
)

监听的逻辑也很简单,当 include 发生变化的时候,从缓存中删除那些 name 不匹配 include 的 vnode 节点;当 exclude 发生变化的时候,从缓存中删除那些 name 匹配 exclude 的 vnode 节点。

除了 include 和 exclude 之外,KeepAlive 组件还支持了 max prop 来控制缓存的最大个数。

// packages/runtime-core/src/components/KeepAlive.ts
if (cachedVNode) {
    ...
  } else {
    keys.add(key)
    // prune oldest entry
    if (max && keys.size > parseInt(max as string, 10)) {
      pruneCacheEntry(keys.values().next().value)
    }
  }

由于新的缓存 key 都是在 keys 的结尾添加的,所以当缓存的个数超过 max 的时候,就从最前面开始删除。

组件卸载

组件的卸载会执行 unmount 方法,其中有一个关于 KeepAlive 组件的逻辑,如下:

// packages/runtime-core/src/renderer.ts
const unmount: UnmountFn = (vnode,parentComponent,parentSuspense,doRemove = false,optimized = false) => {
  ...
  if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
    ;(parentComponent!.ctx as KeepAliveContext).deactivate(vnode)
    return
  }
   ...
}

如果 shapeFlag 满足 KeepAlive 的条件,则执行相应的 deactivate 函数,它的定义如下:

// packages/runtime-core/src/components/KeepAlive.ts
sharedContext.deactivate = (vnode: VNode) => {
  const instance = vnode.component!
  move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
  queuePostRenderEffect(() => {
    if (instance.da) {
      invokeArrayFns(instance.da)
    }
    const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted
    if (vnodeHook) {
      invokeVNodeHook(vnodeHook, instance.parent, vnode)
    }
    instance.isDeactivated = true
  }, parentSuspense)

  if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
    // Update components tree
    devtoolsComponentAdded(instance)
  }
}

函数首先通过 move 方法从 DOM 树中移除该节点,接着通过 queuePostRenderEffect 的方式执行定义的 deactivated 钩子函数。

当 KeepAlive 所在的组件卸载时,由于卸载的递归特性,也会触发 KeepAlive 组件的卸载,在卸载的过程中会执行 onBeforeUnmount 钩子函数,如下:

// packages/runtime-core/src/components/KeepAlive.ts
onBeforeUnmount(() => {
  cache.forEach(cached => {
    const { subTree, suspense } = instance
    const vnode = getInnerChild(subTree)
    if (cached.type === vnode.type) {
      // current instance will be unmounted as part of keep-alive's unmount
      resetShapeFlag(vnode)
      // but invoke its deactivated hook here
      const da = vnode.component!.da
      da && queuePostRenderEffect(da, suspense)
      return
    }
    unmount(cached)
  })
})

它会遍历所有缓存的 vnode,并且比对缓存的 vnode 是不是当前 KeepAlive 组件渲染的 vnode。

如果是的话,则执行 resetShapeFlag 方法,它的作用是修改 vnode 的 shapeFlag,不让它再被当作一个 KeepAlive 的 vnode 了,这样就可以走正常的卸载逻辑。接着通过 queuePostRenderEffect 的方式执行子组件的 deactivated 钩子函数。

如果不是,则执行 unmount 方法重置 shapeFlag 以及执行缓存 vnode 的整套卸载流程。

写在最后

最后总结一下KeepAlive组件的实现原理:

  • KeepAlive 组件渲染的 vnode 就是子节点 children 的第一个元素,它是函数的返回值。
  • KeepAlive 组件注入了两个钩子函数,onMounted 和 onUpdated,在这两个钩子函数内部都执行了 cacheSubtree 函数来做缓存。而钩子函数早于render 函数执行,所以切换的时候会缓存上一个组件,之后 patch 过程调用 activate 函数直接使用缓存vnode挂载。
  • KeepAlive 一共支持了三个 Props,分别是 include、exclude 和 max。如果子组件名称不匹配 include 的 vnode ,以及子组件名称匹配 exclude 的 vnode 都不应该被缓存,而应该直接返回。缓存 key 都是在 keys 的结尾添加的,所以当缓存的个数超过 max 的时候,就从最前面开始删除。
  • 组件的卸载会执行 unmount 方法,该方法直接调用 KeepAlive 的 deactivate 移除 DOM,当 KeepAlive 所在的组件卸载时,由于卸载的递归特性,也会触发 KeepAlive 组件的卸载,在卸载的过程中会执行 onBeforeUnmount 钩子函数循环卸载节点。

作者:蛙哇
链接:https://juejin.cn/post/7039257072201891854
来源:稀土掘金

链接: https://fly63.com/article/detial/10924

vue3.x 新特性 - CompositionAPI

安装 vue-cli3,在使用任何 @vue/composition-api 提供的能力前,必须先通过 Vue.use() 进行安装,安装插件后,您就可以使用新的 Composition API 来开发组件了。

Vue3数据响应系统

Vue3 就是基于 Proxy 对其数据响应系统进行了重写,现在这部分可以作为独立的模块配合其他框架使用。数据响应可分为三个阶段: 初始化阶段 --> 依赖收集阶段 --> 数据响应阶段

快速进阶Vue3.0

在2019.10.5日发布了Vue3.0预览版源码,但是预计最早需要等到 2020 年第一季度才有可能发布 3.0 正式版。新版Vue 3.0计划并已实现的主要架构改进和新功能:

Vue 3 对 Web 应用性能的改进

有关即将发布的 Vue.js 的第 3 个主要版本的信息越来越多。通过下面的讨论,虽然还不能完全确定其所有内容,但是我们可以放心地认为,它将是对当前版本(已经非常出色)的巨大改进。 Vue 团队在改进框架 API 方面做得非常出色

Vue3 中令人兴奋的新功能

用新的 Vue 3 编写的程序效果会很好,但性能并不是最重要的部分。对开发人员而言,最重要的是新版本将会怎样影响我们编写代码的方式。如你所料,Vue 3 带来了许多令人兴奋的新功能。值得庆幸的是

200 行从零实现 vue3

emmm 用半天时间捋顺了 vue3 的源码,再用半天时间写了个 mini 版……我觉得我也是没谁了,vue3 的源码未来一定会烂大街的,我们越早的去复现它,就……emm可以越早的装逼hhh

从 Proxy 到 Vue 源码,深入理解 Vue 3.0 响应系统

10 月 5 日,尤雨溪在 GitHub 开放了 Vue 3.0 处于 pre-alpha 状态的源码,这次 Vue 3.0 Updates 版本的更新,将带来五项重大改进:速度体积、可维护性、面向原生、易用性

Vue 的数据响应式(Vue2 及 Vue3)

从一开始使用 Vue 时,对于之前的 jq 开发而言,一个很大的区别就是基本不用手动操作 dom,data 中声明的数据状态改变后会自动重新渲染相关的 dom。换句话说就是 Vue 自己知道哪个数据状态发生了变化及哪里有用到这个数据需要随之修改。

在Vue2与Vue3中构建相同的组件

Vue 开发团队终于在今天发布了 3.0-beta.1 版本,也就是测试版。通常来说,从测试版到正式版,只会修复 bug,不会引入新功能,或者删改老功能。所以,如果你对新版本非常感兴趣,或者有新项目即将上马,不妨尝试一下新版本

Vue3中的Vue Router初探

对于大多数单页应用程序而言,管理路由是一项必不可少的功能。随着新版本的Vue Router处于Alpha阶段,我们已经可以开始查看下一个版本的Vue中它是如何工作的。

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!