前言
Vue提供一些内部组件用于相关处理,例如keep-alive、transition等。不同的内部组件有不同的处理方式,通过Vue官网对不同内部组件的描述信息得到一些重要说明,具体如下:
- transition:不会额外渲染DOM元素,也不会出现在组件层级中
- transition-group:会渲染额外DOM元素,默认是span
- keep-alive:不会额外渲染DOM元素,也不会出现在组件层级中
本文主要通过源码层次来看Vue对于内部组件的处理,这里不具体看某一个内部组件的具体处理,而且从Vue整个过程看内部组件的生成,实际上就是看三种情况的处理:
- 内部组件
- 实例组件
- HTML和SVG标签
Vue整体构建过程
在看Vue内部组件的处理前先看看Vue整体的构建过程,依据构建过程中一些关键性的逻辑点去看内部组件的一些处理。
上面是Vue构建逻辑的大概逻辑,实际上通过阅读源码可知内部组件与其他组件和标签一些关键点的区别主要有2处(实际上还有其他点的区别,这里只关注第一次生成实例过程):
- $options生成
- render函数调用中$createElement生成组件中逻辑区别
$options生成的处理
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation(优化内部组件实例化)
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options);
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
}
内部组件的选项并没有需要特殊处理的选项,所以Vue源码优化了内部组件的options处理,调用initInternalComponent来处理。
$createElement中组件生成
从之前的构建过程逻辑知道_render函数主要就是执行render函数,而render函数主要就是调用$createElement来生成虚拟DOM,实际上createElement主要的逻辑如下:
if (config.isReservedTag(tag)) {
vnode = new VNode();
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
vnode = createComponent(Ctor, data, context, children, tag);
} else {
vnode = new VNode();
}
isReservedTag方法就是判断是否是HTML标签和SVG标签。如果是浏览器可识别标签直接调用VNode构造函数创建虚拟节点,而对于自定义组件(外部组件和内部组件)会调用createComponent函数来处理。
这里注意2点:createComponent 和 resolveAsset这2个函数,而resolveAsset是返回对应的构造函数。
resolveAsset函数
对于组件,$createElement函数中存在下面这个逻辑:
resolveAsset(context.$options, 'components', tag)
实际上就是取Vue.options.components中对应tag对应的值。首先需要明确的是components中是什么,components实际上是所有组件的集合包含内部组件keep-alive、transition等。
通过Vue源码可知内部组件添加到components中的逻辑如下:
// keep-alive
extend(Vue.options.components, builtInComponents);
// transition、transition-group
extend(Vue.options.components, platformComponents);
上面逻辑实际上添加到components中内部组件都是对象,这里看下Transition内部组件:
var Transition = {
name: 'transition',
props: transitionProps,
abstract: true,
render: function render (h) {
// 相关逻辑
}
};
abstract属性表示其为抽象组件,从上面可以看出所谓内部组件从形式上有2点:
- 抽象(当然transition-group是非抽象的)
- render已经定义了
createComponent函数
该函数主要关注的逻辑有3点:
-
isObject(Ctor):判断构造函数是否是对象,实际上就是内部组件
第一点逻辑实际就是处理内部组件的,内部组件传入的构造函数是对象形式的。
需要调用Vue.extend构建函数形式的构造函数 -
installComponentHooks:注册虚拟节点对象的hook
var componentVNodeHooks = {
init: function() {},
prepatch: function() {},
insert: function() {},
destroy: function() {}
} -
new VNode:生成虚拟对象
该函数就是针对组件类型生成虚拟节点,其中很重要的一点就是注册相关的hook钩子。组件钩子在后续vnode转真实DOM各个节点会被执行。
通过之前Vue构建过程中处理逻辑,在_update实例方法中会调用VNode init钩子。init钩子函数逻辑如下:
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
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);
}
这里会调用createComponentInstanceForVnode和$mount来实现组件实例生成和组件的挂载。
虚拟节点hook init中$mount和_init实例方法中$mount
组件对应的虚拟节点必然存在一系列的hook钩子,而init钩子函数逻辑主要就是创建组件实例和挂载,在Vue.prototype._init实例方法中也存在实例挂载的逻辑,那这两处逻辑存在什么区别呢?
- _init实例方法中$mount执行是针对挂载点标签的即根节点的实例处理
- 虚拟节点hook init函数是针对子组件的实例创建和组件挂载的