问题来源:时常在思考,vue每个组件实例大体一致,但是组件都是导出的一个组件构造对象,并没有看到组件new Vue成一个实例的一个操作,在Vue中每个组件都是一个vue实例,那么多个vue组件是不是会new Vue多次呢?伴随这个问题我百度了很多,有的说一个vue文件/组件就会new一次,有多少个就new多少次,还有的说vue作为个单页面应用,只new一次...。啊这我到底该听哪个?随着这个答案不确定,还是自己去解答吧!
1、我们可以看到唯一的一次new Vue
是在main.js
中,在new Vue
以后调用的是this._init
方法。
//vue初始化
Vue.prototype._init = function (options?: Object) {
//vue实例: 在new Vue函数后的this._init,所以this就是Vue实例
const vm: Component = this
// a uid
//每个vue实例都有一个uid,并且是依次递增的,类似于key避免重复
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// a flag to avoid this being observed
vm._isVue = true
// 处理组件配置项
if (options && options._isComponent) {
/**
* 每个子组件初始化走这里,只做一些性能优化
* 将组件配置项对象的一些深层次属性放到 vue.$options 选项中,以提高代码的执行效率
*/
initInternalComponent(vm, options)
} else {
/**
* 初始化根组件走这里,合并 vue 的全局配置到根组件的局部配置,比如Vue.component 注册的全局组件会合并到 根实例的 components 选项
* 至于每个子组件的选项合并则发生在两个地方:
* 1、Vue.component 方法注册的全局组件在注册时做了选择合并
* 2、{ components: { xx } } 方式注册的局部组件在执行编译器生产的 render 函数进行了选项合并,包括根组件中的 components 配置
* @type {Object}
*/
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor), //vm的构造器:Vue函数对象,也就是全局配置对象
options || {}, //vm上的属性
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
// 设置代理,将 vm 实例上的属性代理到 vm._renderProxy
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
// 初始化组件实例关系属性,比如 $parent、$children、$root、$refs 等
// 也可以理解为找出组件实例的生命周期所在位置
initLifecycle(vm)
/**
* 初始化自定义事件,这里需要注意一点,所以我们在 <comp @click="handleClick" /> 上注册的事件,监听者不是父组件,
* 而是子组件本身,也就是说事件的派发和监听者都是子组件本身,和父组件无关
*/
initEvents(vm)
// 解析组件的插槽信息,得到 vm.$slot,处理渲染函数,得到 vm.$createElement 方法,即 h 函数
initRender(vm)
// 调用 beforeCreate 钩子函数
callHook(vm, 'beforeCreate')
// 初始化组件的 inject 配置项,得到 result[key] = val 形式的配置对象,然后对结果数据进行响应式处理,并代理每个 key 到 vm 实例
initInjections(vm) // resolve injections before data/props
// 数据响应式的重点,处理 props、methods、data、computed、watch
initState(vm)
// 解析组件配置项上的 provide 对象,将其挂载到 vm._provided 属性上
initProvide(vm) // resolve provide after data/props
// 调用 created 钩子函数
callHook(vm, 'created')
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
//如果发现配置项有el属性,则自动调用$mount方法,也就是有了el属性就不需要手动调用$mount,反之没有el则必须手动调用$mount
if (vm.$options.el) {
//调用$mount方法,进入挂载阶段
vm.$mount(vm.$options.el)
}
}
组件的属性合并、数据响应式等都是在这个函数里面完成的,那么组件每次导出的都是export default { ...属性 }
,那组件是在哪初始化的?
2、为了理解这个问题我试着去找在哪一步有处理子组件的。当分析组件嵌套的生命周期时,我们可以看到子组件开始创建是在父组件beforeMount
之后
3、那么推断与render
函数有关,于是找到_init
函数内的initRender(vm)
函数,我们可以看到函数内部得到了$createElement
,即h渲染函数
。在父组件开始渲染的时候会触发这个$createElement
,也就是执行createElement
export function initRender (vm: Component) {
//省略
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
//省略
}
4、createElement
函数返回一个_createElement
,这里就直接贴_createElement
的代码了。
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)
if (config.isReservedTag(tag)) {
//...省略
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// 仔细看这一步,当判断当前实例有components时,就执行createComponent
vnode = createComponent(Ctor, data, context, children, tag)
} else {
//...省略
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
//...省略
}
5、接着往下走,createComponent
做了什么操作,主要是以下代码,先拿到baseCtor
也就是Vue
这个构造函数,然后判断这个子组件export
的是不是一个对象,是的话执行Vue.extend把这个子组件的构造对象传入进去
const baseCtor = context.$options._base
// plain options object: turn it into a constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
6、那么Vue
身上的extend
是从哪里来的,又做了什么。看以下代码可以看到extedn
方法导出了一个Sub
构造函数,并且继承自Vue
构造函数的,然后最后执行baseCtor.extend
等于调用了Sub
方法也就是this._init
做子组件的初始化创建。
Vue.extend = function (extendOptions: Object): Function {
//...省略
// 定义 Sub 构造函数,和 Vue 构造函数一样
const Sub = function VueComponent (options) {
// 初始化
this._init(options)
}
//...省略
return Sub
}
总结:Vue作为个单页面实际只new了一次,后续组件都是通过this._init
创建的实例。所以关于父子组件的生命周期也大体明白,这也是为什么父组件的beforeMount
后,就到了子组件的创建环节。