ps:建议PC端观看,移动端代码高亮错乱
这张我们开始进入组件化的学习,上一章我们在分析 createElement 的实现的时候,它最终会调用 _createElement 方法,其中有一段逻辑是对参数 tag 的判断,如果是一个普通的 html 标签,则会实例化一个普通 VNode 节点,否则通过 createComponent 方法创建一个组件 VNode。
// 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> {
// ...
if (typeof tag === 'string') {
// ...
} else {
vnode = createComponent(tag, data, context, children);
}
}
比如说当我们使用 vue-cli 脚手架开发时,我们这样 new Vue:
new Vue({
render: h => h(App),
}).$mount('#app')
我们给 createElement 传入的是一个组件,会走到 else 逻辑,调用 createComponent 创建一个组件
接下来看看 createComponent 的真面目,定义在 src/core/vdom/create-component.js 中
// 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 {
if (isUndef(Ctor)) {
return
}
// 核心逻辑1:创建子类构造函数
const baseCtor = context.$options._base
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
if (typeof Ctor !== 'function') {
if (process.env.NODE_ENV !== 'production') {
warn(`Invalid Component definition: ${String(Ctor)}`, context)
}
return
}
// 暂时不需要关心的其他逻辑:
// 1. 异步组件
// 2. 如果在创建组件构造函数之后应用了全局mixin,则解析构造函数options
// 3. 将组件 v-model 转换成 props & events
// 4. 提取props
// 5. 函数式组件
// 6. 对事件监听的处理
// 7. 抽象组件处理
// 核心逻辑2:安装组件钩子函数
installComponentHooks(data)
// 核心逻辑3:实例化 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的一些逻辑...
return vnode
}
注释中标注了三个需要在本章中关注的核心逻辑:
- 创建子类构造函数
- 安装组件钩子函数
- 实例化 VNode
1. 创建子类构造函数
首先需要知道 baseCtor 就是大 Vue 构造函数,这个的定义是在最开始初始化 Vue 的阶段,在 src/core/global-api/index.js 中的 initGlobalAPI 函数中:
Vue.options._base = Vue
细心的同学会发现,这里定义的是 Vue.options,而我们的 createComponent 取的是 context.$options,实际上在 src/core/instance/init.js 里的 _init 函数中有这么一段逻辑:
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
这样就把 Vue 上的一些 option 扩展到了 vm.$options 上,所以我们也就能通过 vm.$options._base 拿到 Vue 这个构造函数了。mergeOptions 的实现我们会在后续章节中具体分析,现在只需要理解它的功能是把 Vue 构造函数的 options 和用户传入的 options 做一层合并到 vm.$options 上。
另外,我们的组件通常都是一个普通的对象,比如通过 vue-loader 对我们的单文件组件处理以后返回的就是一个普通的对象
所以 isObject(Ctor) 为真,然后通过 baseCtor.extend(Ctor) 创建构造函数,也就是 Vue.extend
1.1 Vue.extend
Vue.extend 函数定义在 initExtend 中,initExtend 定义在 initGlobalAPI中。
// src/core/global-api/extend.js
export function initExtend (Vue: GlobalAPI) {
// 每个构造函数实例,包括Vue,都有唯一的cid,可以用于缓存子构造函数
Vue.cid = 0
let cid = 1
Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {}
const Super = this
const SuperId = Super.cid
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
// 是否返回缓存构造函数
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
const name = extendOptions.name || Super.options.name
if (process.env.NODE_ENV !== 'production' && name) {
validateComponentName(name) // 验证name
}
const Sub = function VueComponent (options) {
// 执行 this._init 逻辑再次走到了 Vue 实例的初始化逻辑,实例化子组件的逻辑在之后的章节会介绍。
this._init(options)
}
// 原型继承
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
Sub.options = mergeOptions(
Super.options,
extendOptions
)
Sub['super'] = Super
// ...
// 缓存构造函数
cachedCtors[SuperId] = Sub
return Sub
}
}
- 定义子类构造函数
Sub,基于原型链继承于Vue - 然后对
Sub这个对象本身扩展了一些属性,如:- 扩展 options,添加全局 API
- 对配置中的
props和computed做了初始化工作。 - 关于
initProps还有会专门的章节来介绍,这里留个印象
- 缓存构造函数,避免多次执行
Vue.extend的时候对同一个子组件重复构造。
这样当我们去实例化 Sub 的时候,就会执行 this._init 逻辑再次走到了 Vue 实例的初始化逻辑,实例化子组件的逻辑在之后的章节会介绍。
const Sub = function VueComponent (options) {
this._init(options)
}
2. 安装组件钩子函数
安装的作用就是在 VNode 执行 patch 的过程中执行相关的钩子函数,具体的执行我们稍后在介绍 patch 过程中会详细介绍。
installComponentHooks 函数的逻辑:
// src/core/vdom/create-component.js
function installComponentHooks (data: VNodeData) {
const hooks = data.hook || (data.hook = {})
// hooksToMerge 是 Object.keys(componentVNodeHooks)
for (let i = 0; i < hooksToMerge.length; i++) {
const key = hooksToMerge[i] // init, prepatch, insert, destroy
const existing = hooks[key]
const toMerge = componentVNodeHooks[key]
if (existing !== toMerge && !(existing && existing._merged)) {
hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
}
}
}
要看懂这个函数,还需要了解两个东西:
- componentVNodeHooks对象
- mergeHook函数
2.1 componentVNodeHooks
Vue.js 使用的 Virtual DOM 参考的是开源库 snabbdom,它的一个特点是在 VNode 的 patch 流程中对外暴露了各种时机的钩子函数,方便我们做一些额外的事情,Vue.js 也是充分利用这一点,在初始化一个 Component 类型的 VNode 的过程中实现了几个钩子函数:
// src/core/vdom/create-component.js
// inline hooks to be invoked on component VNodes during patch
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
// ...
},
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
// ...
},
insert (vnode: MountedComponentVNode) {
// ...
},
destroy (vnode: MountedComponentVNode) {
// ...
}
}
2.2 mergeHook
// src/core/vdom/create-component.js
function mergeHook (f1: any, f2: any): Function {
const merged = (a, b) => {
f1(a, b)
f2(a, b)
}
merged._merged = true
return merged
}
mergeHook 函数逻辑很简单,所谓合并就是先执行 componentVNodeHooks 定义的再执行 data.hooks 定义的,再将合并标志位设为 true。
3. 实例化 VNode
// 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 {
// ...
// 核心逻辑3:实例化 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
}
- 通过
new VNode实例化一个vnode并返回。 - 和普通元素节点的
vnode不同,组件的vnode是没有children的,这点很关键,在之后的patch过程中我们会再提。 - 第七个参数是
componentOptions,在patch过程中可以通过new vnode.componentOptions.Ctor来实例化子组件构造函数
总结
这一节我们分析了 createComponent 的实现,了解到它在渲染一个组件的时候的 3 个关键逻辑:创建子类构造函数,安装组件钩子函数,实例化 vnode。createComponent 后返回的是组件 vnode,它也一样走到 vm._update 方法,进而执行了 patch 函数,我们在上一章对 patch 函数做了简单的分析,那么下一节我们会对它做进一步的分析。