结合例子有以下情况
[JavaScript] 纯文本查看 复制代码 <keep-alive>
<coma v-if="visible"></coma>
<comb v-else></comb>
</keep-alive>
<button @click="visible = !visible">更改</button>
例如在coma和comb都有一个input都有对应的value,如果我们不用keep-alive,当更改visible的时候,这两个组件都会重新渲染,先前输入的内容就会丢失,会执行一遍完整的生命周期流程:beforeCreate => created...。
但是如果我们用了keep-alive,那么在次切换visible的时候,input对应的value为上次更改时候的值。所以keep-alive主要是用于保持组件的状态,避免组件反复创建。
原理keep-alive的使用方法定在core/components/keep-alive中
[JavaScript] 纯文本查看 复制代码 export default {
abstract: true,
props: {
include: patternTypes, // 缓存白名单
exclude: patternTypes, // 缓存黑名单
max: [String, Number] // 缓存的实例上限
},
created() {
// 用于缓存虚拟DOM
this.cache = Object.create(null);
this.keys = [];
},
mounted() {
// 用于监听i黑白名单,如果发生调用pruneCache
// pruneCache更新vue的cache缓存
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
}
render() {
//...
}
}
上面代码中定义了多个声明周期的操作,最重要的render函数,下面来看看是如何实现的
render[JavaScript] 纯文本查看 复制代码 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 // 定义组件的缓存key
// 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
if (cache[key]) { // 已经缓存过该组件
vnode.componentInstance = cache[key].componentInstance
// make current key freshest
remove(keys, key)
keys.push(key) // 调整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 // 渲染和执行被包裹组件的钩子函数需要用到
}
return vnode || (slot && slot[0])
}
进行分步骤进行分析
- 获取keep-alive对象包括的第一个子组件对象
- 根据白黑名单是否匹配返回本身的vnode
- 根据vnode的cid和tag生成的key,在缓存对象中是否有当前缓存,如果有则返回,并更新key在keys中的位置
- 如果当前缓存对象不存在缓存,就往cache添加这个的内容,并且根据LRU算法删除最近没有使用的实例
- 设置为第一个子组件对象的keep-alive为true
首次渲染结合文章开头的文章进行分析当前例子,当页面首次渲染的时候,因为组件的渲染过程是先子组件后父组件的,所以这里就能拿到子组件的数据,然后把子组件的vnode信息存储到cache中,并且把coma组件的keepAlive的置为true。这个有个疑问,为什么能拿到子组件的componentOptions,借助上面个例子,我们知道生成vnode是通过render函数,render函数是通过在platforms/web/entry-runtime-with-compiler中定义,通过compileToFunctions将template编译为render函数,看一下生成的对应render函数
[JavaScript] 纯文本查看 复制代码 <template>
<div class="parent">
<keep-alive>
<coma v-if="visible"></coma>
<comb v-else></comb>
</keep-alive>
</div>
</template>
<script>
(function anonymous() {
with(this) {
return _c('div', {
staticClass: "parent"
}, [
_c('keep-alive', [(visibility) ? _c('coma') : _c('comb')], 1),
_c('button', {
on: {
"click": change
}
}, [_v("change")])], 1)
}
})
</script>
可以看到生成的render函数中有关keep-alive的生成过程
[JavaScript] 纯文本查看 复制代码 _c('keep-alive', [(visibility) ? _c('coma') : _c('comb')], 1),
在keep-alive中先调用了_c('coma'),所以才能访问到到子组件的componentOptions,具体的_c是在vdom/create-element.js中定义,他判断是生成组件vnode还是其他的。
更改data,触发patch在首次渲染的时候,我们更改coma中的input的值,看当visible再次更改为true的时候,input是否会记住先前的值。因为更改了visible的值后,会重新执行这段代码
[JavaScript] 纯文本查看 复制代码 updateComponent = () => {
vm._update(vm._render())
}
所以就会重新执行keep-alive的render函数,因为在首次渲染的时候已经把数据存入到cache中,所以这次数据直接从cache中获取执行。
[JavaScript] 纯文本查看 复制代码 vnode.componentInstance = cache[key].componentInstance
在首次渲染的时候提到当key值不存在的时候会先将子组件的vnode缓存起来,如果通过打断点的方式可以看到首次渲染的时候componentInstance为undefined,componentInstance实际是在patch过程中调用组件的init钩子才生成的,那么为什么这个时候能拿到呢,这里通过一个例子来进行讲解例如有下面例子
[JavaScript] 纯文本查看 复制代码 a = {
b: 1
}
c = a;
a.b = 5;
console.log(c.b) // 5
object是引用类型,所以原对象发生更改的时候引用的地方也会发生改变
那么就把先前的状态信息重新赋值给了coma,然后为什么赋值给了coma,coma的就不会执行组件的创建过程呢,看patch的代码,当执行到createComponent的时候,因为coma为组件,就会执行组件相关的逻辑
[JavaScript] 纯文本查看 复制代码 // core/vdom/patch.js
function createComponent(vnode, insertedVnodeQueue, parentElm, refELm) {
let i = vnode.data;
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false);
}
}
}
// core/vdom/create-component
init(vnode) {
if (vnode.componentInstance &&
!vnode.componentInstance._isDetroyed &&
vnode.data.keepAlive) {
const mountedNode: any = node;
componentVnodeHooks.prepatch(mountedNode, mountedNode)
} else {
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(vnode.elm)
}
}
因为vnode.componentInstance在keep-alive已经进行了重新赋值,所以并且keepAlive为true,所以只会执行prepatch,所以created、mounted钩子都不会执行。
keep-alive本身创建和patch过程在core/instance/render中,可以看到updateComponent的定义
[JavaScript] 纯文本查看 复制代码 updateComponent = () => {
vm._update(vm._render())
}
所以首先调用keep-alive的render函数生成vnode,然后调用vm._update执行patch操作,那么keep-alive和普通组件在首次创建的时候和patch过程中有什么差异呢?
首次渲染不管keep-alive是不是抽象组件,他终究是个也是个组件,所以也会执行组件相应的逻辑,在首次渲染的时候执行patch操作,执行到core/vdom/patch中
[JavaScript] 纯文本查看 复制代码 function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
}
}
因为是首次渲染所以componentInstance并不存在,所以只执行了init钩子,init的具体作用就是创建子组件实例。
但keep-alive毕竟是抽象组件,那抽象组件和正常组件区别体现在哪儿呢?在core/instance/lifecycle中可以看到,不是抽象组件的时候才会往父组件中加入本身,,并且子组件也不会往抽象组件$children中加入自己。这个函数又是在vm._init中进行调用的
[JavaScript] 纯文本查看 复制代码 let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
更改数据后patch过程结合上面的例子,当visible发生更改的时候,会影响到keep-alive组件吗,在patch的那片文章提到过当data中的值发生改变的时候,会触发updateComponent
[JavaScript] 纯文本查看 复制代码 updateComponent = () => {
vm._update(vm._render())
}
就会重新执行keep-alive的render函数,重新执行根组件的patch过程,具体的原理课参照Vue 源码patch过程详解,这里就直接执行了keep-alive组件的prepatch钩子
文章转载自:https://juejin.im/post/5e1ed2635188254c46131aaf
|