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>
内被切换,它的 activated
和 deactivated
这两个生命周期钩子函数将会被对应执行。
在 2.2.0 及其更高版本中,
activated
和deactivated
将会在<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
(单页面应用) 就搭建完成了,三个平级的页面 home
、pageA
、pageB
。
我们试着运行一下项目:
npm run serve
可以看到:
-
首页打开
home
页面created--->home
直接触发了
home
页面的created
方法。 -
home
页面 —>pageA
页面
created--->page-a
destoryed--->home
home
页面触发了 destoryed
直接销毁了,然后触发了pageA
页面的 created
方法。
pageA
页面 —>pageB
页面
created--->page-b
destoryed--->page-a
pageA
页面触发了 destoryed
直接销毁了,然后触发了pageB
页面的 created
方法。
-
pageB
页面返回created--->page-a destoryed--->page-b
pageB
页面触发了destoryed
直接销毁了,然后触发了pageA
页面的created
方法。 -
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>
组件,然后再次之前的操作:-
首页打开
home
页面created--->home activated--->home
直接触发了
home
页面的created
方法。 -
home
页面 —>pageA
页面
created--->page-a deactivated--->home activated--->page-a
home
页面触发了deactivated
变成非活跃状态,然后触发了pageA
页面的activated
方法。pageA
页面 —>pageB
页面
created--->page-b deactivated--->page-a activated--->page-b
pageA
页面触发了deactivated
变成非活跃状态,然后触发了pageB
页面的activated
方法。-
pageB
页面返回deactivated--->page-b activated--->page-a
pageB
页面触发了deactivated
变成非活跃状态,然后触发了pageA
页面的activated
方法。 -
pageA
页面返回deactivated--->page-a activated--->home
-
细心的童鞋应该已经发现区别了吧?每个页面的 destoryed
不触发了,替换成了 deactivated
,然后第一次创建页面的时候除了之前的 created
还多了一个 activated
方法。
是的!当我们加了<keep-alive>
组件后,所有页面都被缓存起来了,但是我们只需要缓存的是 home
页面,我们该怎么做呢?
-
利用
include
属性规定缓存的范围我们修改一下
App.vue
给<keep-alive>
组件添加include
属性:<keep-alive :include="['home']"> <router-view/> </keep-alive>
include
可以是一个字符串数组,也可以是一个正则表达式,匹配的就是组件的名字,比如这里的home
,其实就是home
组件的名称:... export default name: 'home', mixins: [LifeRecycle] ...
-
利用
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>
每次会缓存最新的那个页面:
-
首页打开
home
页面created--->home activated--->home
直接触发了
home
页面的created
方法新创建了一个页面,然后调用了activated
方法激活了当前页面。 -
home
页面 —>pageA
页面created--->page-a deactivated--->home activated--->page-a
home
页面触发了deactivated
变成了非活跃状态,然后触发了pageA
页面的created
方法新创建了一个页面,然后调用了activated
方法激活了当前页面。 -
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 组件的那点事的主要内容,如果未能解决你的问题,请参考以下文章