{
render: function render () {
var slot = this.$slots.default;
/**
* getFirstComponentChild 找到第一个组件节点
*/
var vnode = getFirstComponentChild(slot);
var componentOptions = vnode && vnode.componentOptions;
if (componentOptions) {
// check pattern
var name = getComponentName(componentOptions);
var ref = this;
var include = ref.include;
var exclude = ref.exclude;
/**
* 不匹配直接返回 vnode
*/
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
var ref$1 = this;
var cache = ref$1.cache;
var keys = ref$1.keys;
/**
* 优先使用 key 属性做为缓存的 keys,否则使用 cid + tag 拼接做为 keys
*/
var key = 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;
if (cache[key]) {
/**
* 命中缓存,直接将缓存的 vnode 上的 instance 赋给新 vnode
* 接着更新当前组件的调用顺序
*/
vnode.componentInstance = cache[key].componentInstance;
// make current key freshest
remove(keys, key);
keys.push(key);
} else {
/**
* 没有命中缓存,缓存 vnode
* LRU 最近最少使用 根据 max 更新缓存 map
*/
cache[key] = vnode;
keys.push(key);
// prune oldest entry
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode);
}
}
/**
* 打上 keepAlive 标记
* 在 prepatch 钩子中用到
*/
vnode.data.keepAlive = true;
}
/**
* keep-alive 最后无论是否命中缓存都需要返回 vnode。
* 区别在于 vnode 上的 componentInstance 和 keepAlive 属性。
* 初次渲染只有 keepAlive 属性
* 重新渲染则 componentInstance 和 keepAlive 都存在。
*/
return vnode || (slot && slot[0])
}
}
大致流程如下:
首先是获取 keep-alive 下插槽的内容,也就是 keep-alive 需要渲染的子组件。
function getFirstComponentChild (children) {
if (Array.isArray(children)) {
for (var i = 0; i < children.length; i++) {
var c = children[i];
// 组件实例存在,则返回,理论上返回第一个组件vnode
if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {
return c
}
}
}
}
keep-alive 本质上只是存缓存和拿缓存的过程。
首先看下 keep-alive 的 render 函数。
大致流程如下:
首先是获取 keep-alive 下插槽的内容,也就是 keep-alive 需要渲染的子组件。
然后判断组件是否满足缓存的匹配条件,如果不满足直接返回 vnode,不做任何处理,此时组件会进入正常的挂载阶段。
如果是重新渲染,则将缓存的实例赋给新的 vnode;如果是初次渲染,缓存 vnode
将已经缓存的 vnode 打上标记,并将 vnode 返回
有个地方需要注意的是,vnode 上是会保存 DOM 的。
由此可知,为什么 keep-alive 需要一个 max 来限制缓存组件的数量,原因就是 keep-alive 缓存的组件数据除了包括 vnode 这一描述对象外,还保留着真实的 DOM 节点。
初次渲染
初次渲染和普通的流程一致,只是在 keep-alive 内部缓存了 vnode,
vnode.data.keepAlive = true
。唯一需要注意的就是在
patch
最后调用了invokeInsertHook
函数,执行了insert
钩子,在执行callHook(componentInstance, 'mounted');
后,keepAlive
的判断为true
,会额外进入activateChildComponent
函数,调用callHook(vm, 'activated');
。初次渲染的
activated
钩子直接在这里调用,重新渲染的activated
钩子是queueActivatedComponent
放进数组中,最后在flushSchedulerQueue
中调用。重新渲染
重新渲染就有不同了,假设一个案例如下:
首先渲染 A 组件,然后渲染 B 组件,这两次都是初次渲染,所以 keep-alive 中缓存了 A,B 组件的 vnode。
接着再次切换为 A 组件,首先触发的是
app.vue
的更新,也就是会执行app.vue
的render
函数,进入patch
过程。app.vue
新旧 vnode diff 时,会发现 keep-alive 节点没有变化,所以会进patchVnode
函数中,这个函数里面最重要的是会执行prepatch
钩子函数,这个钩子函数会更新组件的 props、listeners、children 等。updateChildComponent
流程大致如下:对于 keep-alive 来说,必有 children,所以通过 $forceUpdate 进入 keep-alive 的 render 函数中,此时命中缓存,返回的 A 组件 vnode 上带有缓存的 componentInstance 和 keepAlive 标记。
进入 keep-alive 的
patch
过程,新旧 vnode 一个是 A 组件,一个是 B 组件,所以是根据新 vnode 生成 DOM 再替换旧 vnode 的 DOM 这一个过程。createElm → createComponent
此时,
isReactivated
的值是true
,进入init
钩子。这时候会进第一个分支逻辑,执行 prepatch 去更新 vnode 上保存的组件实例,而不是走新建实例、挂载的流程。
接着回到
patch
函数中。大概是这么个流程,先 invokeDestroyHook 调用旧 vnode 的 destroy 钩子,因为 keepAlive 的关系,不会真的销毁而是
callHook(vm, 'deactivated');
接着的
invokeInsertHook
调用新 vnode 的 insert 钩子,向activatedChildren
队列中加入当前实例,最后在flushSchedulerQueue
中遍历调用callHook(vm, 'activated');