1.vue的渲染原理
- 通过.vue模版文件转换为AST抽象语法树
- AST转换成render函数
- render函数生成vnode 虚拟dom
- 通过patch比较当前真实与虚拟dom差异,做dom操作更新成vnode需要的内容。
2.keep-alive定义
- 是一个vue的全局组件,也是包含生命周期方法,
- keep-alive被定义为一个抽象的vnode 不做渲染,不出现在父组件上。
- 他主要是用来包裹标签内的元素,使内部的节点缓存起来,组件离开时候不销毁。
3.keep-alive使用方法
<keep-alive>
<router-view></router-view>
</keep-alive>
include:可传字符串、正则表达式、数组,名称匹配成功的组件会被缓存exclude:可传字符串、正则表达式、数组,名称匹配成功的组件不会被缓存max:可传数字,限制缓存组件的最大数量
官方使用说明:
cn.vuejs.org/v2/guide/co…
在线演示:
codesandbox.io/s/reverent-…
4.实例
<!DOCTYPE html>
<html>
<head>
<title>Vue Component Blog Post Example</title>
<!-- <script src="../../dist/vue.js"></script>本地源码调试 -->
<script src="https://unpkg.com/vue@2"></script>
<style>
.tab-button {
padding: 6px 10px;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
border: 1px solid #ccc;
cursor: pointer;
background: #f0f0f0;
margin-bottom: -1px;
margin-right: -1px;
}
.tab-button:hover {
background: #e0e0e0;
}
.tab-button.active {
background: #e56d6d;
}
.tab {
border: 1px solid #ccc;
padding: 10px;
}
.posts-tab {
display: flex;
}
.posts-sidebar {
max-width: 40vw;
margin: 0;
padding: 0 10px 0 0;
list-style-type: none;
border-right: 1px solid #ccc;
}
.posts-sidebar li {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
cursor: pointer;
}
.posts-sidebar li:hover {
background: #eee;
}
.posts-sidebar li.selected {
background: rgb(69, 132, 153);
}
.selected-post-container {
padding-left: 10px;
}
</style>
</head>
<body>
<div id="dynamic-component-demo">
<button
v-for="tab in tabs"
v-bind:key="tab"
v-bind:class="['tab-button', { active: currentTab === tab }]"
v-on:click="currentTab = tab"
>
{{ tab }}
</button>
<keep-alive >
<component v-bind:is="currentTabComponent" class="tab"></component>
</keep-alive>
</div>
<script>
Vue.component("aaa", {
data: function () {
return {
list: [
{
id: 1,
title: "111",
content:
"<p>111内容</p>"
},
{
id: 2,
title: "222",
content:
"<p>222内容</p>"
},
{
id: 3,
title: "333",
content:
"<p>333内容</p>"
}
],
selectedPost: null
};
},
template: `
<div class="posts-tab">
<ul class="posts-sidebar">
<li
v-for="post in list"
v-bind:key="post.id"
v-bind:class="{ selected: post === selectedPost }"
v-on:click="selectedPost = post"
>
{{ post.title }}
</li>
</ul>
<div class="selected-post-container">
<div
v-if="selectedPost"
class="selected-post"
>
<h3>{{ selectedPost.title }}</h3>
<div v-html="selectedPost.content"></div>
</div>
<strong v-else>
Click on a blog title to the left to view it.
</strong>
</div>
</div>
`
});
Vue.component("bbb", {
template: "<div>bbb内容</div>"
});
Vue.component("ccc", {
template: "<div>ccc内容</div>"
});
new Vue({
el: "#dynamic-component-demo",
data: {
currentTab: "aaa",
tabs: ["aaa", "bbb", "ccc"]
},
computed: {
currentTabComponent: function () {
return this.currentTab.toLowerCase();
}
}
});
</script>
</body>
</html>
-
先点击aaa 和 333
-
再点击bbb
- 最后再点击aaa,之前的选择333 被保留下来 没有重新实例
5.keep-alive执行逻辑
- .vue模版文件 -> AST -> render函数 -> vnode (注意这里个vnode 是keepalive)
- 通过path (createPatchFunction -> createComponent -> init) 把 vnode转为真实dom
- keepalive 调用 $mounted 触发 vm._update(vm._render(), hydrating);
- 最后回调回keep-alive自己定义的 render()方法
- 找keepalive 插槽下的第一个子节点vnode,aaa。
- 同时查看是否满足缓存策略,不满足直接返回子节点vnode,待vue框架去重新生成componentInstance实例
- 如果满足才去读取和更新cache缓存(LRU),把componentInstance赋给vnode, 同时给子vnode打上 vnode.data.keepAlive = ture(为下次vue触发刷新渲染时,识别不重新调用render,直接patch和mounted)
- 第一个子组件aaa 开始实例化,render -> vnode -> $mounted 显示
- 点击第二个子组件bbb 由于全局的vue和切换数组是同一个大组件内,会触发vm._update (keepalive再次触发render方法)
- 重复步骤4, 第二个子组件bbb 开始实例化,render -> vnode -> $mounted 显示
- 这时候 点击切回第二个子组件aaa,再次调用keep-alive自己定义的 render()方法
- 但由于缓存存在aaa的vnode的实例componentInstance,直接取出赋值给aaa,不再调用render方法生成。
- 并且在componentVNodeHooks init方法通过 vnode.data.keepAlive 判断,跳过render,直接patch。
关键区分缓存vnode 读取的判断地方
src/core/vdom/create-component.js
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive //根据keepAlive == true标识
) {
// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode) //直接转dom
} else {
//没有缓存 走正常render方法渲染生成vnode 再挂载。
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},
...
6.源码学习
vue.2.6
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)
}
//支持 字符串 1,2,3 匹配
//也支持 数组[1,2,3] 匹配
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
}
//所有:根据当前是keepalive实例,遍历所有缓存实例,拿到对应name 通过matches方法判断是否匹配,不匹配则移除缓存。
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) //当整个keepalive 销毁 同步移除所有缓存
}
},
mounted () {
this.$watch('include', val => { //前端项目动态设置include 或 exclude后,会实时通知keep-alive刷新自己的
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
//这里的key = tag + name 组合,主要为了实现LRU算法
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
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
}
return vnode || (slot && slot[0])
}
}
相关算法
LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。