1. 程式人生 > >vue原始碼(五)Vue 選項的規範化

vue原始碼(五)Vue 選項的規範化

本文是學習vue原始碼,之所以轉載過來是方便自己隨時檢視,在這裡要感謝HcySunYang大神,提供的開源vue原始碼解析,寫的非常非常好,簡單易懂,比自己看要容易多了,他的文章連結地址是http://hcysun.me/vue-design/art/

注意:本節討論依舊沿用前文的例子

#弄清楚傳遞給 mergeOptions 函式的三個引數

這一小節我們繼續前面的討論,看一看 mergeOptions 都做了些什麼。根據 core/instance/init.js 頂部的引用關係可知,mergeOptions 函式來自於 core/util/options.js

 檔案,事實上不僅僅是 mergeOptions 函式,整個檔案所做的一切都為了一件事:選項的合併。

不過在我們深入 core/util/options.js 檔案之前,我們有必要搞清楚一件事,就是如下程式碼中:

vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
)

傳遞給 mergeOptions 函式的三個引數到底是什麼。

其中第一個引數是通過呼叫一個函式得到的,這個函式叫做 resolveConstructorOptions

,並將 vm.constructor 作為引數傳遞進去。第二個引數 options 就是我們呼叫 Vue 建構函式時透傳進來的物件,第三個引數是當前 Vue 例項,現在我們逐一去看。

resolveConstructorOptions 是一個函式,這個函式就宣告在 core/instance/init.js 檔案中,如下:

export function resolveConstructorOptions (Ctor: Class<Component>) {
  let options = Ctor.options
  if (Ctor.super) {
    const superOptions = resolveConstructorOptions(Ctor.super)
    const cachedSuperOptions = Ctor.superOptions
    if (superOptions !== cachedSuperOptions) {
      // super option changed,
      // need to resolve new options.
      Ctor.superOptions = superOptions
      // check if there are any late-modified/attached options (#4976)
      const modifiedOptions = resolveModifiedOptions(Ctor)
      // update base extend options
      if (modifiedOptions) {
        extend(Ctor.extendOptions, modifiedOptions)
      }
      options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
      if (options.name) {
        options.components[options.name] = Ctor
      }
    }
  }
  return options
}

在具體去看程式碼之前,大家能否通過這個函式的名字猜一猜這個函式的作用呢?其名字是 resolve Constructor Options 那麼這個函式是不是用來*解析構造者的 options*的呢?答案是:對,就是幹這個的。接下來我們就具體看一下它是怎麼做的,首先第一句:

let options = Ctor.options

其中 Ctor 即傳遞進來的引數 vm.constructor,在我們的例子中他就是 Vue 建構函式,可能有的同學會問:難道它還有不是 Vue 建構函式的時候嗎?當然,當你使用 Vue.extend 創造一個子類並使用子類創造例項時,那麼 vm.constructor 就不是 Vue 建構函式,而是子類,比如:

const Sub = Vue.extend()
const s = new Sub()

那麼 s.constructor 自然就是 Sub 而非 Vue,大家知道這一點即可,但在我們的例子中,這裡的 Ctor 就是 Vue 建構函式,而有關於 Vue.extend 的東西,我們後面會專門討論的。

所以,Ctor.options 就是 Vue.options,然後我們再看 resolveConstructorOptions 的返回值是什麼?如下:

return options

也就是把 Vue.options 返回回去了,所以這個函式的確就像他的名字那樣,是用來獲取構造者的 options 的。不過同學們可能注意到了,resolveConstructorOptions 函式的第一句和最後一句程式碼中間還有一坨包裹在 if 語句塊中的程式碼,那麼這坨程式碼是幹什麼的呢?

我可以很明確地告訴大家,這裡水稍微有那麼點深,比如 if 語句的判斷條件 Ctor.supersuper 這是子類才有的屬性,如下:

const Sub = Vue.extend()
console.log(Sub.super)  // Vue

也就是說,super 這個屬性是與 Vue.extend 有關係的,事實也的確如此。除此之外判斷分支內的第一句程式碼:

const superOptions = resolveConstructorOptions(Ctor.super)

我們發現,又遞迴地呼叫了 resolveConstructorOptions 函式,只不過此時的引數是構造者的父類,之後的程式碼中,還有一些關於父類的 options 屬性是否被改變過的判斷和操作,並且大家注意這句程式碼:

// check if there are any late-modified/attached options (#4976)
const modifiedOptions = resolveModifiedOptions(Ctor)

我們要注意的是註釋,有興趣的同學可以根據註釋中括號內的 issue 索引去搜一下相關的問題,這句程式碼是用來解決使用 vue-hot-reload-api 或者 vue-loader 時產生的一個 bug 的。

現在大家知道這裡的水有多深了嗎?關於這些問題,我們在講 Vue.extend 時都會給大家一一解答,不過有一個因素從來沒有變,那就是 resolveConstructorOptions 這個函式的作用永遠都是用來獲取當前例項構造者的 options 屬性的,即使 if 判斷分支內也不例外,因為 if 分支只不過是處理了 options,最終返回的永遠都是 options

所以根據我們的例子,resolveConstructorOptions 函式目前並不會走 if 判斷分支,即此時這個函式相當於:

export function resolveConstructorOptions (Ctor: Class<Component>) {
  let options = Ctor.options
  return options
}

所以,根據我們的例子,此時的 mergeOptions 函式的第一個引數就是 Vue.options,那麼大家還記得 Vue.options 長成什麼樣子嗎?不記得也沒關係,這就得益於我們整理的 附錄/Vue建構函式整理-全域性API 了,通過檢視我們可知 Vue.options 如下:

Vue.options = {
	components: {
		KeepAlive
		Transition,
    	TransitionGroup
	},
	directives:{
	    model,
        show
	},
	filters: Object.create(null),
	_base: Vue
}

接下來,我們再看看第二個引數 options,這個引數實際上就是我們呼叫 Vue 建構函式的透傳進來的選項,所以根據我們的例子 options 的值如下:

{
  el: '#app',
  data: {
    test: 1
  }
}

而第三個引數 vm 就是 Vue 例項物件本身,綜上所述,最終如下程式碼:

vm.$options = mergeOptions(
  resolveConstructorOptions(vm.constructor),
  options || {},
  vm
)

相當於:

vm.$options = mergeOptions(
  // resolveConstructorOptions(vm.constructor)
  {
    components: {
      KeepAlive
      Transition,
      TransitionGroup
    },
    directives:{
      model,
      show
    },
    filters: Object.create(null),
    _base: Vue
  },
  // options || {}
  {
    el: '#app',
    data: {
      test: 1
    }
  },
  vm
)

現在我們已經搞清楚傳遞給 mergeOptions 函式的三個引數分別是什麼了,那麼接下來我們就開啟 core/util/options.js 檔案並找到 mergeOptions 方法,看一看都發生了什麼。

#檢查元件名稱是否符合要求

開啟 core/util/options.js 檔案,找到 mergeOptions 方法,這個方法上面有一段註釋:

/**
 * Merge two option objects into a new one.
 * Core utility used in both instantiation and inheritance.
 */

合併兩個選項物件為一個新的物件,這個函式在例項化和繼承的時候都有用到,這裡要注意兩點:第一,這個函式將會產生一個新的物件;第二,這個函式不僅僅在例項化物件(即_init方法中)的時候用到,在繼承(Vue.extend)中也有用到,所以這個函式應該是一個用來合併兩個選項物件為一個新物件的通用程式。

所以我們現在就看看它是怎麼去合併兩個選項物件的,找到 mergeOptions 函式,開始的一段程式碼如下:

if (process.env.NODE_ENV !== 'production') {
  checkComponents(child)
}

在非生產環境下,會以 child 為引數呼叫 checkComponents 方法,我們看看 checkComponents 是做什麼的,這個方法同樣定義在 core/util/options.js 檔案中,內容如下:

/**
 * Validate component names
 */
function checkComponents (options: Object) {
  for (const key in options.components) {
    validateComponentName(key)
  }
}

由註釋可知,這個方法是用來校驗元件的名字是否符合要求的,首先 checkComponents 方法使用一個 for in 迴圈遍歷 options.components 選項,將每個子元件的名字作為引數依次傳遞給 validateComponentName 函式,所以 validateComponentName 函式才是真正用來校驗名字的函式,該函式就定義在 checkComponents 函式下方,原始碼如下:

export function validateComponentName (name: string) {
  if (!/^[a-zA-Z][\w-]*$/.test(name)) {
    warn(
      'Invalid component name: "' + name + '". Component names ' +
      'can only contain alphanumeric characters and the hyphen, ' +
      'and must start with a letter.'
    )
  }
  if (isBuiltInTag(name) || config.isReservedTag(name)) {
    warn(
      'Do not use built-in or reserved HTML elements as component ' +
      'id: ' + name
    )
  }
}

validateComponentName 函式由兩個 if 語句塊組成,所以可想而知,對於元件的名字要滿足這兩條規則才行,這兩條規則就是這兩個 if 分支的條件語句:

  • ①:元件的名字要滿足正則表示式:/^[a-zA-Z][\w-]*$/
  • ②:要滿足:條件 isBuiltInTag(name) || config.isReservedTag(name) 不成立

對於第一條規則,Vue 限定元件的名字由普通的字元和中橫線(-)組成,且必須以字母開頭。

對於第二條規則,首先將 options.components 物件的 key 小寫化作為元件的名字,然後以元件的名字為引數分別呼叫兩個方法:isBuiltInTag 和 config.isReservedTag,其中 isBuiltInTag 方法的作用是用來檢測你所註冊的元件是否是內建的標籤,這個方法可以在 shared/util.js 檔案工具方法全解 中檢視其實現,於是我們可知:slot 和 component 這兩個名字被 Vue 作為內建標籤而存在的,你是不能夠使用的,比如這樣:

new Vue({
  components: {
    'slot': myComponent
  }
})

你將會得到一個警告,該警告的內容就是 checkComponents 方法中的 warn 文案:

除了檢測註冊的元件名字是否為內建的標籤之外,還會檢測是否是保留標籤,即通過 config.isReservedTag 方法進行檢測,大家是否還記得 config.isReservedTag 在哪裡被賦值的?前面我們講到過在 platforms/web/runtime/index.js 檔案中有這樣一段程式碼:

// install platform specific utils
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement

其中:

Vue.config.isReservedTag = isReservedTag

就是在給 config.isReservedTag 賦值,其值為來自於 platforms/web/util/element.js 檔案的 isReservedTag 函式,大家可以在附錄 platforms/web/util 目錄下的工具方法全解 中檢視該方法的作用及實現,可知在 Vue 中 html 標籤和部分 SVG 標籤被認為是保留的。所以這段程式碼是在保證選項被合併前的合理合法。最後大家注意一點,這些工作是在非生產環境下做的,所以在非生產環境下開發者就能夠發現並修正這些問題,所以在生產環境下就不需要再重複做一次校驗檢測了。

另外要說一點,我們的例子中並沒有使用 components 選項,但是這裡還是給大家順便介紹了一下。如果按照我們的例子的話,mergeOptions 函式中的很多程式碼都不會執行,但是為了保證讓大家理解整個選項合併所做的事情,這裡都會有所介紹。

#允許合併另一個例項構造者的選項

我們繼續看程式碼,接下來的一段程式碼同樣是一個 if 語句塊:

if (typeof child === 'function') {
  child = child.options
}

這說明 child 引數除了是普通的選項物件外,還可以是一個函式,如果是函式的話就取該函式的 options 靜態屬性作為新的 child,我們想一想什麼樣的函式具有 options 靜態屬性呢?現在我們知道 Vue 建構函式本身就擁有這個屬性,其實通過 Vue.extend 創造出來的子類也是擁有這個屬性的。所以這就允許我們在進行選項合併的時候,去合併一個 Vue 例項構造者的選項了。

#規範化 props(normalizeProps)

接著看程式碼,接下來是三個用來規範化選項的函式呼叫:

normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)

這三個函式是用來規範選項的,什麼意思呢?以 props 為例,我們知道在 Vue 中,我們在使用 props 的時候有兩種寫法,一種是使用字串陣列,如下:

const ChildComponent = {
  props: ['someData']
}

另外一種是使用物件語法:

