上一节我们研究了从 render 函数生成 vnode 的流程,这是通过 createElement 函数去实现的,那么在这其中遇到组件标签时,它实际又是调用 createComponent 函数去创建组件 vnode,或许叫组件占位 vnode 更准确,因为它并非渲染时 DOM 元素的 vnode。我们来看看它的内部实现。
createComponent 函数是在上一节介绍的 _createElement 函数中被调用的,如下代码。传入 _createElement 的参数 tag 为普通 HTML 元素标签则创建其元素 VNode 对象,如果为组件选项或组件构造函数则调用 createComponent 函数创建组件占位 VNode 对象。
file: /src/core/vdom/create-element.js
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
// 响应式的 data 直接返回空注释的 vnode
if (isDef(data) && isDef((data: any).__ob__)) {
...
return createEmptyVNode()
}
// 如有 is 属性则把其值当组件名赋给 tag
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
if (!tag) {
// 无tag 则创建空注释 vnode
return createEmptyVNode()
}
...
// 扁平化 children
...
let vnode, ns
// tag 为字符串
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
...
// 创建 HTML\svg 标签的 vnode
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// tag 为组件,调用 createComponent创建 vnode
vnode = createComponent(Ctor, data, context, children, tag) // <-----
} else {
// 除了上面两种情况的 vnode
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
// tag 为 组件选项 或 组件构造函数,调用 createComponent创建 vnode
vnode = createComponent(tag, data, context, children) // <------
}
...
}
我们来看看 createComponent 这个函数的源码
file: /src/core/vdom/create-component.js
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData, context: Component,
children: ?Array<VNode>, tag?: string
): VNode | Array<VNode> | void {
// 1、处理生成组件构造函数 ================================
// Ctor 参数为空直接退出; baseCtor 即是 Vue
if (isUndef(Ctor)) { return }
const baseCtor = context.$options._base
// 如果 Ctor 是选项对象, Vue.extend 扩展为 Vue 子类
if (isObject(Ctor)) { Ctor = baseCtor.extend(Ctor) }
// Ctor 如不是 组件构造函数 则退出
if (typeof Ctor !== 'function') { return }
// 处理异步组件,生成异步组件构造函数 Ctor
let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
if (Ctor === undefined) {
return createAsyncPlaceholder(asyncFactory,data,context,
children,tag)
}
}
// 2、处理data\props\事件监听器 ==========================
data = data || {}
// 在创建组件构造函数后 应用全局混合时 解析构造函数选项
// 更新后的父类 options 合并到 Ctor.options
resolveConstructorOptions(Ctor)
// 将组件 v-model 数据转换为 props 和 events
if (isDef(data.model)) {
transformModel(Ctor.options, data)
}
// 提取 props的值
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
// 函数式组件则调用 createFunctionalComponent创建 vnode
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor, propsData, data, context, children)
}
// on 监听器 赋 listeners
const listeners = data.on
// 原生事件监听器 赋 data.on
data.on = data.nativeOn
// 抽象组件(如 keep-alive)除了 props、listeners、slot 外不保留任何东西
if (isTrue(Ctor.options.abstract)) {
const slot = data.slot
data = {}
if (slot) { data.slot = slot }
}
// 3、安装组件钩子函数 ==============================
installComponentHooks(data)
// 4、创建组件 占位vnode ==============================
const name = Ctor.options.name || tag
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
...
return vnode
}
如上代码中注释所述,createComponent 处理大致可分为 4 步:
1、处理组件构造函数,传入选项对象则 Vue.extend 扩展生成,异步组件则调用异步生成,处理后 Ctor 必须是构造函数。
2、更新合并组件 options,处理组件 data\props 等,这个过程中还会判断是不是函数式组件,是则直接创建 vnode 并返回,然后中止后续执行。
3、安装组件钩子函数,钩子函数存于 data.hook 中,用于后续实例化组件和渲染等等场合,其中函数类似:
var componentVNodeHooks = {
init: function init (vnode, hydrating) { ... },
prepatch: function prepatch (oldVnode, vnode) { ... },
insert: function insert (vnode) { ... },
destroy: function destroy (vnode) { ... }
};
4、创建组件的占位 vnode,传入前面处理过的一系列数据创建占位 vnode 对象,其中 tag 为固定拼接的字符串,一般类似于 vue-component-(数字)-(组件名),children 为空,然后返回该占位 vnode 对象。
下图简单列举这两类 VNode 对象的部分不同属性:
如上所述,createComponent 函数先处理组件的构造函数,然后整理相关数据,再安装钩子函数,最后创建组件占位 vnode 对象。值得注意的是,这个 vnode 对象并不是实际渲染时 DOM 元素对应的 vnode,正如它的名字一样,它更像是用来占位的。其中的钩子函数在渲染时会创建组件实例,这个后面我们继续研究学习。