Vue 中 keep-alive 组件与 router-view 组件的那点事

Posted vv_小虫

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Vue 中 keep-alive 组件与 router-view 组件的那点事相关的知识,希望对你有一定的参考价值。

最近项目中有小伙伴找到我,问我“为啥他写的页面第一次进去可以触发 onCreate 函数,第二次再进的时候就不触发了呢?”(因为我们项目是一个大型的项目,每个开发可能只接触到自己开发的一小部分),然后我就说你可以试着在 activated 钩子函数中做处理,然后他又接着问我“activated 钩子函数又是怎么调用的呢?”,ok!这小子是问上瘾了,我们下面就来详细解析一下。

keep-alive

<keep-alive> 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。和 <transition> 相似,<keep-alive> 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中。

当组件在 <keep-alive> 内被切换,它的 activateddeactivated 这两个生命周期钩子函数将会被对应执行。

在 2.2.0 及其更高版本中,activateddeactivated 将会在 <keep-alive> 树内的所有嵌套组件中触发。

主要用于保留组件状态或避免重新渲染。

为了更好的来解析 <keep-alive>,我们 copy 到一份源码(vue@^2.6.10),vue/src/core/components/keep-alive.js

/* @flow */

import  isRegExp, remove  from 'shared/util'
import  getFirstComponentChild  from 'core/vdom/helpers/index'

type VNodeCache =  [key: string]: ?VNode ;

function getComponentName (opts: ?VNodeComponentOptions): ?string 
  return opts && (opts.Ctor.options.name || opts.tag)


function matches (pattern: string | RegExp | Array<string>, name: string): boolean 
  if (Array.isArray(pattern)) 
    return pattern.indexOf(name) > -1
   else if (typeof pattern === 'string') 
    return pattern.split(',').indexOf(name) > -1
   else if (isRegExp(pattern)) 
    return pattern.test(name)
  
  /* istanbul ignore next */
  return false


function pruneCache (keepAliveInstance: any, filter: Function) 
  const  cache, keys, _vnode  = keepAliveInstance
  for (const key in cache) 
    const cachedNode: ?VNode = cache[key]
    if (cachedNode) 
      const name: ?string = getComponentName(cachedNode.componentOptions)
      if (name && !filter(name)) 
        pruneCacheEntry(cache, key, keys, _vnode)
      
    
  


function pruneCacheEntry (
  cache: VNodeCache,
  key: string,
  keys: Array<string>,
  current?: VNode
) 
  const cached = cache[key]
  if (cached && (!current || cached.tag !== current.tag)) 
    cached.componentInstance.$destroy()
  
  cache[key] = null
  remove(keys, key)


const patternTypes: Array<Function> = [String, RegExp, Array]

export default 
  name: 'keep-alive',
  abstract: true,

  props: 
    include: patternTypes,
    exclude: patternTypes,
    max: [String, Number]
  ,

  created () 
    this.cache = Object.create(null)
    this.keys = []
  ,

  destroyed () 
    for (const key in this.cache) 
      pruneCacheEntry(this.cache, key, this.keys)
    
  ,

  mounted () 
    this.$watch('include', val => 
      pruneCache(this, name => matches(val, name))
    )
    this.$watch('exclude', val => 
      pruneCache(this, name => !matches(val, name))
    )
  ,

  render () 
    const slot = this.$slots.default
    const vnode: VNode = getFirstComponentChild(slot) // 获取第一个子节点
    const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
    if (componentOptions) 
      // check pattern
      const name: ?string = getComponentName(componentOptions) // 获取节点的名称
      const  include, exclude  = this
      if (
        // not included
        (include && (!name || !matches(include, name))) ||
        // excluded
        (exclude && name && matches(exclude, name)) // 不在范围内的节点将不会被缓存
      ) 
        return vnode
      

      const  cache, keys  = this
      const key: ?string = vnode.key == null
        // same constructor may get registered as different local components
        // so cid alone is not enough (#3269)
        ? componentOptions.Ctor.cid + (componentOptions.tag ? `::$componentOptions.tag` : '')
        : vnode.key // 获取缓存的 key 值
      if (cache[key])  // 如果有缓存就使用缓存
        vnode.componentInstance = cache[key].componentInstance
        // make current key freshest
        remove(keys, key)
        keys.push(key)
       else  // 没有缓存就将当前节点加入到缓存
        cache[key] = vnode
        keys.push(key)
        // prune oldest entry
        if (this.max && keys.length > parseInt(this.max))  // 如果缓存超过最大限制将不再缓存
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        
      

      vnode.data.keepAlive = true // 标记该节点为 keepAlive 类型
    
    return vnode || (slot && slot[0])
  

其实从源码我们可以看到,代码并没有多少,还是比较简单的,下面我们用一下 keep-alive 组件。

Props

从源码中我们可以看到,<keep-alive> 组件有三个属性:

  • include - 字符串或正则表达式。只有名称匹配的组件会被缓存。
  • exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存。
  • max - 数字。最多可以缓存多少组件实例。

下面我们结合 Demo 来分析一下。

我们直接用 vue-cli 创建一个简单的 vue 项目,取名为 keep-alive-demo:

vue create keep-alive-demo

然后选一下 Router 后一路回车:

我们修改一下 App.vue 文件:

<template>
  <div id="app">
    <router-view/>
  </div>
</template>

<style>
#app 
  text-align: center;

</style>

然后 views 目录创建一个 A 组件当作 页面 A

<template>
  <div class="about">
    <h1>我是 a 页面</h1>
    <router-link to="/pageB">点我跳转到 b 页面</router-link>
  </div>
</template>
<script>
  import LifeRecycle from "../life-recycle";
  export default 
    name: "page-a",
    mixins:[LifeRecycle]
  
</script>

A 页面很简单,里面一个按钮链接到了 B 页面。为了更好的显示每个组件的生命周期,我们为每个页面添加了一个 mixin

export default 
    computed: 
        name()
            return this.$options.name;
        
    ,
    created()
        console.log("created--->"+this.name);
    ,
    activated() 
        console.log("activated--->"+this.name);
    ,
    deactivated() 
        console.log("deactivated--->"+this.name);
    ,
    destroyed() 
        console.log("destoryed--->"+this.name);
    

直接 copy 一份 A.vue 代码创建一个 页面 B

<template>
  <div class="about">
    <h1>我是 b 页面</h1>
  </div>
</template>
<script>
  import LifeRecycle from "../life-recycle";
  export default 
    name: "page-b",
    mixins:[LifeRecycle]
  
</script>

然后修改一下 views/Home.vue

<template>
    <div class="home">
        <h1>我是首页</h1>
        <router-link to="/pageA">点我跳转到 a 页面</router-link>
    </div>
</template>
<script>
    import LifeRecycle from "../life-recycle";

    export default 
        name: 'home',
        mixins: [LifeRecycle]
    
</script>

给一个按钮直接链接到了 页面 A

最后我们修改一下 router.js

import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'

Vue.use(Router)

export default new Router(
  mode: "history",
  routes: [
    
      path: '/',
      name: 'home',
      component: Home,
    ,
    
      path: "/pageA",
      name: "pageA",
      component: () => import(/* webpackChunkName: "about" */ './views/A.vue')
    ,
    
      path: "/pageB",
      name: "pageB",
      component: () => import(/* webpackChunkName: "about" */ './views/B.vue')
    
  ]
)

代码很简单,我就不详细解析了,一个简单的 SPA(单页面应用) 就搭建完成了,三个平级的页面 homepageApageB

我们试着运行一下项目:

npm run serve

可以看到:

  1. 首页打开 home 页面

    created--->home
    

    直接触发了 home 页面的 created 方法。

  2. home 页面 —> pageA 页面

created--->page-a
destoryed--->home

home 页面触发了 destoryed 直接销毁了,然后触发了pageA 页面的 created 方法。

  1. pageA 页面 —> pageB 页面
created--->page-b
destoryed--->page-a