const ChildComponent = {
  props: {
    someData: {
      type: Number,
      default: 0
    }
  }
}

其實不僅僅是 props,在 Vue 中擁有多種使用方法的選項有很多,這給開發者提供了非常靈活且便利的選擇,但是對於 Vue 來講,這並不是一件好事兒,因為 Vue 要對選項進行處理,這個時候好的做法就是,無論開發者使用哪一種寫法,在內部都將其規範成同一種方式,這樣在選項合併的時候就能夠統一處理,這就是上面三個函式的作用。

現在我們就詳細看看這三個規範化選項的函式都是怎麼規範選項的,首先是 normalizeProps 函式,這看上去貌似是用來規範化 props 選項的,找到 normalizeProps 函式原始碼如下:

/**
 * Ensure all props option syntax are normalized into the
 * Object-based format.
 */
function normalizeProps (options: Object, vm: ?Component) {
  const props = options.props
  if (!props) return
  const res = {}
  let i, val, name
  if (Array.isArray(props)) {
    i = props.length
    while (i--) {
      val = props[i]
      if (typeof val === 'string') {
        name = camelize(val)
        res[name] = { type: null }
      } else if (process.env.NODE_ENV !== 'production') {
        warn('props must be strings when using array syntax.')
      }
    }
  } else if (isPlainObject(props)) {
    for (const key in props) {
      val = props[key]
      name = camelize(key)
      res[name] = isPlainObject(val)
        ? val
        : { type: val }
    }
  } else if (process.env.NODE_ENV !== 'production') {
    warn(
      `Invalid value for option "props": expected an Array or an Object, ` +
      `but got ${toRawType(props)}.`,
      vm
    )
  }
  options.props = res
}

根據註釋我們知道,這個函式最終是將 props 規範為物件的形式了,比如如果你的 props 是一個字串陣列:

props: ["someData"]

那麼經過這個函式,props 將被規範為:

props: {
  someData:{
    type: null
  }
}

如果你的 props 是物件如下:

props: {
  someData1: Number,
  someData2: {
    type: String,
    default: ''
  }
}

將被規範化為:

props: {
  someData1: {
    type: Number
  },
  someData2: {
    type: String,
    default: ''
  }
}

現在我們具體看一下程式碼,首先是一個判斷,如果選項中沒有 props 選項,則直接 return,什麼都不做:

const props = options.props
if (!props) return

如果選項中有 props,那麼就開始正式的規範化工作,首先聲明瞭四個變數:

const res = {}
let i, val, name

其中 res 變數是用來儲存規範化後的結果的,我們可以發現 normalizeProps 函式的最後一行程式碼使用 res 變數覆蓋了原有的 options.props

options.props = res

然後開始了判斷分支,這個判斷分支就是用來區分開發者在使用 props 時,到底是使用字串陣列的寫法還是使用純物件的寫法的,我們先看字串陣列的情況:

if (Array.isArray(props)) {
  i = props.length
  while (i--) {
    val = props[i]
    if (typeof val === 'string') {
      name = camelize(val)
      res[name] = { type: null }
    } else if (process.env.NODE_ENV !== 'production') {
      warn('props must be strings when using array syntax.')
    }
  }
} else if (isPlainObject(props)) {
  ...
} else if (process.env.NODE_ENV !== 'production') {
  ...
}

如果 props 是一個字串陣列,那麼就使用 while 迴圈遍歷這個陣列,我們看這裡有一個判斷:

if (typeof val === 'string') {
  ...
} else if (process.env.NODE_ENV !== 'production') {
  warn('props must be strings when using array syntax.')
}

也就是說 props 陣列中的元素確確實實必須是字串,否則在非生產環境下會給你一個警告。如果是字串那麼會執行這兩句程式碼:

name = camelize(val)
res[name] = { type: null }

首先將陣列的元素傳遞給 camelize 函式,這個函式來自於 shared/util.js 檔案,可以在附錄 shared/util.js 檔案工具方法全解 中檢視詳細解析,這個函式的作用是將中橫線轉駝峰。

然後在 res 物件上添加了與轉駝峰後的 props 同名的屬性,其值為 { type: null },這就是實現了對字串陣列的規範化,將其規範為物件的寫法,只不過 type 的值為 null

下面我們再看看當 props 選項不是陣列而是物件時的情況:

if (Array.isArray(props)) {
  ...
} else if (isPlainObject(props)) {
  for (const key in props) {
    val = props[key]
    name = camelize(key)
    res[name] = isPlainObject(val)
      ? val
      : { type: val }
  }
} else if (process.env.NODE_ENV !== 'production') {
  ...
}

首先使用 isPlainObject 函式判斷 props 是否是一個純的物件,其中 isPlainObject 函式來自於 shared/util.js 檔案,可以在附錄 shared/util.js 檔案工具方法全解 中檢視詳細解析。

如果是一個純物件,也是需要規範化的,我們知道即使是純物件也是有兩種寫法的,如下:

props: {
  // 第一種寫法,直接寫型別
  someData1: Number,
  // 第二種寫法,物件
  someData2: {
    type: String,
    default: ''
  }
}

最終第一種寫法將被規範為物件的形式,具體實現是採用一個 for in 迴圈,檢測 props 每一個鍵的值,如果值是一個純物件那麼直接使用,否則將值作為 type 的值:

res[name] = isPlainObject(val)
  ? val
  : { type: val }

這樣就實現了對純物件語法的規範化。

最後還有一個判斷分支,即當你傳遞了 props 選項,但其值既不是字串陣列又不是純物件的時候,會給你一個警告:

if (Array.isArray(props)) {
  ...
} else if (isPlainObject(props)) {
  ...
} else if (process.env.NODE_ENV !== 'production') {
  warn(
    `Invalid value for option "props": expected an Array or an Object, ` +
    `but got ${toRawType(props)}.`,
    vm
  )
}

在警告中使用了來自 shared/util.js 檔案的 toRawType 方法獲取你所傳遞的 props 的真實資料型別。

#規範化 inject(normalizeInject)

現在我們已經瞭解了,原來 Vue 底層是這樣處理 props 選項的,下面我們再來看看第二個規範化函式:normalizeInject,原始碼如下:

/**
 * Normalize all injections into Object-based format
 */
function normalizeInject (options: Object, vm: ?Component) {
  const inject = options.inject
  if (!inject) return
  const normalized = options.inject = {}
  if (Array.isArray(inject)) {
    for (let i = 0; i < inject.length; i++) {
      normalized[inject[i]] = { from: inject[i] }
    }
  } else if (isPlainObject(inject)) {
    for (const key in inject) {
      const val = inject[key]
      normalized[key] = isPlainObject(val)
        ? extend({ from: key }, val)
        : { from: val }
    }
  } else if (process.env.NODE_ENV !== 'production') {
    warn(
      `Invalid value for option "inject": expected an Array or an Object, ` +
      `but got ${toRawType(inject)}.`,
      vm
    )
  }
}

首先是這三句程式碼:

const inject = options.inject
if (!inject) return
const normalized = options.inject = {}

第一句程式碼使用 inject 變數快取了 options.inject,通過這句程式碼和函式的名字我們能夠知道,這個函式是用來規範化 inject 選項的。第二句程式碼判斷是否傳遞了 inject 選項,如果沒有則直接 return。然後在第三句程式碼中重寫了 options.inject 的值為一個空的 JSON 物件,並定義了一個值同樣為空 JSON 物件的變數 normalized。現在變數 normalized 和 options.inject 將擁有相同的引用,也就是說當修改 normalized 的時候,options.inject 也將受到影響。

在這兩句程式碼之後,同樣是判斷分支語句,判斷 inject 選項是否是陣列和純物件,類似於對 props 的判斷一樣。說到這裡我們需要了解一下 inject 選項了,這個選項是 2.2.0 版本新增,它要配合 provide 選項一同使用,具體介紹可以檢視官方文件,這裡我們舉一個簡單的例子:

// 子元件
const ChildComponent = {
  template: '<div>child component</div>',
  created: function () {
    // 這裡的 data 是父元件注入進來的
    console.log(this.data)
  },
  inject: ['data']
}

// 父元件
var vm = new Vue({
  el: '#app',
  // 向子元件提供資料
  provide: {
    data: 'test provide'
  },
  components: {
    ChildComponent
  }
})

上面的程式碼中,在子元件的 created 鉤子中我們訪問了 this.data,但是在子元件中我們並沒有定義這個資料,之所以在沒有定義的情況下能夠使用,是因為我們使用了 inject 選項注入了這個資料,這個資料的來源就是父元件通過 provide 提供的。父元件通過 provide 選項向子元件提供資料,然後子元件中可以使用 inject 選項注入資料。這裡我們的 inject 選項使用一個字串陣列,其實我們也可以寫成物件的形式,如下:

// 子元件
const ChildComponent = {
  template: '<div>child component</div>',
  created: function () {
    console.log(this.d)
  },
  // 物件的語法類似於允許我們為注入的資料宣告一個別名
  inject: {
    d: 'data'
  }
}

上面的程式碼中,我們使用物件語法代替了字串陣列的語法,物件語法實際上相當於允許我們為注入的資料宣告一個別名。現在我們已經知道了 inject 選項的使用方法和寫法,其寫法與 props 一樣擁有兩種,一種是字串陣列,一種是物件語法。所以這個時候我們再回過頭去看 normalizeInject 函式,其作用無非就是把兩種寫法規範化為一種寫法罷了,由註釋我們也能知道,最終規範化為物件語法。接下來我們就看看具體實現,首先是 inject 選項是陣列的情況下,如下:

if (Array.isArray(inject)) {
  for (let i = 0; i < inject.length; i++) {
    normalized[inject[i]] = { from: inject[i] }
  }
} else if (isPlainObject(inject)) {
  ...
} else if (process.env.NODE_ENV !== 'production') {
  ...
}

使用 for 迴圈遍歷陣列的每一個元素,將元素的值作為 key,然後將 { from: inject[i] } 作為值。大家不要忘了一件事,那就是 normalized 物件和 options.inject 擁有相同的引用,所以 normalized 的改變就意味著 options.inject 的改變。

也就是說如果你的 inject 選項是這樣寫的:

['data1', 'data2']

那麼將被規範化為:

{
  'data1': { from: 'data1' },
  'data2': { from: 'data2' }
}

當 inject 選項不是陣列的情況下,如果是一個純物件,那麼將走 else if 分支:

if (Array.isArray(inject)) {
  ...
} else if (isPlainObject(inject)) {
  for (const key in inject) {
    const val = inject[key]
    normalized[key] = isPlainObject(val)
      ? extend({ from: key }, val)
      : { from: val }
  }
} else if (process.env.NODE_ENV !== 'production') {
  ...
}

有的同學可能會問:normalized 函式的目的不就是將 inject 選項規範化為物件結構嗎?那既然已經是物件了還規範什麼呢?那是因為我們期望得到的物件是這樣的:

inject: {
  'data1': { from: 'data1' },
  'data2': { from: 'data2' }
}

即帶有 from 屬性的物件,但是開發者所寫的物件可能是這樣的:

let data1 = 'data1'

// 這裡為簡寫,這應該寫在Vue的選項中
inject: {
  data1,
  d2: 'data2',
  data3: { someProperty: 'someValue' }
}

對於這種情況,我們將會把它規範化為:

inject: {
  'data1': { from: 'data1' },
  'd2': { from: 'data2' },
  'data3': { from: 'data3', someProperty: 'someValue' }
}

而實現方式,就是 else if 分支內的程式碼所實現的,即如下程式碼:

for (const key in inject) {
  const val = inject[key]
  normalized[key] = isPlainObject(val)
    ? extend({ from: key }, val)
    : { from: val }
}

使用 for in 迴圈遍歷 inject 選項,依然使用 inject 物件的 key 作為 normalized 的 key,只不過要判斷一下值(即 val)是否為純物件,如果是純物件則使用 extend 進行混合,否則直接使用 val 作為 from 欄位的值,程式碼總體還是很簡單的。

最後一個判斷分支同樣是在當你傳遞的 inject 選項既不是陣列又不是純物件的時候,在非生產環境下給你一個警告:

if (Array.isArray(inject)) {
  ...
} else if (isPlainObject(inject)) {
  ...
} else if (process.env.NODE_ENV !== 'production') {
  warn(
    `Invalid value for option "inject": expected an Array or an Object, ` +
    `but got ${toRawType(inject)}.`,
    vm
  )
}

#規範化 directives(normalizeDirectives)

最後一個規範化函式是 normalizeDirectives,原始碼如下:

/**
 * Normalize raw function directives into object format.
 */
function normalizeDirectives (options: Object) {
  const dirs = options.directives
  if (dirs) {
    for (const key in dirs) {
      const def = dirs[key]
      if (typeof def === 'function') {
        dirs[key] = { bind: def, update: def }
      }
    }
  }
}

看其程式碼內容,應該是規範化 directives 選項的。我們知道 directives 選項用來註冊區域性指令,比如下面的程式碼我們註冊了兩個區域性指令分別是 v-test1 和 v-test2

<div id="app" v-test1 v-test2>{{test}}</div>

var vm = new Vue({
  el: '#app',
  data: {
    test: 1
  },
  // 註冊兩個區域性指令
  directives: {
    test1: {
      bind: function () {
        console.log('v-test1')
      }
    },
    test2: function () {
      console.log('v-test2')
    }
  }
})

上面的程式碼中我們註冊了兩個區域性指令,但是註冊的方法不同,其中 v-test1 指令我們使用物件語法,而 v-test2 指令我們使用的則是一個函式。所以既然出現了允許多種寫法的情況,那麼當然要進行規範化了,而規範化的手段就如同 normalizeDirectives 程式碼中寫的那樣:

for (const key in dirs) {
  const def = dirs[key]
  if (typeof def === 'function') {
    dirs[key] = { bind: def, update: def }
  }
}

注意 if 判斷語句,當發現你註冊的指令是一個函式的時候,則將該函式作為物件形式的 bind 屬性和 update 屬性的值。也就是說,可以把使用函式語法註冊元件的方式理解為一種簡寫。

這樣,我們就徹底瞭解了這三個用於規範化選項的函式的作用了,相信通過上面的介紹,大家對 propsinject 以及 directives 這三個選項會有一個新的認識。知道了 Vue 是如何做到允許我們採用多種寫法,也知道了 Vue 是如何統一處理的,這也算是看原始碼的收穫之一吧。

看完了 mergeOptions 函式裡的三個規範化函式之後,我們繼續看後面的程式碼,接下來是這樣一段程式碼:

const extendsFrom = child.extends
if (extendsFrom) {
  parent = mergeOptions(parent, extendsFrom, vm)
}
if (child.mixins) {
  for (let i = 0, l = child.mixins.length; i < l; i++) {
    parent = mergeOptions(parent, child.mixins[i], vm)
  }
}

很顯然,這段程式碼是處理 extends 選項和 mixins 選項的,首先使用變數 extendsFrom 儲存了對 child.extends 的引用,之後的處理都是用 extendsFrom 來做,然後判斷 extendsFrom 是否為真,即 child.extends 是否存在,如果存在的話就遞迴呼叫 mergeOptions 函式將 parent 與 extendsFrom進行合併,並將結果作為新的 parent。這裡要注意,我們之前說過 mergeOptions 函式將會產生一個新的物件,所以此時的 parent 已經被新的物件重新賦值了。

接著檢測 child.mixins 選項是否存在,如果存在則使用同樣的方式進行操作,不同的是,由於 mixins是一個數組所以要遍歷一下。

經過了上面兩個判斷分支,此時的 parent 很可能已經不是當初的 parent 的,而是經過合併後產生的新物件。關於 extends 與 mixins 的更多東西以及這裡遞迴呼叫 mergeOptions 所產生的影響,等我們看完整個 mergeOptions 函式對選項的處理之後會更容易理解,因為現在我們還不清楚 mergeOptions到底怎麼合併選項,等我們瞭解了 mergeOptions 的作用之後再回頭來看一下這段程式碼。

到目前為止我們所看到的 mergeOptions 的程式碼,還都是對選項的規範化,或者說的明顯一點:現在所做的事兒還都在對 parent 以及 child 進行預處理,而這是接下來合併選項的必要步驟。