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.super
,super
這是子類才有的屬性,如下:
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
屬性的值。也就是說,可以把使用函式語法註冊元件的方式理解為一種簡寫。
這樣,我們就徹底瞭解了這三個用於規範化選項的函式的作用了,相信通過上面的介紹,大家對 props
、inject
以及 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
進行預處理,而這是接下來合併選項的必要步驟。