众所周知,Vue 的灵魂在于: 数据驱动视图 + 组件化,本篇将为大家讲解 Vue组件 的创建,还有与其息息相关的 Vue.extend 方法。
在这个文件中定义了 Vue.component 方法,我修改了部分源码便于理解,但准确度不变:
Vue.component = function(id, definition) {
if (!definition) {
return this.options.components[id]
} else {
if (isPlainObject(definition)) {
definition.name = definition.name || id
definition = this.options._base.extend(definition)
}
this.options.components[id] = definition
return definition
}
}
简单解读一下就是:
如果 definition 不存在,说明此前被全局注册过,那就去 Vue.options.components 中找到对应的组件返回;
如果 definition 存在,说明这是一个新的全局组件,需要被全局注册。先判断 definition 是否是一个对象,如果是则将 definition 创建为继承自 Vue构造函数 的子类实例,如果不是则说明是一个已经继承自子类的实例,将其放置到 Vue.options.components 中,然后返回。
有一些绕,那我们就通过代码来进行说明:
// 方式1
const childComponent = Vue.extend({
data: {},
created() {},
methods: {},
})
Vue.component('child', childComponent)
// 方式2
Vue.component('child', {
name: 'child-component',
data: {},
created() {},
methods: {},
})
如果是方式1,那么这时候不会进入 isPlainObject(definition) 的判断,直接将 childComponent 放置到 this.options.components[‘child’] 中,然后将 childComponent 返回。
如果是方式2,那么这时候会进入 isPlainObject(definition) 的判断,当前 definition.name 有值且为 ‘child-component’,definition 会继续走入 this.options._base.extend(definition)
,这个方法其实就是 Vue.extend,然后将继承自 Vue构造函数 的子类的实例返回给 definition,再将这个 definition 放置到 this.options.components[‘child-component’] 中,最后将 definition 返回。
通过这两种方式我们可以知道,通过 Vue.component(id, definition) 注册的组件都会经过一层 Vue.extend(definition) 的包装生成一个子类实例,我们取名为 componentVM ,然后将这个 componentVM 放入 Vue.options.components 中,最后返回 componentVM。
你如果不明白 this.options.components 和 this.options._base.extend 是哪儿来的,请看上一章 Vue构造函数的创建过程 -> core/global-api/index.js 这部分。
从 Vue.component 可以得知,组件化的核心其实是在于 Vue.extend,通过 Vue.extend 包装过后的实例才是真正的组件,那么它其中的原理又是什么呢?我将源码又修改了一下,但准确度不变:
Vue.extend = function (extendOptions) {
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
const Sub = function VueComponent (options) {
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
if (Sub.options.props) {
initProps(Sub)
}
if (Sub.options.computed) {
initComputed(Sub)
}
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type]
})
if (name) {
Sub.options.components[name] = Sub
}
Sub.superOptions = Super.options
Sub.extendOptions = extendOptions
Sub.sealedOptions = extend({}, Sub.options)
cachedCtors[SuperId] = Sub
return Sub
}
从 Vue.extend 的写法来看,很明显这是 Vue构造函数 的静态方法,所以内部的 this 指向了 Vue构造函数本身,也就是函数体内定义的 Super。
源码简单解读一下就是:
新建一个 VueComponent构造函数 命名为 Sub,通过将 VueComponent 的原型指向 Vue构造函数 的原型的方式继承 Vue构造函数 原型上所有的属性和方法,再将 Vue.use / Vue.mixin / Vue.extend / Vue.component / Vue.directive / Vue.filter 方法定义给它,让 VueComponent 也拥有这些静态方法。
接着检查传入的 extendOptions 是否拥有 props 和 computed 属性,如果有就进行初始化。如果检测出传入的 extendOptions 中含有 name 属性,则将其自动放入 VueComponent 的全局组件中 ( 不要和 Vue 的全局组件混淆 )。
然后将 Vue.options 保存到 VueComponent.superOptions 属性中,将传入的 extendOptions 保存到 VueComponent.extendOptions 属性中,并将传入的 extendOptions 封存一份保存到 VueComponent.sealedOptions 中。
最后将 VueComponent 返回,这就是继承了 Vue构造函数 的 Vue组件 的构造函数。
VueComponent = {
cid,
super: Vue,
options: {
_base: Vue,
components: {
KeepAlive,
Transition,
TransitionGroup,
}
directives: {
'model',
'show',
}
filters: {},
_Ctor: {},
...extendOptions
},
use() {},
mixin() {},
extend() {},
component() {},
directive() {},
filter() {},
superOptions,
extendOptions,
sealedOptions,
prototype: {
_init() {},
$data: {},
$props: {},
$set() {},
$del() {},
$watch() {},
$on() {},
$once() {},
$off() {},
$emit() {},
_update() {},
$forceUpdate() {},
$destroy() {},
$nextTick() {},
_render() {},
_o() {},
_n() {},
_s() {},
_l() {},
_t() {},
_q() {},
_i() {},
_m() {},
_f() {},
_k() {},
_b() {},
_v() {},
_e() {},
_u() {},
_g() {},
$isServer: false,
$ssrContext: {},
__patch__() {},
$mount() {},
}
}
我们细心些可以注意到在 VueComponent构造函数 内调用了 this._init(options)
,这串代码好像有些似曾相识?没错,在 Vue构造函数 定义的时候也调用过,为了让 new Vue(options)
后自动开启初始化工作,这里也是同理。但是除了我们直接使用 Vue.extend 挂载一个组件时会去 new + $mount 一下,在它内部我们似乎没有看到哪儿使用过 new VueComponent(options)
,这是怎么回事呢?
而且最重要的是,我们在 Vue.prototype._init 中曾经判断过 _isComponent 这个属性,怎么既没有在 Vue.component 的定义中看到也没有在 Vue.extend 的定义中看到呢?
这时候就要牵扯到父子组件的生命周期了。具体内容会在下一篇揭晓,这里就先说个初始化生命周期过程,大家可以猜一猜是在什么时候有了 _isComponent,又是什么时候进行了 new VueComponent(options)
:
父组件 beforeCreate -> 父组件 created -> 父组件 beforeMount -> 子组件 beforeCreate -> 子组件 created -> 子组件 beforeMount -> 子组件 mounted -> 父组件 mounted