pageA 页面触发了 destoryed 直接销毁了,然后触发了pageB 页面的 created 方法。

  1. pageB 页面返回

    created--->page-a
    destoryed--->page-b
    

    pageB 页面触发了 destoryed 直接销毁了,然后触发了pageA 页面的 created 方法。

  2. pageA 页面返回

    created--->home
    destoryed--->page-a
    

    pageA 页面触发了 destoryed 直接销毁了,然后触发了home 页面的 created 方法。

    效果是没问题的,但是作为一个 SPA 的项目,这种用户体验肯定是不友好的,试想一下,你现在在一个 app 的首页浏览页面,然后滑呀滑呀,滑动了很长的页面好不容易看到了一个自己感兴趣的东西,然后点击查看详情离开了首页,再回到首页时候肯定是想停留在之前浏览器的地方,而不是说重新又打开一个新的首页,又要滑半天,这种体验肯定是不好的,而且也有点浪费资源,所以下面我们用一下 <keep-alive> 把首页缓存起来。

    我们修改一下 App.vue 文件:

    <template>
      <div id="app">
        <keep-alive>
          <router-view/>
        </keep-alive>
      </div>
    </template>
    

    可以看到,我们添加了一个<keep-alive> 组件,然后再次之前的操作:

    1. 首页打开 home 页面

      created--->home
      activated--->home
      

      直接触发了 home 页面的 created 方法。

    2. home 页面 —> pageA 页面

    created--->page-a
    deactivated--->home
    activated--->page-a
    

    home 页面触发了 deactivated 变成非活跃状态,然后触发了pageA 页面的 activated 方法。

    1. pageA 页面 —> pageB 页面
    created--->page-b
    deactivated--->page-a
    activated--->page-b
    

    pageA 页面触发了 deactivated 变成非活跃状态,然后触发了pageB 页面的 activated 方法。

    1. pageB 页面返回

      deactivated--->page-b
      activated--->page-a
      

      pageB 页面触发了 deactivated 变成非活跃状态,然后触发了pageA 页面的 activated 方法。

    2. pageA 页面返回

      deactivated--->page-a
      activated--->home
      

细心的童鞋应该已经发现区别了吧?每个页面的 destoryed 不触发了,替换成了 deactivated,然后第一次创建页面的时候除了之前的 created 还多了一个 activated 方法。

是的!当我们加了<keep-alive> 组件后,所有页面都被缓存起来了,但是我们只需要缓存的是 home 页面,我们该怎么做呢?

  1. 利用 include 属性规定缓存的范围

    我们修改一下 App.vue<keep-alive> 组件添加 include 属性:

     <keep-alive :include="['home']">
          <router-view/>
      </keep-alive>
    

    include 可以是一个字符串数组,也可以是一个正则表达式,匹配的就是组件的名字,比如这里的 home,其实就是 home 组件的名称:

    ...
    export default 
            name: 'home',
            mixins: [LifeRecycle]
        
        ...
    
  2. 利用 exclude 属性规定不缓存的范围

    这个刚好跟 include 属性相反,我们可以修改一下 App.vue<keep-alive> 组件添加 exclude 属性:

    ..
    <keep-alive :exclude="/page-/">
          <router-view/>
        </keep-alive>
    ...
    

到这里我们思考一个问题,<keep-alive> 是会帮我们缓存组件,但是缓存的数量小倒还好,数量大了就有点得不偿失了,所以 vue 考虑到这个情况了,然后给<keep-alive> 添加了一个 max 属性,比如我们只需要缓存一个页面,我们只需要设置 :max=1 即可:

..
<template>
  <div id="app">
    <keep-alive :max="1">
      <router-view/>
    </keep-alive>
  </div>
</template>
...

<keep-alive> 每次会缓存最新的那个页面:

  1. 首页打开 home 页面

    created--->home
    activated--->home
    

    直接触发了 home 页面的 created 方法新创建了一个页面,然后调用了 activated 方法激活了当前页面。

  2. home 页面 —> pageA 页面

    created--->page-a
    deactivated--->home
    activated--->page-a
    

    home 页面触发了 deactivated 变成了非活跃状态,然后触发了pageA 页面的 created 方法新创建了一个页面,然后调用了 activated 方法激活了当前页面。

  3. pageA 页面点击返回

    created--->home
    deactivated--->page-a
    activated--->home
    

    pageA 页面触发了 deactivated 变成了非活跃状态,然后触发了home 页面的 created 方法新创建了一个页面,然后调用了 activated 方法激活了当前页面。

当缓存页面的个数大于最大限制的时候,每次都移除数据的第 0 个位置的缓存,源码为:

// 如果缓存数 > 最大缓存数,移除缓存数组的第 0 位置数据
if (this.max && keys.length > parseInt(this.max))  
          pruneCacheEntry(cache, keys[0], keys, this._vnode)

...
function pruneCacheEntry (
  cache: VNodeCache,
  key: string,
  keys: Array<string>,
  current?: VNode
) 
  const cached = cache[key] // 获取需要移除的缓存页面
  if (cached && (!current || cached.tag !== current.tag))  // 如果当前页面跟缓存的页面不一致的时候
    // 触发移除的缓存页面的 destroy 方法
    cached.componentInstance.$destroy()
  
  cache[key] = null
  remove(keys, key)

比如当 :max="2" 的时候,home —> pageA —> pageB,当进入 pageB 的时候,home 页面就会被销毁,会触发 home 页面的 destroyed 方法。

到这里 <keep-alive> 组件的基本用法我们算是 ok 了。

activated 生命周期

通过上面的 demo 我们可以知道,当页面被激活的时候会触发当前页面的 activated 方法,那么 vue 是在什么时候才会去触发这个方法呢?

我们找到 vue 源码位置 /vue/src/core/vdom/create-component.js:

...
insert (vnode: MountedComponentVNode) 
    const  context, componentInstance  = vnode
    if (!componentInstance._isMounted) 
      componentInstance._isMounted = true
      callHook(componentInstance, 'mounted')
    
    if (vnode.data.keepAlive) 
      if (context._isMounted) 
        // vue-router#1212
        // 在更新过程中,一个缓存的页面的子组件可能还会改变,
        // 当前的子组件并不一定就是最后的子组件,所以这个时候去调用 activaved 方法会不准确
        // 当页面都组件更新完毕之后再去调用。
        queueActivatedComponent(componentInstance)
       else 
        // 递归激活所有子组件
        activateChildComponent(componentInstance, true /* direct */)
      
    
  ,
    ...
export function activateChildComponent (vm: Component, direct?: boolean) 
  if (direct) 
    vm._directInactive = false
    if (isInInactiveTree(vm)) 
      return
    
   else if (vm._directInactive) 
    return
  
  if (vm._inactive || vm._inactive === null) 
    vm._inactive = false
    // 先循环调用所有子组件的 activated 方法
    for (let i = 0; i < vm.$children.length; i++) 
      activateChildComponent(vm.$children[i])
    
    // 再调用当前组件的 activated 方法
    callHook(vm, 'activated')
  

当前 vnode 节点被插入的时候会判断当前 vnode 节点 data 上是不是有 keepAlive 标记,有的话就会激活自身和自己所有的子组件,通过源码我们还发现,当组件第一次创建的时候 activated 方法是在 mounted 方法之后执行。

deactivated 生命周期

通过上面的 demo 我们可以知道,当页面被隐藏的时候会触发当前页面的 deactivated 方法,那么 vue 是在什么时候才会去触发这个方法呢?

activated 方法一样,我们找到 vue 源码位置 /vue/src/core/vdom/create-component.js:

...
destroy (vnode: MountedComponentVNode) 
    const  componentInstance  = vnode
    if (!componentInstance._isDestroyed) 
      if (!vnode.data.keepAlive) 
        componentInstance.$destroy()
       else 
        deactivateChildComponent(componentInstance, true /* direct */)
      
    
  
...
export function deactivateChildComponent (vm: Component, direct?: boolean) 
  if (direct) 
    vm._directInactive = true
    if (isInInactiveTree(vm)) 
      return
    
  
  if (!vm._inactive) 
    vm._inactive = true
    for (let i = 0; i < vm.$children.length; i++) 
      deactivateChildComponent(vm.$children[i])
    
    callHook(vm, 'deactivated')
  

当前vnode 节点被销毁的时候,会判断当前节点是不是有 keepAlive 标记,有的话就不会直接调用组件的 destroyed 了,而是直接调用组件的 deactivated 方法。

那么节点的 keepAlive 是啥时候被标记的呢?还记得我们的 <keep-alive> 组件的源码不?

vue/src/core/components/keep-alive.js:

...
render () 
    ...
      vnode.data.keepAlive = true // 标记该节点为 keepAlive 类型
    
    return vnode || (slot && slot[0])
  
...

ok!看到这里是不是就一目了然了呢?

router-view

router-view 组件的基本用法跟原理我就不在这里解析了,感兴趣的童鞋可以去看 官网 ,也可以去看我之前的一些文章 前端入门之(vue-router全解析二)

我们看一下router-view 组件中的源码:

import  warn  from '../util/warn'
import  extend  from '../util/misc'
import  handleRouteEntered  from '../util/route'

export default 
  name以上是关于Vue 中 keep-alive 组件与 router-view 组件的那点事的主要内容,如果未能解决你的问题,请参考以下文章

vue 缓存组件keep-alive

keep-alive实现返回保留筛选条件及筛选结果

Vue设置导航栏为公共模块并在登录页不显示

Vue 中 keep-alive 组件与 router-view 组件的那点事

vue中动态路由组件缓存及生命周期函数

vue中的缓存——keep-alive,activated,deactivated