本篇文章打算分析梳理下Vue提供的创建组件的方法,打开Vue官网,在API中只有Vue.component和Vue.extend提供组件的创建。
严格意义上是这么说,但是在实际项目中常使用局部注册以及文件即组件这种方式来实现开发,这里先不讨论这种方式背后的处理,主要放在Vue.component和Vue.extend这两个API的逻辑处理。
结合简单实例来梳理下整个的处理逻辑,实例如下:
// html部分
<element-block>element-block>
// js部分
Vue.component('element-block', {
template: '<div>子组件div>'
});
在Vue初始化这篇文章中就分析了Vue初始化过程一些相关的处理,其中有一点是:
Vue静态方法的定义
实际上在Vue初始化阶段Vue.component和Vue.extend都已经定义了。
当使用Vue.component创建组件时,梳理源码中具体的处理逻辑如下:
首先提及下上面逻辑中涉及到的options中的相关属性,实际上这边还是Vue.js文件加载之后初始化过程中定义的,具体定义的函数是:
initGlobalAPI
initGlobalAPI这个函数中涉及到上面逻辑中的有:
Vue.options = Object.create(null);
// ASSET_TYPES值为[component, directive, filter]
ASSET_TYPES.forEach(function (type) {
Vue.options[type + 's'] = Object.create(null);
});
Vue.options._base = Vue;
从这里以及上面的处理逻辑可知:
全局注册的组件都会保存Vue.options.components对象中,该对象以组件名为key
回到Vue.component的处理逻辑,主要逻辑如下:
- 判断是否传递了选项对象,如果没有,则会返回options.components对应的组件
- 校验组件名是否符合要求,实际上判断是否非字母开头、是否是HTML标签、是否是SVG标签
- 判断是否是创建组件并且选项是普通对象,
- 组件名定义,如果每定义name,则会以id为name
- 调用Vue.extend方法
从上面逻辑中至少了解了3点:
从Vue.component中逻辑可知最后还是调用了Vue.extend,实际上Vue.extend这个API对于我来说使用频率很低,导致对其基本就不了解。
Vue官方对其的用法的说明:
使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。
// 创建构造器
var Profile = Vue.extend({
template: '{
{firstName}} {
{lastName}} aka {
{alias}}
',
})
// 创建 Profile 实例,并挂载到一个元素上。
new Profile().$mount('#mount-point')
就其用法来看,就是提供选项对象创建组件,跟Vue.component的区别有:
- 传入组件name选项
- 执行new运算,Vue.component这里不会执行new运算
所以可以将Vue.component看成Vue.extend的提供的便捷方式。
Vue.extend处理需要重要关注有两点:
var Sub = function VueComponent(options) {
this._init(options);
};
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
定义Sub构造函数,其内部还是调用Vue.prototype._init实例方法做初始化的过程,即跟new Vue()逻辑相似的处理。
之后就是典型的原型继承,接下来的处理逻辑中有几点需要注意的,如下:
if (Sub.options.props) initProps$1(Sub);
if (Sub.options.computed) initComputed$1(Sub);
extendOptions._Ctor[superId] = Sub
如果传递的合并选项中存在props和computed实际上这里会调对应的方法来初始化,而且会缓存构造函数。
function initProps$1 (Comp) {
var props = Comp.options.props;
for (var key in props) {
proxy(Comp.prototype, "_props", key);
}
}
function initComputed$1 (Comp) {
var computed = Comp.options.computed;
for (var key in computed) {
// 将computed注册到VueComponent.prototype原型对象上
defineComputed(Comp.prototype, key, computed[key]);
}
}
这里就有疑问了:
Sub构造函数中this._init的初始化过程也会initProps和initComputed
在_init处理中实际上会判断当前实例原型链中是否存在同名的prop或computed,这边在源码中有注释,大概意思是:
在扩展的原型上,在_init之前处理initComputed$1等会避免Object.defineProperty调用
说实话没有明白这边要表达的意思,之后会找找看处理什么情况的。
Vue.component与单独使用Vue.extend不同点有:
在VNode创建这篇文章中简单梳理了createElement函数的功能,该函数是Vue创建VNode的关键函数,所有节点都会有一个VNode,无论是文本、注释、组件,都会调用createElement函数。
实际上在构建render函数,元素节点都会被构建成如下形式:
_c('名称')
就拿本篇文章中实例实际上构建出来最后的render函数如下:
return _c('div', {
attrs: {
'id': 'app'}
},[
_c('element-block')
])
在执行render函数时,就会触发执行_c函数(即createElement函数),这里就要看看createElement中是如何处理子组件的问题了。
关于createElement中针对组件的处理主要逻辑如下:
从这里可以看到createElement就是完成组件类型的VNode创建,这里还是没有调用Sub构造函数创建组件实例,这里有一点需要额外注意下,就是installComponentHooks()函数,这里的生命周期有些特别:
- init
- prepatch
- insert
- destory
针对Component VNode的生命周期函数,即组件类型的VNode的生命周期。
接下来的的逻辑顺序可以参考 VNode创建、VNode转换成HTML这两篇文章,创建VNode对象之后,就是_update实例方法的调用,_update实例方法的作用概括来讲两点:
而涉及到创建Vue.component创建组件这边,就主要是下面函数了:
createElm
涉及到Vue.component相关的主要逻辑如下:
createElm ->createComponent -> 判断data是否存在hook并且存在init生命周期函数 -> createComponentInstanceForVnode
需要区分的是这里的createComponent不是_c中的createComponent,而是另外一个局部函数。
调用Component VNode的init生命周期函数,在生命周期函数中会调用createComponentInstanceForVnode,而该函数内部有一行代码
return new vnode.componentOptions.Ctor(options)
至此才调用Vue.component构建的Sub构造函数,完成组件实例的创建
Vue提供的创建组件API:Vue.component和Vue.extend,通过上面的梳理分析,可以得到它们之间的联系如下:
主要区别如下: