本文已参与「新人创作礼」活动,一起开启掘金创作之路。
组件是如何渲染到页面的
我之前的文章介绍了在Vue中普通的数据是如何渲染到页面的,关于这段逻辑可以点击这里,主要包括init,$mount,patch的过程:
graph TD
init --> $mount --> patch
那么组件与普通数据在执行整个流程中有什么不同呢,主要不同的点主要包括两个流程,一个是$mount过程中,创建vnode(虚拟节点)的时候,另一个过程是patch(vnode转化成真实DOM)过程,下面我们针对这两个过程进行分析:
在页面渲染过程中,如果遇到了组件节点,会进入组件的$mount(原因下文会分析),这个过程中需要生成组件vnode,会执行createElement函数,接下来我们来看看组件的createElement过程,上源码:
1.createElement(创建组件虚拟节点)
_createElement
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
if (isDef(data) && isDef((data: any).__ob__)) {
process.env.NODE_ENV !== 'production' && warn(
`Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
'Always create fresh vnode data objects in each render!',
context
)
return createEmptyVNode()
}
// object syntax in v-bind
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
// 渲染组件节点的时候,tag存在且是string类型
if (!tag) {
// in case of component :is set to falsy value
return createEmptyVNode()
}
// warn against non-primitive key
if (process.env.NODE_ENV !== 'production' &&
isDef(data) && isDef(data.key) && !isPrimitive(data.key)
) {
if (!__WEEX__ || !('@binding' in data.key)) {
warn(
'Avoid using non-primitive value as key, ' +
'use string/number value instead.',
context
)
}
}
// support single function children as default scoped slot
// 插槽相关逻辑,跳过
if (Array.isArray(children) &&
typeof children[0] === 'function'
) {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
// 这儿的逻辑是将children参数规范化,使其统一格式
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
let vnode, ns
// tag是string类型,进入逻辑
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
// 判断是否是保留标签
if (config.isReservedTag(tag)) {
// platform built-in elements
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
// 判断当前实例上的options.components中是否存在该标签,渲染组件节点的逻辑
} else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
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, context
)
}
} else {
// tag传入的不是string类型的时候直接创建组件节点
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
if (isDef(ns)) applyNS(vnode, ns)
if (isDef(data)) registerDeepBindings(data)
// 返回vnode
return vnode
} else {
return createEmptyVNode()
}
}
如果是组件节点的时候,有两种情况,第一种情况,传入的tag参数是字符串,首先会判断该字符串是否是保留标签,不是的话会执行resolveAsset(context.$options, 'components', tag)的方法,去查找该tag定义的对象:
resolveAsset
/**
* Resolve an asset.
* This function is used because child instances need access
* to assets defined in its ancestor chain.
*/
export function resolveAsset (
options: Object,
type: string,
id: string,
warnMissing?: boolean
): any {
/* istanbul ignore if */
// 没有id直接返回
if (typeof id !== 'string') {
return
}
// 获取实例的options.components
const assets = options[type]
// check local registration variations first
// 检查实例中是否存在这个构造器,有则直接返回
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
}
查找到定义的components对象后,根据拿到查找到的对象执行createComponent函数,创建vnode节点;第二种情况传入的tag参数直接就是component对象或构造器,就直接去执行createComponent函数,我们继续看下这个函数:
createComponent
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
// 如果Ctor未定义,直接返回
if (isUndef(Ctor)) {
return
}
// baseCtor 是Vue
const baseCtor = context.$options._base
// plain options object: turn it into a constructor
// 创建组件构造器,将Vue上面的components也合并了,可以参考我的另一篇文章,extend
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
// if at this stage it's not a constructor or an async component factory,
// reject.
// 组件构造器不是函数的情况下,报错
if (typeof Ctor !== 'function') {
if (process.env.NODE_ENV !== 'production') {
warn(`Invalid Component definition: ${String(Ctor)}`, context)
}
return
}
// async component
// 异步组件的逻辑,跳过
let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
if (Ctor === undefined) {
// return a placeholder node for async component, which is rendered
// as a comment node but preserves all the raw information for the node.
// the information will be used for async server-rendering and hydration.
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
}
data = data || {}
// resolve constructor options in case global mixins are applied after
// component constructor creation
resolveConstructorOptions(Ctor)
// transform component v-model data into props & events
if (isDef(data.model)) {
transformModel(Ctor.options, data)
}
// extract props
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
// functional component
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor, propsData, data, context, children)
}
// extract listeners, since these needs to be treated as
// child component listeners instead of DOM listeners
const listeners = data.on
// replace with listeners with .native modifier
// so it gets processed during parent component patch.
data.on = data.nativeOn
// 抽象组件逻辑
if (isTrue(Ctor.options.abstract)) {
// abstract components do not keep anything
// other than props & listeners & slot
// work around flow
const slot = data.slot
data = {}
if (slot) {
data.slot = slot
}
}
// install component management hooks onto the placeholder node
// 安装钩子函数
installComponentHooks(data)
// return a placeholder vnode
// 创建占位符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
)
// Weex specific: invoke recycle-list optimized @render function for
// extracting cell-slot template.
// https://github.com/Hanks10100/weex-native-directive/tree/master/component
/* istanbul ignore if */
if (__WEEX__ && isRecyclableComponent(vnode)) {
return renderRecyclableComponentTemplate(vnode)
}
// 返回vnode
return vnode
}
createComponent函数,通过Vue.extend函数创建了一个子类构造器(关于Vue.extend的逻辑可以点击这里),这个创建的子类构造器会继承Vue构造函数上面的一些能力,同时也会使用mergeOptions函数将Vue上面的一些配置项进行并入(关于mergeOptions的逻辑可以点击这里),包括全局注册的组件(components)等,这也是全局注册的组件可以在各个组件内部使用的原因。
拥有了组件构造器之后,就可以创建组件vnode了
// return a placeholder vnode
// 创建占位符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
)
组件vnode与普通vnode有一点不同之处,名称会命名为以vue-component为开头,另外,它的第3、4、5(分别为children,text,elm)个参数传入的都是undefined,这对后面patch阶段的分析很有帮助,到了这里,我们就清楚了组件节点的vnode创建过程了,下一节,我们会继续分析,组件是如何从vnode转化成真实DOM元素的。
未完待续。。。