問題來源:時(shí)常在思考,vue每個(gè)組件實(shí)例大體一致,但是組件都是導(dǎo)出的一個(gè)組件構(gòu)造對(duì)象,并沒有看到組件new Vue成一個(gè)實(shí)例的一個(gè)操作,在Vue中每個(gè)組件都是一個(gè)vue實(shí)例,那么多個(gè)vue組件是不是會(huì)new Vue多次呢?伴隨這個(gè)問題我百度了很多,有的說一個(gè)vue文件/組件就會(huì)new一次,有多少個(gè)就new多少次,還有的說vue作為個(gè)單頁面應(yīng)用,只new一次...。啊這我到底該聽哪個(gè)?隨著這個(gè)答案不確定,還是自己去解答吧!
1、我們可以看到唯一的一次new Vue是在main.js中,在new Vue以后調(diào)用的是this._init方法。
//vue初始化
Vue.prototype._init = function (options?: Object) {
//vue實(shí)例: 在new Vue函數(shù)后的this._init,所以this就是Vue實(shí)例
const vm: Component = this
// a uid
//每個(gè)vue實(shí)例都有一個(gè)uid,并且是依次遞增的,類似于key避免重復(fù)
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
// 處理組件配置項(xiàng)
if (options && options._isComponent) {
/**
* 每個(gè)子組件初始化走這里,只做一些性能優(yōu)化
* 將組件配置項(xiàng)對(duì)象的一些深層次屬性放到 vue.$options 選項(xiàng)中,以提高代碼的執(zhí)行效率
*/
initInternalComponent(vm, options)
} else {
/**
* 初始化根組件走這里,合并 vue 的全局配置到根組件的局部配置,比如Vue.component 注冊(cè)的全局組件會(huì)合并到 根實(shí)例的 components 選項(xiàng)
* 至于每個(gè)子組件的選項(xiàng)合并則發(fā)生在兩個(gè)地方:
* 1、Vue.component 方法注冊(cè)的全局組件在注冊(cè)時(shí)做了選擇合并
* 2、{ components: { xx } } 方式注冊(cè)的局部組件在執(zhí)行編譯器生產(chǎn)的 render 函數(shù)進(jìn)行了選項(xiàng)合并,包括根組件中的 components 配置
* @type {Object}
*/
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor), //vm的構(gòu)造器:Vue函數(shù)對(duì)象,也就是全局配置對(duì)象
options || {}, //vm上的屬性
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
// 設(shè)置代理,將 vm 實(shí)例上的屬性代理到 vm._renderProxy
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
// 初始化組件實(shí)例關(guān)系屬性,比如 $parent、$children、$root、$refs 等
// 也可以理解為找出組件實(shí)例的生命周期所在位置
initLifecycle(vm)
/**
* 初始化自定義事件,這里需要注意一點(diǎn),所以我們?cè)?<comp @click="handleClick" /> 上注冊(cè)的事件,監(jiān)聽者不是父組件,
* 而是子組件本身,也就是說事件的派發(fā)和監(jiān)聽者都是子組件本身,和父組件無關(guān)
*/
initEvents(vm)
// 解析組件的插槽信息,得到 vm.$slot,處理渲染函數(shù),得到 vm.$createElement 方法,即 h 函數(shù)
initRender(vm)
// 調(diào)用 beforeCreate 鉤子函數(shù)
callHook(vm, 'beforeCreate')
// 初始化組件的 inject 配置項(xiàng),得到 result[key] = val 形式的配置對(duì)象,然后對(duì)結(jié)果數(shù)據(jù)進(jìn)行響應(yīng)式處理,并代理每個(gè) key 到 vm 實(shí)例
initInjections(vm) // resolve injections before data/props
// 數(shù)據(jù)響應(yīng)式的重點(diǎn),處理 props、methods、data、computed、watch
initState(vm)
// 解析組件配置項(xiàng)上的 provide 對(duì)象,將其掛載到 vm._provided 屬性上
initProvide(vm) // resolve provide after data/props
// 調(diào)用 created 鉤子函數(shù)
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)
}
//如果發(fā)現(xiàn)配置項(xiàng)有el屬性,則自動(dòng)調(diào)用$mount方法,也就是有了el屬性就不需要手動(dòng)調(diào)用$mount,反之沒有el則必須手動(dòng)調(diào)用$mount
if (vm.$options.el) {
//調(diào)用$mount方法,進(jìn)入掛載階段
vm.$mount(vm.$options.el)
}
}
組件的屬性合并、數(shù)據(jù)響應(yīng)式等都是在這個(gè)函數(shù)里面完成的,那么組件每次導(dǎo)出的都是export default { ...屬性 },那組件是在哪初始化的?
2、為了理解這個(gè)問題我試著去找在哪一步有處理子組件的。當(dāng)分析組件嵌套的生命周期時(shí),我們可以看到子組件開始創(chuàng)建是在父組件beforeMount之后
3、那么推斷與render函數(shù)有關(guān),于是找到_init函數(shù)內(nèi)的initRender(vm)函數(shù),我們可以看到函數(shù)內(nèi)部得到了$createElement,即h渲染函數(shù)。在父組件開始渲染的時(shí)候會(huì)觸發(fā)這個(gè)$createElement,也就是執(zhí)行createElement
export function initRender (vm: Component) {
//省略
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
//省略
}
4、createElement函數(shù)返回一個(gè)_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))) {
// 仔細(xì)看這一步,當(dāng)判斷當(dāng)前實(shí)例有components時(shí),就執(zhí)行createComponent
vnode = createComponent(Ctor, data, context, children, tag)
} else {
//...省略
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
//...省略
}
5、接著往下走,createComponent做了什么操作,主要是以下代碼,先拿到baseCtor也就是Vue這個(gè)構(gòu)造函數(shù),然后判斷這個(gè)子組件export的是不是一個(gè)對(duì)象,是的話執(zhí)行Vue.extend把這個(gè)子組件的構(gòu)造對(duì)象傳入進(jìn)去
const baseCtor = context.$options._base
// plain options object: turn it into a constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
6、那么Vue身上的extend是從哪里來的,又做了什么??匆韵麓a可以看到extedn方法導(dǎo)出了一個(gè)Sub構(gòu)造函數(shù),并且繼承自Vue構(gòu)造函數(shù)的,然后最后執(zhí)行baseCtor.extend等于調(diào)用了Sub方法也就是this._init做子組件的初始化創(chuàng)建。
Vue.extend = function (extendOptions: Object): Function {
//...省略
// 定義 Sub 構(gòu)造函數(shù),和 Vue 構(gòu)造函數(shù)一樣
const Sub = function VueComponent (options) {
// 初始化
this._init(options)
}
//...省略
return Sub
}
總結(jié):Vue作為個(gè)單頁面實(shí)際只new了一次,后續(xù)組件都是通過this._init創(chuàng)建的實(shí)例。所以關(guān)于父子組件的生命周期也大體明白,這也是為什么父組件的beforeMount后,就到了子組件的創(chuàng)建環(huán)節(jié)。