0%

Vue keep-alive 原理

keep-alive 组件是官方自带的用于缓存第一个子组件的组件。组件在 created 钩子内创建两个变量 cache keys 用于存储缓存的子组件和子组件对应缓存列表内的 key,由于 render 函数是在 beforeMount 和 mounted 钩子之间执行,且所有子组件的生命周期会在父组件的 mounted 钩子之前完成,所有在 render 函数内获取第一个子组件是否存在在缓存列表中,如果存在则用缓存到的组件实例 componentInstance 去替换新生成的 子组件 vnode 的实例,否则创建变量 vnodeToCache keyToCache,存储 vode 和 vnode 对应的 key,在 mounted 钩子内执行缓存方法将子组件放入缓存列表内,并监听 include exclude 去判断是否要缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
// 判断值是否定义
function isDef(v) {
return v !== undefined && v !== null
}

function isAsyncPlaceholder(node) {
return node.isComment && node.asyncFactory
}

// 返回所有子元素中第一个子组件
function getFirstComponentChild(children) {
if (Array.isArray(children)) {
for (let i = 0; i < children.length; i++) {
const c = children[i]
if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {
return c
}
}
}
}

// 删除数组元素
function remove(arr, item) {
if (arr.length) {
const index = arr.indexOf(item)
if (index > -1) {
return arr.splice(index, 1)
}
}
}

// 获取组件名
function getComponentName(opts) {
return opts && (opts.Ctor.options.name || opts.tag)
}

// 判断值是否存在 [String, Array]
function matches(pattern, name) {
if (Array.isArray(pattern)) {
return pattern.indexOf(name) > -1
} else if (typeof pattern === 'string') {
return pattern.split(',').indexOf(name) > -1
}

return false
}


function pruneCache(keepAliveInstance, filter) {
const { cache, keys, _vnode } = keepAliveInstance
for (const key in cache) {
const entry = cache[key]
console.log('entry', entry)
if (entry) {
const name = entry.name
if (name && !filter(name)) {
pruneCacheEntry(cache, key, keys, _vnode)
}
}
}
}

function pruneCacheEntry(cache, key, keys, current) {
const entry = cache[key]
if (entry && (!current || entry.tag !== current.tag)) {
entry.componentInstance.$destroy()
}
cache[key] = null
remove(keys, key)
}

const patternTypes = [String, RegExp, Array]

export default {
name: 'keep-alive',
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number],
},
data() {
return {
// cache: Object.create(null),
// keys: [],
}
},
methods: {
cacheVNode() {
const { cache, keys, vnodeToCache, keyToCache } = this

if (vnodeToCache) {
const { tag, componentInstance, componentOptions } = vnodeToCache
cache[keyToCache] = {
name: getComponentName(componentOptions),
tag,
componentInstance,
}
keys.push(keyToCache)
// 判断 prop max 超出则删除缓存列表中的第一个
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
this.vnodeToCache = null
}
},
},
beforeCreate() {
console.log('keep-alive vue beforeCreate')
},
created() {
console.log('keep-alive vue created')
this.cache = Object.create(null)
console.log('cache', this.cache)
this.keys = []
},
beforeMount() {
console.log('keep-alive vue beforeMount')
},
mounted() {
// render 后缓存子节点
this.cacheVNode()
this.$watch('include', (val) => {
pruneCache(this, (name) => matches(val, name))
})
this.$watch('exclude', (val) => {
pruneCache(this, (name) => !matches(val, name))
})
console.log('keep-alive vue mounted')
console.log('keep-alive this', this)
console.log('keep-alive $vnode', this.$vnode)
console.log('keep-alive _vnode', this._vnode)
console.log('this.cache', this.cache)
console.log('this.keys', this.keys)
},
beforeUpdate() {
console.log('keep-alive vue beforeUpdate')
},
updated() {
console.log('keep-alive vue updated')
},
destroyed() {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
render() {
console.log('keep-alive vue render')
const slot = this.$slots.default
const vnode = getFirstComponentChild(slot)
const componentOptions = vnode && vnode.componentOptions
console.log('componentOptions', componentOptions)
if (componentOptions) {
const name = getComponentName(componentOptions)
const { include, exclude, cache, keys } = this

if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}

const key =
vnode.key == null
? componentOptions.Ctor.cid +
(componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
if (cache[key]) {
// 如果缓存中存在该子节点则用缓存的实例替换生成的实例
vnode.componentInstance = cache[key].componentInstance
// keys 数组去重
remove(keys, key)
keys.push(key)
} else {
// 否则将子节点赋值给 vnodeToCache, render 执行完毕后再 mounted 钩子内进行子节点缓存
this.vnodeToCache = vnode
this.keyToCache = key
}

// 在子节点 data 上添加 keepAlive 属性用于跳过其他生命周期钩子
vnode.data.keepAlive = true
}

return vnode || (slot && slot[0])
},
}