本篇文章是细谈 vue 系列的第六篇。看过我这个系列文章的小伙伴都知道:文章贼长,看不下去的建议先点个赞,有时间静下来慢慢看,前端交流群:731175396。以前的文章传送门如下
- 《细谈 vue 核心 - vdom 篇》
- 《细谈 vue - slot 篇》
- 《细谈 vue - transition 篇》
- 《细谈 vue - transition-group 篇》
- 《细谈 vue - 抽象组件实战篇》
用过 vue
的小伙伴肯定知道,在 vue
的开发中,component
可谓是随处可见,项目中的那一个个 .vue (SFC)
文件,可不就是一个个的组件么。
那么,既然 component
这么核心,这么重要,为何不好好来研究一波呢?
why not ?
— 鲁迅
一、组件创建
之前我们分析 vdom
的时候分析过一个函数 createElement
,与它相同的是 createComponent
,两者都是用来创建 vnode
节点的,如果是普通的 html
标签,则直接实例化一个普通的 vnode
节点,否则通过 createComponent
来创建一个 Component
类型的 vnode
节点
1、createElement
这里仅列出不同情况下 vnode
节点创建的代码
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
)
} else if ((!data || !data.pre) && 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 {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
2、createComponent
接下来,我们先看 createComponent()
的定义,具体如下
export 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
}
const baseCtor = context.$options._base
// plain options object: turn it into a constructor
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)
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
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)
}
return vnode
}
- 在其内部,第一件事情就是将构造函数
Vue
赋值给变量baseCtor
,并通过extend
将参数Ctor
进行扩展
const baseCtor = context.$options._base
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
这里我们看到 $options._base
,其实就是构造函数 Vue
// src/core/global-api/index.js
Vue.options._base = Vue
// src/core/instance/init.js
// 1. initMixin()
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// 2. initInternalComponent()
const opts = vm.$options = Object.create(vm.constructor.options)
- 其次,紧接着,判定组件是否为异步组件、函数式组件或者抽象组件。具体每种情况的处理后面我再详细分析
// 异步组件
let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
if (Ctor === undefined) {
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
}
// 函数式组件
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor, propsData, data, context, children)
}
// 抽象组件
if (isTrue(Ctor.options.abstract)) {
const slot = data.slot
data = {}
if (slot) {
data.slot = slot
}
}
- 对于组件上的事件也有相关处理,它会提取组件上的事件监听器。它需要作为子组件的监听器,而并非DOM监听器。所以需要将其替换为拥有
.native
修饰符的侦听器,让其能在父组件 patch 阶段能够得到处理
const listeners = data.on
data.on = data.nativeOn
- 然后,安装组件的钩子函数。它将
componentVNodeHooks
的钩子函数合并到data.hook
中,然后Component
类型的vnode
节点在patch
过程中会执行相关的钩子函数,如果某个时机的钩子函数已经存在,则通过mergeHook
将函数合并,即依次执行同一时机的这两个函数
installComponentHooks(data)
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
}
}
}
const hooksToMerge = Object.keys(componentVNodeHooks)
function mergeHook (f1: any, f2: any): Function {
const merged = (a, b) => {
f1(a, b)
f2(a, b)
}
merged._merged = true
return merged
}
3、componentVNodeHooks
上面的 componentVNodeHooks
则是组件初始化的时候实现的几个钩子函数,分别有 init
、prepatch
、insert
、destroy
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
const mountedNode: any = vnode
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentV