开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 1 天,点击查看活动详情
组件的虚拟 DOM
在前面的分析中,我们分析了 render 函数转换成虚拟 DOM 的过程,对于遇到组件的情况,我们还没有分析。在 render 函数转换成虚拟 DOM 是,会对传入的标签字符串进行判断,如果是平台内置的标签,则创建虚拟 DOM;如果是已经注册的组件标签,则创建组件的虚拟 DOM。
创建虚拟 DOM 基本流程
if (config.isReservedTag(tag)) {
// platform built-in elements
// 内置标签,直接创建虚拟 DOM
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
// 已经注册的自定义组件,创建组件类型的 VNode
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// unknown or unlisted namespaced elements
// check at runtime because it may get assigned a namespace when its
// parent normalizes children
vnode = new VNode(
tag, data, children,
undefined, undefined, contexts
)
}
从上面的代码中, 如果为组件标签,则调用 createComponent 方法来创建组件的虚拟 DOM。 Vue 中通过 resolveAsset 方法来判断组件是否已经注册,来看看 resolveAsset 方法的实现。
function resolveAsset (
options: Object,
type: string,
id: string,
warnMissing?: boolean
): any {
/* istanbul ignore if */
if (typeof id !== 'string') {
return
}
const assets = options[type]
// 这里多个分支对组件标签的多种形式进行判断(大小写、驼峰)
if (hasOwn(assets, id)) return assets[id]
const camelizedId = camelize(id)
if (hasOwn(assets, camelizedId)) return assets[camelizedId]
const PascalCaseId = capitalize(camelizedId)
if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId]
// fallback to prototype chain
const res = assets[id] || assets[camelizedId] || assets[PascalCaseId]
if (process.env.NODE_ENV !== 'production' && warnMissing && !res) {
warn(
'Failed to resolve ' + type.slice(0, -1) + ': ' + id,
options
)
}
// 最后返回子类的构造器函数
return res
}
通过上方代码可以发现,判断组件是否已经注册过程中,需要对标签的不同命名规范进行判断,最后返回子类构造器。在判定标签为组件标签之后,调用 createComponent 方法来创建组件的虚拟 DOM。我们来看一下 createComponent 方法的主要逻辑
function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
if (isUndef(Ctor)) {
return
}
/**
* 这里的 baseCtor 为 Vue 构造函数
*/
const baseCtor = context.$options._base
/**
* 如果传入的构造器时一个对象
* (一般情况下,创建一个 Vue 组件, export default 导出的都是一个对象,这是局部注册组件的情况,对于全局注册的组件, Ctor 是一个子类构造器函数)
* 使用 extend 方法构造一个 Vue 的子类
* extend 方法内部使用的是原型继承
*/
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
/// 合并构造器配置
resolveConstructorOptions(Ctor)
// 安装组件钩子函数
installComponentHooks(data)
// 创建组件的虚拟 DOM , 名称以 vue-component 开头
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
}
从上面的代码可以看出, 创建组件的虚拟 DOM 两个关键步骤就是合并选项和安装组件钩子函数。合并选项与 Vue 实例初始化是选项合并是基本一致的。我们来看下安装组件钩子函数做了些什么。
// 将 componentVNodeHooks 钩子函数合并到组件 data.hooks 中
function installComponentHooks (data: VNodeData) {
const hooks = data.hook || (data.hook = {})
for (let i = 0; i < hooksToMerge.length; i++) {
const key = hooksToMerge[i]
const existing = hooks[key]
const toMerge = componentVNodeHooks[key]
if (existing !== toMerge && !(existing && existing._merged)) {
// 如果钩子函数存在且没有合并过,则进行合并
hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
}
}
}
function mergeHook (f1: any, f2: any): Function {
const merged = (a, b) => {
// flow complains about extra args which is why we use any
f1(a, b)
f2(a, b)
}
merged._merged = true
return merged
}
至此, Vue 组件虚拟 DOM 创建的基本流程就已经分析完了。总结一下整体步骤
-
判断标签为组件标签时,先通过判断时局部注册的组件还是全局注册的组件,对于局部注册的组件,此时的构造器为一个对象,需要通过 extend 方法将其转换成构造器函数
-
合并选项配置,这里合并选项与根实例合并选项流程是一致的
-
安装组件的钩子函数
-
最后创建组件的虚拟 DOM