首次渲染
基本使用
<div id="app">
<button @click="changeTabs('child1')">child1</button>
<button @click="changeTabs('child2')">child2</button>
<keep-alive>
<component :is="chooseTabs">
</component>
</keep-alive>
</div>
var child1 = {
template: '{{num}}
',
data() {
return {
num: 1
}
},
methods: {
add() {
this.num++
}
},
mounted() {
console.log('child1 mounted')
},
activated() {
console.log('child1 activated')
},
deactivated() {
console.log('child1 deactivated')
},
destoryed() {
console.log('child1 destoryed')
}
}
var child2 = {
template: 'child2',
mounted() {
console.log('child2 mounted')
},
activated() {
console.log('child2 activated')
},
deactivated() {
console.log('child2 deactivated')
},
destoryed() {
console.log('child2 destoryed')
}
}
var vm = new Vue({
el: '#app',
components: {
child1,
child2,
},
data() {
return {
chooseTabs: 'child1',
}
},
methods: {
changeTabs(tab) {
this.chooseTabs = tab;
}
}
})
流程分析
重新渲染组件
即执行父组件的vm._update(vm._render(), hydrating);。_render和_update分别代表两个过程,其中_render函数会根据数据的变化为组件生成新的Vnode节点,而_update最终会为新的Vnode生成真实的节点。而在生成真实节点的过程中,会利用vitrual dom的diff算法对前后vnode节点进行对比,使之尽可能少的更改真实节点
patchVnode
function patchVnode (oldVnode,vnode,insertedVnodeQueue,ownerArray,index,removeOnly) {
···
// 新vnode 执行prepatch钩子
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode);
}
···
}
var componentVNodeHooks = {
// 之前分析的init钩子
init: function() {},
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
);
},
}
function updateChildComponent() {
// 更新旧的状态,不分析这个过程
···
// 迫使实例重新渲染。
vm.$forceUpdate();
}
$forceUpdate
Vue.prototype.$forceUpdate = function () {
var vm = this;
if (vm._watcher) {
vm._watcher.update();
}
};
重用缓存组件
// keepalive组件选项
var keepAlive = {
name: 'keep-alive',
abstract: true,
render: function render () {
// 拿到keep-alive下插槽的值
var slot = this.$slots.default;
// 第一个vnode节点
var vnode = getFirstComponentChild(slot);
// 拿到第一个组件实例
var componentOptions = vnode && vnode.componentOptions;
// keep-alive的第一个子组件实例存在
if (componentOptions) {
// check pattern
//拿到第一个vnode节点的name
var name = getComponentName(componentOptions);
var ref = this;
var include = ref.include;
var exclude = ref.exclude;
// 通过判断子组件是否满足缓存匹配
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;
var 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 {
// 初次渲染时,将vnode缓存
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])
}
}
真实节点的替换
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
// vnode为缓存的vnode
var i = vnode.data;
if (isDef(i)) {
// 此时isReactivated为true
var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */);
}
if (isDef(vnode.componentInstance)) {
// 其中一个作用是保留真实dom到vnode中
initComponent(vnode, insertedVnodeQueue);
insert(parentElm, vnode.elm, refElm);
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
}
return true
}
}
}
var componentVNodeHooks = {
init: function init (vnode, hydrating) {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// 当有keepAlive标志时,执行prepatch钩子
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);
}
},
}
缓存优化策略
程序的内存空间是有限的,所以我们无法无节制的对数据进行存储,这时候需要有策略去淘汰不那么重要的数据,保持最大数据存储量的一致。这种类型的策略称为缓存优化策略,根据淘汰的机制不同,常用的有以下三类。
keep-alive在缓存时的优化处理,使用了LRU的缓存策略
var keepAlive = {
render: function() {
···
if (cache[key]) {
//取缓存时进行LRU的移动策略
vnode.componentInstance = cache[key].componentInstance;
//删除key并放到数组尾部
remove(keys, key);
keys.push(key);
} else {
//首次渲染放缓存时,使用LRU的删除策略
cache[key] = vnode;
keys.push(key);
// 当缓存溢出时,会删除数组的头部数据,即将最不频繁使用的数据移除
// 删除缓存时会把keys[0]代表的组件删除,由于之前的处理,最近被访问到的元素会位于数组的尾部,所以头部的数据往往是最少访问的
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode);
}
}
}
}
function remove (arr, item) {
if (arr.length) {
var index = arr.indexOf(item);
if (index > -1) {
return arr.splice(index, 1)
}
}
}
function pruneCacheEntry (cache,key,keys,current) {
var cached$$1 = cache[key];
// 销毁实例
if (cached$$1 && (!current || cached$$1.tag !== current.tag)) {
cached$$1.componentInstance.$destroy();
}
cache[key] = null;
remove(keys, key);
}
示例
keys = ['child1', 'child2']
cache = {
child1: child1Vnode,
child2: child2Vnode
}
再次访问到child1时
keys = ['child2', 'child1']
cache = {
child1: child1Vnode,
child2: child2Vnode
}
访问到child3时
keys = ['child1', 'child3']
cache = {
child1: child1Vnode,
child3: child3Vnode
}
生命周期
deactivated钩子
function patch(···) {
// 分析过的patchVnode过程
// 销毁旧节点
if (isDef(parentElm)) {
removeVnodes(parentElm, [oldVnode], 0, 0);
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode);
}
}
function removeVnodes (parentElm, vnodes, startIdx, endIdx) {
// startIdx,endIdx都为0
for (; startIdx <= endIdx; ++startIdx) {
// ch 会拿到需要销毁的组件
var ch = vnodes[startIdx];
if (isDef(ch)) {
if (isDef(ch.tag)) {
// 真实节点的移除操作
removeAndInvokeRemoveHook(ch);
invokeDestroyHook(ch);
} else { // Text node
removeNode(ch.elm);
}
}
}
}
invokeDestroyHook
function invokeDestroyHook (vnode) {
var i, j;
var data = vnode.data;
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.destroy)) { i(vnode); }
// 执行组件内部destroy钩子
for (i = 0; i < cbs.destroy.length; ++i) { cbs.destroy[i](vnode); }
}
// 如果组件存在子组件,则遍历子组件去递归调用invokeDestoryHook执行钩子
if (isDef(i = vnode.children)) {
for (j = 0; j < vnode.children.length; ++j) {
invokeDestroyHook(vnode.children[j]);
}
}
}
组件见内置的destroy钩子
var componentVNodeHooks = {
destroy: function destroy (vnode) {
// 组件实例
var componentInstance = vnode.componentInstance;
// 如果实例还未被销毁
if (!componentInstance._isDestroyed) {
// 不是keep-alive组件则执行销毁操作
if (!vnode.data.keepAlive) {
componentInstance.$destroy();
} else {
// 如果是已经缓存的组件
deactivateChildComponent(componentInstance, true /* direct */);
}
}
}
}
deactivateChildComponent
function deactivateChildComponent (vm, direct) {
if (direct) {
// 是用来标记这个被打上停用标签的组件是否是最顶层的组件。
vm._directInactive = true;
if (isInInactiveTree(vm)) {
return
}
}
if (!vm._inactive) {
// 已经被停用
vm._inactive = true;
// 对子组件同样会执行停用处理
for (var i = 0; i < vm.$children.length; i++) {
deactivateChildComponent(vm.$children[i]);
}
// 最终调用deactivated钩子
callHook(vm, 'deactivated');
}
}
activated钩子
function patch(···) {
// patchVnode过程
// 销毁旧节点
{
if (isDef(parentElm)) {
removeVnodes(parentElm, [oldVnode], 0, 0);
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode);
}
}
// 执行组件内部的insert钩子
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
}
function invokeInsertHook (vnode, queue, initial) {
// delay insert hooks for component root nodes, invoke them after the
// 当节点已经被插入时,会延迟执行insert钩子
if (isTrue(initial) && isDef(vnode.parent)) {
vnode.parent.data.pendingInsert = queue;
} else {
for (var i = 0; i < queue.length; ++i) {
queue[i].data.hook.insert(queue[i]);
}
}
}
insert
// 组件内部自带钩子
var componentVNodeHooks = {
insert: function insert (vnode) {
var context = vnode.context;
var componentInstance = vnode.componentInstance;
// 实例已经被挂载
if (!componentInstance._isMounted) {
componentInstance._isMounted = true;
callHook(componentInstance, 'mounted');
}
if (vnode.data.keepAlive) {
if (context._isMounted) {
// vue-router#1212
// During updates, a kept-alive component's child components may
// change, so directly walking the tree here may call activated hooks
// on incorrect children. Instead we push them into a queue which will
// be processed after the whole patch process ended.
queueActivatedComponent(componentInstance);
} else {
activateChildComponent(componentInstance, true /* direct */);
}
}
},
}
activateChildComponent
function activateChildComponent (vm, direct) {
if (direct) {
vm._directInactive = false;
if (isInInactiveTree(vm)) {
return
}
} else if (vm._directInactive) {
return
}
if (vm._inactive || vm._inactive === null) {
vm._inactive = false;
for (var i = 0; i < vm.$children.length; i++) {
activateChildComponent(vm.$children[i]);
}
callHook(vm, 'activated');
}
}