13.6 流程分析
13.6.1 重新渲染组件
再次渲染的流程从数据改变说起,在这个例子中,动态组件中chooseTabs
数据的变化会引起依赖派发更新的过程(这个系列有三篇文章详细介绍了vue响应式系统的底层实现,感兴趣的同学可以借鉴)。简单来说,chooseTabs
这个数据在初始化阶段会收集使用到该数据的相关依赖。当数据发生改变时,收集过的依赖会进行派发更新操作。
其中,父组件中负责实例挂载的过程作为依赖会被执行,即执行父组件的vm._update(vm._render(), hydrating);
。_render
和_update
分别代表两个过程,其中_render
函数会根据数据的变化为组件生成新的Vnode
节点,而_update
最终会为新的Vnode
生成真实的节点。而在生成真实节点的过程中,会利用vitrual dom
的diff
算法对前后vnode
节点进行对比,使之尽可能少的更改真实节点,这一部分内容可以回顾深入剖析Vue源码 - 来,跟我一起实现diff算法!,里面详细阐述了利用diff
算法进行节点差异对比的思路。
patch
是新旧Vnode
节点对比的过程,而patchVnode
是其中核心的步骤,我们忽略patchVnode
其他的流程,关注到其中对子组件执行prepatch
钩子的过程中。
function patchVnode (oldVnode,vnode,insertedVnodeQueue,ownerArray,index,removeOnly) {
···
// 新vnode 执行prepatch钩子
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode);
}
···
}
执行prepatch
钩子时会拿到新旧组件的实例并执行updateChildComponent
函数。而updateChildComponent
会对针对新的组件实例对旧实例进行状态的更新,包括props,listeners
等,最终会调用vue
提供的全局vm.$forceUpdate()
方法进行实例的重新渲染。
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
做了什么操作。$forceUpdate
是源码对外暴露的一个api,他们迫使Vue
实例重新渲染,本质上是执行实例所收集的依赖,在例子中watcher
对应的是keep-alive
的vm._update(vm._render(), hydrating);
过程。
Vue.prototype.$forceUpdate = function () {
var vm = this;
if (vm._watcher) {
vm._watcher.update();
}
};
13.6.2 重用缓存组件
由于vm.$forceUpdate()
会强迫keep-alive
组件进行重新渲染,因此keep-alive
组件会再一次执行render
过程。这一次由于第一次对vnode
的缓存,keep-alive
在实例的cache
对象中找到了缓存的组件。
// 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])
}
}
render
函数前面逻辑可以参考前一篇文章,由于cache
对象中存储了再次使用的vnode
对象,所以直接通过cache[key]
取出缓存的组件实例并赋值给vnode
的componentInstance
属性。可能在读到这里的时候,会对源码中keys
这个数组的作用,以及pruneCacheEntry
的功能有疑惑,这里我们放到文章末尾讲缓存优化策略时解答。
13.6.3 真实节点的替换
执行了keep-alive
组件的_render
过程,接下来是_update
产生真实的节点,同样的,keep-alive
下有child1
子组件,所以_update
过程会调用createComponent
递归创建子组件vnode
,这个过程在初次渲染时也有分析过,我们可以对比一下,再次渲染时流程有哪些不同。
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
}
}
}
此时的vnode
是缓存取出的子组件vnode
,并且由于在第一次渲染时对组件进行了标记vnode.data.keepAlive = true;
,所以isReactivated
的值为true
,i.init
依旧会执行子组件的初始化过程。但是这个过程由于有缓存,所以执行过程也不完全相同。
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);
}
},
}
显然因为有keepAlive
的标志,所以子组件不再走挂载流程,只是执行prepatch
钩子对组件状态进行更新。并且很好的利用了缓存vnode
之前保留的真实节点进行节点的替换。