Vue多個(gè)組件到底new Vue了幾次?

問題來源:時(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之后

image

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é)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容