人人都能懂的Vue原始碼系列(二)—Vue建構函式

淼淼真人發表於2019-03-04

上篇博文中說到Vue原始碼的目錄結構是什麼樣的,每個目錄的作用應該也有所瞭解。我們知道core/instance目錄主要是用來例項化Vue物件,所以我們在這個目錄下去尋找Vue建構函式。果然找到了Vue建構函式的定義。

function Vue (options) {
  if (process.env.NODE_ENV !== `production` &&
    !(this instanceof Vue)
  ) {
    warn(`Vue is a constructor and should be called with the `new` keyword`)
  }
  this._init(options)
}
複製程式碼

當你新建一個Vue例項時候,會判斷如果當前的環境不是生產環境,且你在呼叫Vue的時候,沒有用new操作符。就會呼叫warn函式,丟擲一個警告。告訴你Vue是一個建構函式,需要用new操作符去呼叫。這個warn函式不是單純的console.warn,它的實現我們後面的博文會介紹。

接下來,把options作為引數呼叫_init方法。options不做過多的介紹了,就是你呼叫new Vue時候傳入的引數。在深入_init方法之前,我們先把目光移到index.js檔案裡

function Vue (options) {
  ...
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
複製程式碼

在Vue的建構函式定義之後,有一系列方法會被呼叫,這些方法主要用來給Vue函式新增一些原型屬性和方法的。其中就有接下來要介紹的Vue.prototyoe._init

Vue.prototype._init

在core/instance/init.js中我們找到了_init的定義。

  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    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
    // merge options
    // 有子元件時,options._isComponent才會為true
    if (options && options._isComponent) {
      // optimize internal component instantiation(例項)
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== `production`) {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    initLifecycle(vm) 
    initEvents(vm) 
    initRender(vm) 
    callHook(vm, `beforeCreate`)
    initState(vm)
    initProvide(vm) 
    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)
    }
    if (vm.$options.el) {
      vm.$mount(vm.$options.el) 
    }
  }
複製程式碼

我們逐一來分析上述程式碼。首先快取當前的上下文到vm變數中,方便之後呼叫。然後設定_uid屬性。_uid屬性是唯一的。當觸發init方法,新建Vue例項時(當渲染元件時也會觸發)uid都會遞增。

下面這段程式碼主要是用來測試程式碼效能的,在這個時候相當於打了一個”標記點”來測試效能。

let startTag, endTag
    /* istanbul ignore if */
    process.env.NODE_ENV === `develop`
    if (process.env.NODE_ENV !== `production` && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
}
複製程式碼

對這部分內容感興趣的朋友們可以點選我的另一篇文章Performance API檢視。接下來執行這行程式碼vm._isVue = true,Vue的作者對這句話做了註釋。

an flag to avoid this being observed

乍看起來好像不太明白,好像是說為了防止this被observed例項化。那這究竟是什麼意思呢?我們來看observer的程式碼。

export function observe (value: any, asRootData: ?boolean): Observer | void {
  ...
  else if (
    observerState.shouldConvert &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  ...
}
複製程式碼

如果傳入值的_isVue為ture時(即傳入的值是Vue例項本身)不會新建observer例項(這裡可以暫時理解新建observer例項就是讓資料響應式)。

再回到init原始碼部分

if (options && options._isComponent) {
    initInternalComponent(vm, options)
} else {
    vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
        options || {},
        vm
     )
}
複製程式碼
  • 當符合第一個條件是,即當前這個Vue例項是元件。則執行initInternalComponent方法。(該方法主要就是為vm.$options新增一些屬性, 後面講到元件的時候再詳細介紹)。
  • 當符合第二個條件時,即當前Vue例項不是元件。而是例項化Vue物件時,呼叫mergeOptions方法。

mergeOptions主要呼叫兩個方法,resolveConstructorOptions和mergeOptions。這兩個方法牽涉到了很多知識點,為了我們文章篇幅的考慮。接下來準備通過兩篇博文來介紹這兩個方法。

下篇博文主要介紹resolveConstructorOptions相關的內容,涉及到原型鏈和建構函式以及部分Vue.extend的實現,敬請期待!

相關文章