携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第7天,点击查看活动详情
前言
源码分析文章看了很多,也阅读了至少两遍源码。终归还是想自己写写,作为自己的一种记录和学习。重点看注释部分和总结,其余不用太关心,通过总结对照源码回看过程和注释收获更大
组件的定义
组件定义有两种方式,一种是局部注册,另一种是全局注册
// 局部注册
var Dialog = {...}
new Vue({
el: '#app',
components: {
'Dialog': Dialog
}
})
// 全局注册
Vue.component('Dialog': {
...
})
全局注册
初始化全局 api
// /src/core/index.js
import { initGlobalAPI } from './global-api/index'
...
initGlobalAPI(Vue)
...
全局组件函数,初始化了三个全局函数,将全局注册的组件放在Vue.options.components
对象中,利用Vue.extends
进行继承时,会调用_init
方法对全局组件和局部组件进行一个合并,也就是当全局组件和局部组件同名且都存在时,优先使用局部组件。
// src/shared/constants.js
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]
// core/global-api/assets.js
export function initAssetRegisters (Vue: GlobalAPI) {
ASSET_TYPES.forEach(type => {
Vue[type] = function (
id: string,
definition: Function | Object
): Function | Object | void {
if (!definition) {
return this.options[type + 's'][id]
} else {
// 定义是一个component并且是一个普通对象
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id
// 将定义的对象转化成构造器(保证组件之间的隔离) this.options._base是Vue
definition = this.options._base.extend(definition)
}
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition }
}
// 扩展
this.options[type + 's'][id] = definition
return definition
}
}
})
}
// src/core/global-api/extend.js
Vue.extend = function (extendOptions: Object): Function {
...
const Sub = function VueComponent (options) {
this._init(options)
}
...
}
// /src/core/instance/init.js
Vue.prototype._init = function (options?: Object) {
...
vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor),options || {},vm)
...
}
局部注册
在 new 过程中会将template
先转化成 ast,在生成render
函数过程中会判断是否是 html 标签,如果不是就会创建组件 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> {
...
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
// 判断是否是html标签 Ctor是组件的内容
if (config.isReservedTag(tag)) {
vnode = new VNode(config.parsePlatformTagName(tag), data, children,undefined, undefined, context)
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// 创建组件vnode
vnode = createComponent(Ctor, data, context, children, tag)
} else {
vnode = new VNode(tag, data, children,undefined, undefined, context)
}
} else {
vnode = createComponent(tag, data, context, children)
}
...
}
// 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 {
const baseCtor = context.$options._base
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
...
// 如果是局部组件,会调用vue.extend转化成构造函数,并会定义init(创建组件实例并进行挂载)等方法放在data.hook上
// init方法会执行类似 new Ctor().$mount()操作,所以会调用_init方法将组件options进行合并,组件里没有$el,所以得手动调用$mount方法,之后会返回组件的真实节点并赋值再组件的vm.$el上同时放在(vnode是当前父组件的虚拟节点)vnode.componentInstance上
// 所以再创建过程中,子组件也会生成自己的watcher
installComponentHooks(data)
const name = Ctor.options.name || tag
// 组件vnode Ctor是组件的构造函数
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
return vnode
}
渲染
在生成 render 函数后,就会调用 patch 方法进行真实节点的渲染
// src/core/vdom/patch.js
//创建真实节点
function createElm(vnode, insertedVnodeQueue, parentElm, refElm) {
//如果是组件就创建组件真实节点
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return;
}
}
// 创建组件的真实节点 如果data是hook属性,就证明他是组件,将组件的真实节点进行插入
function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data;
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
// 先取data上的hook,再取hook上的init方法,此时i是init
if (isDef((i = i.hook)) && isDef((i = i.init))) {
// 调用init方法
i(vnode, false /* hydrating */);
}
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue);
insert(parentElm, vnode.elm, refElm);
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
}
return true;
}
}
}
总结
组件有全局组件和局部组件两种方式,定义全局组件时,初始化时会利用Vue.extend
方法将组件转化成构造函数储存在Vue.options.components
对象中。如果定义了局部组件,在初始化时会将全局options和当前实例的options进行合并,也就是说全局组件和局部组件同时存在且同名时,优先使用局部组件。在生成虚拟节点时,如果不是html标签,说明是组件,会对组件进行特殊处理,会定义init方法(创建组件实例并进行挂载)存放在当前虚拟节点的data.hook上,生成组件的虚拟节点。在创建真实节点时,如果data上有hook属性,就证明是组件,此时就会去调用hook里的init方法,init方法会对子组件进行实例化(基于Vue,所以有_init
方法),实例化时就会调用_init
方法,会将父组件options和当前组件options进行合并,在初始化完数据等后调用$mount
方法生成真实节点同时会存放在父组件vnode的componentInstance
上,此时代表子组件已经创建完成。之后会将子组件的$el
插入到父组件中。由于子组件调用了$mount
方法,因此它有了自己的watcher,所以子组件也是响应式。
系列链接
【Vue2.x原理剖析一】响应式原理
【Vue2.x原理剖析二】计算属性原理
【Vue2.x原理剖析三】侦听属性原理
【Vue2.x原理剖析四】模板编译原理
【Vue2.x原理剖析五】初始渲染及更新原理
【Vue2.x原理剖析六】diff算法原理
【Vue2.x原理剖析七】组件原理