当咱们经过Vue开发项目时候,是否会有如下场景需求?javascript
/a
跳转到/b
/a
时候,但愿从缓存中恢复页面/b
时,分两种状况
/b
页面,而不是从缓存中读取这个场景需求着重强调了缓存,缓存带来的好处是,我上次页面的数据及状态都被保留,无需在从服务器拉取数据,使用户体验大大提升。html
<keep-alive>
<router-view></router-view>
</keep-alive>
复制代码
so easy可是理想很完美,现实很残酷vue
-/a
跳到/b
,再跳转到/a
的时候,页面中的数据是第一次访问的/a
页面,明明是连接跳转,确出现了缓存的效果,而咱们指望的是像app同样开启一个新的页面。java
/page/1
->/page/2
由于两个页面引用的是同一个组件,因此跳转时页面就不会有任何改变,由于keep-alive的缓存的key是根据组件来生成的(固然Vue提供了beforeRouteUpdate钩子供咱们刷新数据)举个应用场景node
例如浏览文章页面,依次访问3篇文章react
当我从/artical/3
后退到/artical/2
时候,因为组件缓存,此时页面仍是文章3的内容,因此必须经过beforeRouteUpdate来从新拉取页面2的数据。(注意此处后退不会触发组件的activated钩子,由于两个路由都渲染同个组件,因此实例会被复用,不会执行reactivateComponent)git
若是你想从/artical/3
后退到/artical/2
时,同时想恢复以前在/artical/2
中的一些状态,那么你还须要本身针对/artical/2
中的全部状态数据进行存储和恢复。github
综上:keep-alive实现的组件级别的缓存和咱们想象中的缓存仍是有差距的,keep-alive并不能知足咱们的需求。浏览器
==针对这些问题,因此feb-alive插件诞生了==缓存
因为feb-alive是基于keep-alive实现的,因此咱们先简单分析一下keep-alive是如何实现缓存的
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
// 获取第一个组件,也就和官方说明的同样,keep-alive要求同时只有一个子元素被渲染,若是你在其中有 v-for 则不会工做。
const vnode: VNode = getFirstComponentChild(slot)
// 判断是否存在组件选项,也就是说只对组件有效,对于普通的元素则直接返回对应的vnode
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// 检测include和exclude
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
// 若是指定了子组件的key则使用,不然经过cid+tag生成一个key
const key: ?string = vnode.key == null
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
// 判断是否存在缓存
if (cache[key]) {
// 直接复用组件实例,并更新key的位置
vnode.componentInstance = cache[key].componentInstance
remove(keys, key)
keys.push(key)
} else {
// 此处存储的vnode尚未实例,在以后的流程中经过在createComponent中会生成实例
cache[key] = vnode
keys.push(key)
// 当缓存数量大于阈值时,删除最先的key
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
// 设置keepAlive属性,createComponent中会判断是否已经生成组件实例,若是是且keepAlive为true则会触发actived钩子。
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
}
复制代码
keep-alive是一个抽象组件,组件实例中维护了一份cache,也就是如下代码部分
created () {
// 存储组件缓存
this.cache = Object.create(null)
this.keys = []
}
复制代码
因为路由切换并不会销毁keep-alive组件,因此缓存是一直存在的(嵌套路由中,子路由外层的keep-alive状况会不同,后续会提到)
继续看下keep-alive在缓存的存储和读取的具体实现,先用一个简单的demo来描述keep-alive对于组件的缓存以及恢复缓存的过程
let Foo = {
template: '<div class="foo">foo component</div>',
name: 'Foo'
}
let Bar = {
template: '<div class="bar">bar component</div>',
name: 'Bar'
}
let gvm = new Vue({
el: '#app',
template: ` <div id="#app"> <keep-alive> <component :is="renderCom"></component> </keep-alive> <button @click="change">切换组件</button> </div> `,
components: {
Foo,
Bar
},
data: {
renderCom: 'Foo'
},
methods: {
change () {
this.renderCom = this.renderCom === 'Foo' ? 'Bar': 'Foo'
}
}
})
复制代码
上面例子中,根实例的template会被编译成以下render函数
function anonymous( ) {
with(this){return _c('div',{attrs:{"id":"#app"}},[_c('keep-alive',[_c(renderCom,{tag:"component"})],1),_c('button',{on:{"click":change}})],1)}
}
复制代码
可以使用在线编译:cn.vuejs.org/v2/guide/re…
根据上面的render函数能够知道,vnode生成的过程是深度递归的,先建立子元素的vnode再建立父元素的vnode。 因此首次渲染的时候,在生成keep-alive组件vnode的时候,Foo组件的vnode已经生成好了,而且做为keep-alive组件vnode构造函数(_c)的参数传入。
_c('keep-alive',[_c(renderCom,{tag:"component"})
复制代码
生成的keep-alive组件的vnode以下
{
tag: 'vue-component-2-keep-alive',
...
children: undefined,
componentInstance: undefined,
componentOptions: {
Ctor: f VueComponent(options),
children: [Vnode],
listeners: undefined,
propsData: {},
tag: 'keep-alive'
},
context: Vue {...}, // 调用 $createElement/_c的组件实例, 此处是根组件实例对象
data: {
hook: {
init: f,
prepatch: f,
insert: f,
destroy: f
}
}
}
复制代码
此处须要注意组件的vnode是没有children的,而是将本来的children做为vnode的componentOptions的children属性,componentOptions在组件实例化的时候会被用到,同时在初始化的时候componentOptions.children最终会赋值给vm.$slots,源码部分以下
// createComponent函数
function createComponent (Ctor, data, context, children, tag) {
// 此处省略部分代码
...
var vnode = new VNode(
("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
data, undefined, undefined, undefined, context,
{ Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
asyncFactory
);
return vnode
}
复制代码
Vue最后都会经过patch函数进行渲染,将vnode转换成真实的dom,对于组件则会经过createComponent进行渲染
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
var i = vnode.data;
if (isDef(i)) {
var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */);
}
// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it. the child
// component also has set the placeholder vnode's elm.
// in that case we can just return the element and be done.
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue);
insert(parentElm, vnode.elm, refElm);
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
}
return true
}
}
}
复制代码
接下去分两步介绍
因此在执行createElm(keepAliveVnode)
的过程当中会对keep-alive组件的实例化及挂载,而在实例化的过程当中,keep-alive包裹的子组件的vnode会赋值给keep-alive组件实例的$slot属性,因此在keep-alive实例调用render函数时,能够经过this.$slot拿到包裹组件的vnode,在demo中,就是Foo组件的vnode,具体分析下keep-alive组件的render函数
render () {
const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot)
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
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
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
remove(keys, key)
keys.push(key)
} else {
cache[key] = vnode
keys.push(key)
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
复制代码
上面分析到,在执行createElm(keepAliveVnode)
的过程当中,会执行keep-alive组件的实例化及挂载($mount)
,而在挂载的过程当中,会执行keep-alive的render函数,以前分析过,在render函数中,能够经过this.$slot获取到子组件的vnode,从上面源码中,能够知道,keep-alive只处理默认插槽的第一个子组件,言外之意若是在keep-alive中包裹多个组件的话,剩下的组件会被忽略,例如:
<keep-alive>
<Foo />
<Bar />
</keep-alive>
// 只会渲染Foo组件
复制代码
继续分析,在拿到Foo组件vnode后,判断了componentOptions,因为咱们的Foo是一个组件,因此这里componentOptions是存在的,进到if逻辑中,此处include 表示只有匹配的组件会被缓存,而 exclude 表示任何匹配的组件都不会被缓存,demo中并无设置相关规则,此处先忽略。
const { cache, keys } = this
复制代码
cache, keys是在keep-alive组件的create钩子中生成的,用来存储被keep-alive缓存的组件的实例以及对应vnode的key
created () {
this.cache = Object.create(null)
this.keys = []
}
复制代码
继续下面
const key: ?string = vnode.key == null
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
remove(keys, key)
keys.push(key)
} else {
cache[key] = vnode
keys.push(key)
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
复制代码
首先,取出vnode的key,若是vnode.key存在则使用vnode.key,不存在则用componentOptions.Ctor.cid + (componentOptions.tag ?
::${componentOptions.tag}: '')
做为存储组件实例的key,据此能够知道,若是咱们不指定组件的key的话,对于相同的组件会匹配到同一个缓存,这也是为何最开始在描述keep-alive的时候强调它是一个组件级的缓存方案。
那么首次渲染的时候,cache和keys都是空的,这里就会走else逻辑
cache[key] = vnode
keys.push(key)
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
复制代码
以key做为cache的健进行存储Foo组件vnode(注意此时vnode上面尚未componentInstance)
,这里利用了对象存储的原理,以后进行Foo组件实例化的时候会将其实例赋值给vnode.componentInstance,那么在下次keep-alive组件render的时候就能够获取到vnode.componentInstance。
因此首次渲染仅仅是在keep-alive的cache上面,存储了包裹组件Foo的vnode。
上面已经讲到执行了keep-alive的render函数,根据上面的源码能够知道,render函数返回了Foo组件的vnode,那么在keep-alive执行patch的时候,会建立Foo组件的实例,而后再进行Foo组件的挂载,这个过程与普通组件并无区别,在此不累述。
本例中因为renderCom属性的变化,会触发根组件的renderWatcher,以后会执行patch(oldVnode, vnode) 在进行child vnode比较的时候,keep-alive的新老vnode比较会被断定为sameVnode,以后会进入到patchVnode的逻辑
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
if (oldVnode === vnode) {
return
}
// 此处省略代码
...
var i;
var data = vnode.data;
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode);
}
// 此处省略代码
...
}
复制代码
因为咱们的keep-alive是组件,因此在vnode建立的时候,会注入一些生命周期钩子,其中就包含prepatch钩子,其代码以下
prepatch: function prepatch (oldVnode, vnode) {
var options = vnode.componentOptions;
var child = vnode.componentInstance = oldVnode.componentInstance;
updateChildComponent(
child,
options.propsData, // updated props
options.listeners, // updated listeners
vnode, // new parent vnode
options.children // new children
);
}
复制代码
由此可知,keep-alive组件的实例在这次根组件重渲染的过程当中会复用,这也保证了keep-alive组件实例上面以前存储cache仍是存在的
var child = vnode.componentInstance = oldVnode.componentInstance;
复制代码
下面的updateChildComponent这个函数很是关键,这个函数担任了Foo组件切换到Bar组件的关键任务。咱们知道,因为keep-alive组件是在此处是复用的,因此不会再触发initRender
,因此vm.$slot不会再次更新。因此在updateChildComponent函数担起了slot更新的重任
function updateChildComponent ( vm, propsData, listeners, parentVnode, renderChildren ) {
if (process.env.NODE_ENV !== 'production') {
isUpdatingChildComponent = true;
}
// determine whether component has slot children
// we need to do this before overwriting $options._renderChildren
var hasChildren = !!(
renderChildren || // has new static slots
vm.$options._renderChildren || // has old static slots
parentVnode.data.scopedSlots || // has new scoped slots
vm.$scopedSlots !== emptyObject // has old scoped slots
);
// ...
// resolve slots + force update if has children
if (hasChildren) {
vm.$slots = resolveSlots(renderChildren, parentVnode.context);
vm.$forceUpdate();
}
if (process.env.NODE_ENV !== 'production') {
isUpdatingChildComponent = false;
}
}
复制代码
updateChildComponent函数主要更新了当前组件实例上的一些属性,这里包括props,listeners,slots。咱们着重讲一下slots更新,这里经过resolveSlots获取到最新的包裹组件的vnode,也就是demo中的Bar组件,以后经过vm.$forceUpdate强制keep-alive组件进行从新渲染。(小提示:当咱们的组件有插槽的时候,该组件的父组件re-render时会触发该组件实例$fourceUpdate,这里会有性能损耗,由于无论数据变更是否对slot有影响,都会触发强制更新,根据vueConf上尤大的介绍,此问题在3.0会被优化),例如
// Home.vue
<template>
<Artical>
<Foo />
</Artical>
</tempalte>
复制代码
此例中当Home组件更新的时候,会触发Artical组件的强制刷新,而这种刷新是多余的。
继续,在更新了keep-alive实例的forceUpdate,以后再次进入到keep-alive的render函数中
render () {
const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot)
// ...
}
复制代码
此时render函数中获取到vnode就是Bar组件的vnode,接下去的流程和Foo渲染同样,只不过也是把Bar组件的vnode缓存到keep-alive实例的cache对象中。
针对keep-alive组件逻辑仍是和上面讲述的同样
再次进入到render函数,这时候cache[key]就会匹配到Foo组件首次渲染时候缓存的vnode了,看下这部分逻辑
const key: ?string = vnode.key == null
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
remove(keys, key)
keys.push(key)
} else {
cache[key] = vnode
keys.push(key)
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
复制代码
因为keep-alive包裹的组件是Foo组件,根据规则,此时生成的key和第一此渲染Foo组件时生成的key是同样的,因此本次keep-alive的render函数进入到了第一个if分支,也就是匹配到了cache[key],把缓存的componentInstance赋值给当前vnode,而后更新keys(当存在max的时候,可以保证被删除的是比较老的缓存)。
不少同窗可能会问,这里设置vnode.componentInstance会有什么做用。这里涉及到vue的源码部分。
因为是从Bar组件切换到Foo组件,因此在patch的时候,比对到此处,并不会被断定为sameVnode,因此天然而然走到createElm,因为Foo是Vue组件,因此会进入到createComponent,因此最终进入到下面函数片断
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
var i = vnode.data;
if (isDef(i)) {
var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */);
}
// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it. the child
// component also has set the placeholder vnode's elm.
// in that case we can just return the element and be done.
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue);
insert(parentElm, vnode.elm, refElm);
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
}
return true
}
}
}
复制代码
能够根据上面对于keep-alive源码的分析,此处isReactivated为true,接下去会进入到vnode生成的时候挂在的生命周期init函数
var componentVNodeHooks = {
init: function init (vnode, hydrating) {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
var mountedNode = vnode; // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode);
} else {
var child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
);
child.$mount(hydrating ? vnode.elm : undefined, hydrating);
}
},
prepatch: function prepatch (oldVnode, vnode) {
var options = vnode.componentOptions;
var child = vnode.componentInstance = oldVnode.componentInstance;
updateChildComponent(
child,
options.propsData, // updated props
options.listeners, // updated listeners
vnode, // new parent vnode
options.children // new children
);
},
...
}
复制代码
此时因为实例已经存在,且keepAlive为true,因此会走第一个if逻辑,会执行prepatch,更新组件属性及一些监听器,若是存在插槽的话,还会更新插槽,并执行$forceUpdate,此处在前面已经分析过,不作累述。
继续createComponent,在函数内部会执行initComponent和insert
if (isDef(vnode.componentInstance)) {
// 将实例上的dom赋值给vnode
initComponent(vnode, insertedVnodeQueue);
// 插入dom
insert(parentElm, vnode.elm, refElm);
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
}
return true
}
复制代码
至此,当组件从Bar再次切换到Foo时,实例与dom都获得了复用,达到一个很高的体验效果!而咱们以后要实现的feb-alive就是基于keep-alive实现的。