在我们创建Vue实例时,是通过new Vue
、this._init(options)
方法来进行初始化,然后再执行$mount
等,那么在组件渲染时,可不可以让组件使用同一套逻辑去处理呢?
答:当然是可以的,需要使用到Vue.extend
方法来实现。
element-ui
的Message Box
弹窗,在全局注册(Vue.use
)后,能像alert
方法一样,调用函数就可以弹出(先简单说下vue
的use
方法基础使用,use注册时,如果是函数会执行函数,如果是对象,会执行对象中的install
方法进行注册)
根据需求,我们在调用use方法后,需要实现两个目的:将组件注册并直接挂载到dom上,将方法放在Vue.prototype
下;
show(){this.visible = true}
use
的方式注册组件,就要有一个install
方法,在方法中首先调用Vue.extend(messageBox组件)
,然后调用该对象的$mount()
方法进行渲染,最后将生成的DOM节点messageBox.$el
上树,然后上show
方法放到Vue.prototype
上,就完成了function install(Vue) {
// 生成messageBox 构造函数
var messageBox = Vue.extend(this);
messageBox = new messageBox();
// 挂载组件,生成dom节点(这里没传参,所以只是生成dom并没有上树)
messageBox.$mount();
// 节点上树
document.body.appendChild(messageBox.$el);
// 上show方法挂载到全局
Vue.prototype.$showMessageBox = messageBox.show;
}
根据例子,我们来看一下这个extend
方法:
Vue
中,有一个extend
方法,组件的渲染就是通过调用extend
创建一个继承于Vue
的构造函数。
extend
中的创建的主要过程是:
在内部创建一个最终要返回的构造函数
Sub
,Sub
函数内部与Vue
函数相同,都是调用this._init(options)
继承Vue
,合并Vue.options
和组件的options
在Sub
上赋值静态方法
缓存Sub
构造函数,并在extend
方法开始时判断缓存,避免重复渲染同一组件
返回Sub构造函数(要注意extend调用后返回的是个还未执行的构造函数 Sub)
// 注:mergeOptions方法是通过不同的策略,将options中的属性进行合并
Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {}
const Super = this // 父级构造函数
// 拿到cid,并通过_Ctor属性缓存,判断是否已经创建过,避免重复渲染同一组件
const SuperId = Super.cid
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
// name校验+抛出错误
const name = extendOptions.name || Super.options.name
if (process.env.NODE_ENV !== 'production' && name) {
validateComponentName(name)
}
// 创建构造函数Sub
const Sub = function VueComponent (options) {
this._init(options)
}
// 继承原型对象
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++ // cid自增
// 父级options与当前传入的组件options合并
Sub.options = mergeOptions(
Super.options,
extendOptions
)
Sub['super'] = Super // 缓存父级构造函数
// For props and computed properties, we define the proxy getters on the Vue instances at extension time, on the extended prototype. This avoids Object.defineProperty calls for each instance created.
// 对于props和computed属性,我们在扩展时在扩展原型的Vue实例上定义代理getter。这避免了object。为创建的每个实例调用defineProperty。
if (Sub.options.props) {
initProps(Sub)
}
if (Sub.options.computed) {
initComputed(Sub)
}
// 将全局方法放在Sub上,允许进一步调用
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use
// create asset registers, so extended classes
// can have their private assets too.
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type]
})
// enable recursive self-lookup
if (name) {
Sub.options.components[name] = Sub
}
// keep a reference to the super options at extension time.
// later at instantiation we can check if Super's options have
// been updated.
Sub.superOptions = Super.options
Sub.extendOptions = extendOptions
Sub.sealedOptions = extend({}, Sub.options)
// 对应上边的_Ctor属性缓存
cachedCtors[SuperId] = Sub
return Sub
}
}
看完export
函数后,思考下,生成组件时是一个怎样的执行流程呢?
Vue.component()
祖册为例子):用户在调用Vue.component
时,其实就只执行了三行代码
// 简化版component源码
Vue.component = function (id,definition) {
definition.name = definition.name || id
// _base指向的是new Vue()时的这个Vue实例,调用的是Vue实例上的extend方法
definition = this.options._base.extend(definition)
this.options.components[id] = definition
return definition
}
获取并赋值组件的name
definition.name
调用根Vue上的extend方法
将组件放到options.components
上
返回definition
(如果是异步组件的话,只会走后边两步,不会执行extend
)
在下文中,我们会将extend
方法返回的Sub对象称为Ctor
在创建组件时,我们实际只是为组件执行了extend
方法,但在option.components
中传入的组件不会被执行extend
方法,在3.渲染流程中会执行
在createElement
函数执行时,根据tag
字段来判断是不是一个组件,如果是组件,执行组件初始化方法createComponent
createComponent
extend
方法,没有执行的话执行一遍createAsyncPlaceholder
生成并返回)data
,创建data.hook
中的钩子函数,比如init
new VNode()
生成节点先看下createElement
函数源码,然后在底下主要说下init
函数
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
}
// _base指向的是new Vue()时的这个Vue实例
const baseCtor = context.$options._base
// 如果extend没有执行过,在这里执行
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
}
// 异步处理
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 = data || {}
resolveConstructorOptions(Ctor)
if (isDef(data.model)) {
transformModel(Ctor.options, data)
}
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor, propsData, data, context, children)
}
const listeners = data.on
data.on = data.nativeOn
if (isTrue(Ctor.options.abstract)) {
const slot = data.slot
data = {}
if (slot) {
data.slot = slot
}
}
// 重点 创建init方法
installComponentHooks(data)
// return a placeholder vnode
const name = Ctor.options.name || tag
// 得到vnode
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
return vnode
}
init,prepatch,insert,destroy
等方法在源码中是创建在componentVNodeHooks
对象上,通过installComponentHooks
和installComponentHooks
方法判断data.hook
中是否有该值,然后进行合并处理等操作实现的,在这里,我们不考虑其他的直接看init
方法
先放上完整代码:
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
// 挂载到vnode上,方便取值
// 在这个函数中会new并返回extend生成的Ctor
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
// 重点
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
}
// createComponentInstanceForVnode函数示例
export function createComponentInstanceForVnode (
vnode: any, // we know it's MountedComponentVNode but flow doesn't
parent: any, // activeInstance in lifecycle state
): Component {
const options: InternalComponentOptions = {
_isComponent: true,
_parentVnode: vnode,
parent
}
// check inline-template render functions
const inlineTemplate = vnode.data.inlineTemplate
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render
options.staticRenderFns = inlineTemplate.staticRenderFns
}
return new vnode.componentOptions.Ctor(options)
}
init
方法中,执行createComponentInstanceForVnode
时会调用new Ctor(options)
extend
方法中可以看到new Ctor
时会调用Vue
的_init
方法,执行Vue
实例的初始化逻辑Vue.prototype._init
方法初始化完毕,执行$mount
是,会有下边代码这样一个判断,组件这时没有el
,所以不会执行$mount
函数if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
$mount
函数在组件渲染流程createElm
函数中,有一段代码
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
所以,组件的生成和判断都是在createComponent
函数中发生的
createComponent
vnode
就是该函数中传入的vnode
,并且在vnode
创建时把data
放在了vnode
上,那么vnode.data.hook.init
就可以获取到上边说的init
函数,我们可以判断,如果有该值,就可以认定本次vnode
为组件,并执行vnode.data.hook.init
,init
的内容详见上边init
执行完毕后,Ctor
的实例会被挂载到vnode.componentInstance
上,并且已经生成了真实dom,可以在vnode.componentInstance.$el
上获取到initComponent
和insert
,将组件挂载function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
// 在判断是否定义的同时,把变量做了改变,最终拿到了i.hook.init(在extend函数中注册的Ctor的init方法)
if (isDef(i = i.hook) && isDef(i = i.init)) {
// 执行init
i(vnode, false /* hydrating */)
}
// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it. the child
// component also has set the placeholder vnode's elm.
// in that case we can just return the element and be done.
//调用init hook之后,如果vnode是子组件
//它应该创建一个子实例并挂载它。孩子
//组件还设置了占位符vnode的elm。
//在这种情况下,我们只需返回元素就可以了。
// componentInstance是组件的ctor实例,有了代表已经创建了vnode.elm(真实节点)
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}