Vue的mergeOptions函式分析-上
Vue的mergeOptions函式的主要作用是用於合併選項(將倆個選項物件合併成一個),它是用於例項化和繼承的核心函式。這也是為什麼我們要去分析它。並且與函式相關的選項合併策略也都在一個檔案裡,定義在 /src/core/util/options.js
檔案中。
使用場景
因為Vue的核心程式碼都是放在src資料夾下,所以我們可以在src目錄下全域性搜尋下 mergeOptions
的使用場景,可以發現函式在 Vue.extend
、 Vue.mixin
、 例項化
都有用到。(只考慮web平臺)
// src/core/global-api/extend.js檔案中 Vue.extend = function (extendOptions: Object): Function { // ... 忽略無關程式碼 Sub.options = mergeOptions( Super.options, extendOptions ) } // src/core/global-api/mixin.js檔案中 Vue.mixin = function (mixin: Object) { this.options = mergeOptions(this.options, mixin) return this } // src/core/instance/init.js檔案中 執行new 例項化的時候會執行 Vue.prototype._init = function (options?: Object) { // ... 忽略無關程式碼 vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } 複製程式碼
這也證實了mergeOptions函式的註釋所寫的一樣, Core utility used in both instantiation and inheritance.
。
逐行分析
mergeOptions函式被定義在 /src/core/util/options.js
檔案中,原始碼如下:
/** * Merge two option objects into a new one. * Core utility used in both instantiation and inheritance. */ export function mergeOptions ( parent: Object, child: Object, vm?: Component ): Object { if (process.env.NODE_ENV !== 'production') { checkComponents(child) } if (typeof child === 'function') { child = child.options } normalizeProps(child, vm) normalizeInject(child, vm) normalizeDirectives(child) // Apply extends and mixins on the child options, // but only if it is a raw options object that isn't // the result of another mergeOptions call. // Only merged options has the _base property. if (!child._base) { if (child.extends) { parent = mergeOptions(parent, child.extends, vm) } if (child.mixins) { for (let i = 0, l = child.mixins.length; i < l; i++) { parent = mergeOptions(parent, child.mixins[i], vm) } } } const options = {} let key for (key in parent) { mergeField(key) } for (key in child) { if (!hasOwn(parent, key)) { mergeField(key) } } function mergeField (key) { const strat = strats[key] || defaultStrat options[key] = strat(parent[key], child[key], vm, key) } return options } 複製程式碼
我們先看函式接受的引數,這裡有一點過要注意, mergeOptions函式第三個引數的可選的,可以不傳。 Vue.mixin
、 Vue.extend
函式中呼叫mergeOptions的時候是不傳第三個引數的 。選項的合併策略函式會根據vm引數來確定是 例項化選項合併 還是 繼承選項合併 ,從而做不同的處理,這個後面會詳細講到。
函式第一行,檢查非生產環境下,執行 checkComponents
函式,該函式定義在同一檔案下,主要是檢查元件的名字是否符合規範。可以看到核心函式是 validateComponentName
,而且它被暴露出去,因為在 Vue.component()
、 Vue.extend()
函式中都有用到。
if (process.env.NODE_ENV !== 'production') { checkComponents(child) } /** * 檢驗元件的名字 */ function checkComponents (options: Object) { // 遍歷物件的components屬性,依次檢驗 for (const key in options.components) { validateComponentName(key) } } // 如果檢驗不通過,給出相應警告 export function validateComponentName (name: string) { // 符合HTML5規範,由普通字元和中橫線(-)組成,並且必須以字母開頭。 if (!new RegExp(`^[a-zA-Z][\\-\\.0-9_${unicodeRegExp.source}]*$`).test(name)) { warn( 'Invalid component name: "' + name + '". Component names ' + 'should conform to valid custom element name in html5 specification.' ) } // isBuiltInTag是檢驗名字不能與slot,component重名 // isReservedTag是檢驗不能與html、svg內建標籤重名 if (isBuiltInTag(name) || config.isReservedTag(name)) { warn( 'Do not use built-in or reserved HTML elements as component ' + 'id: ' + name ) } } 複製程式碼
接下來是檢查傳入的child是否是函式,如果是的話,取到它的options選項重新賦值給child。所以說child引數可以是普通選項物件,也可以是Vue建構函式和通過 Vue.extend
繼承的子類建構函式。(Vue.options定義在 src/core/global-api/index.js
檔案中)
if (typeof child === 'function') { child = child.options } 複製程式碼
再往後看有三個函式,分別是 normalizeProps
、 normalizeInject
、 normalizeDirectives
,它們的作用是規範化選項,用過Vue的同學應該都知道,我們在寫 props
、 inject
既可以是字串陣列,也可以是物件。 directives
既可以是一個函式,也可以是物件。Vue對外提供了便捷的寫法,但內部處理要把他們規範成一樣,才更方便處理。其實三個函式都是將選項轉換物件的形式,接下來我們會逐個分析。
規範化props
function normalizeProps (options: Object, vm: ?Component) { // 定義props,是選項中的props屬性的引用 const props = options.props if (!props) return const res = {} let i, val, name // 1. 是陣列的情況 例如:['name', 'age'] if (Array.isArray(props)) { i = props.length // 迴圈遍歷變成物件格式{ type: null } while (i--) { val = props[i] if (typeof val === 'string') { name = camelize(val) // 將key值變成駝峰形式 res[name] = { type: null } } else if (process.env.NODE_ENV !== 'production') { // 如果不是字串陣列,非生產環境給出警告 warn('props must be strings when using array syntax.') } } } else if (isPlainObject(props)) { // 2. 是物件 for (const key in props) { val = props[key] name = camelize(key) // 如果是物件,則直接賦值,不是的話,則賦值type屬性 // 例如 { sex: String, job: { type: String, default: 'xxx' } } 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 options.props = res } 複製程式碼

['name', 'age']
),說明只指定了key值,只需要將陣列遍歷,轉成物件形式,把type屬性設定null,當傳入的是物件時,又分為倆種情況,一種是key值對應的是物件,那直接賦值就好。否則那代表只指定了型別(例如:
{ sex: String, }
),同樣轉成物件形式。
規範化inject
function normalizeInject (options: Object, vm: ?Component) { // 取到options.inject的引用 const inject = options.inject if (!inject) return // 重置物件,之後重新賦值屬性 const normalized = options.inject = {} // 1. 陣列情況,直接遍歷。與normalizeProps同理 if (Array.isArray(inject)) { for (let i = 0; i < inject.length; i++) { normalized[inject[i]] = { from: inject[i] } } } else if (isPlainObject(inject)) { // 2. 物件情況。如果key值對應的是物件,則通過exntend合併,如果不是,則代表直接是from 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 ) } } 複製程式碼

extend
合併物件。因為from屬性不能為空,所以如果物件中沒有from屬性,預設還是賦予同名的from。否則就會被覆蓋。例如:如上圖中的age屬性的from值
parentAge
就會覆蓋預設的age,而job屬性沒有指定from,所以會賦予同名的from屬性。
規範化directives
function normalizeDirectives (options: Object) { const dirs = options.directives // 遍歷物件,如果key值對應的是函式。則修改成物件形式。 // Vue提供了自定義指令的簡寫,如果只傳函式,等同於{ bind: func, update: func } if (dirs) { for (const key in dirs) { const def = dirs[key] if (typeof def === 'function') { dirs[key] = { bind: def, update: def } } } } } 複製程式碼
以上三個函式每個if分支都是根據Vue提供的feature來進行不同的處理,其根本目的就是為了使傳入的引數統一。如果你對哪個分支還有疑惑,可以去閱讀下相關的官方文件。props、 inject 、 directives 。
我們回到 mergeOptions
函式繼續往下看,這裡判斷沒有_base屬性的話( 被合併過不再處理,只有合併過的選項會帶有_base屬性 ),處理子選項的extend、mixins,處理方法就是將extend和mixins再通過 mergeOptions
函式與parent合併,因為mergeOptions函式合併後會返回新的物件,所以這時parent已經是個嶄新的物件啦。
if (!child._base) { // 如果有extends屬性(`extends: xxx`),則還是呼叫mergeOptions函式返回的結果賦值給parent if (child.extends) { parent = mergeOptions(parent, child.extends, vm) } // 如果有mixins屬性(`mixins: [xxx, xxx]`) // 則遍歷陣列,遞迴呼叫mergeOptions,結果也賦值給parent if (child.mixins) { for (let i = 0, l = child.mixins.length; i < l; i++) { parent = mergeOptions(parent, child.mixins[i], vm) } } } 複製程式碼
接下來的最後一段程式碼如下:
// 定義options為空物件,最後函式返回結果是options const options = {} let key // 先遍歷parent執行mergeField for (key in parent) { mergeField(key) } // 再遍歷child,當parent沒有key的時候,在執行mergeField。 // 如果有key屬性,就不需要合併啦,因為上一步已經合併到options上了 for (key in child) { if (!hasOwn(parent, key)) { mergeField(key) } } // 該函式主要是通過key獲取到對應的合併策略函式,然後執行合併,賦值給options[key] function mergeField (key) { const strat = strats[key] || defaultStrat options[key] = strat(parent[key], child[key], vm, key) } return options 複製程式碼
到最後可以知道, mergeOptions
函式進行真正的合併是最後一段程式碼,前面都是對選項進行規範化,以及 extend
、 mixins
進行遞迴合併。那strat是啥呢?其實它是檔案頂部定義的一個物件,它是 config.optionMergeStrategies
的引用,並且在之後對特殊的合併策略進行了重寫,比如說 el
、 data
、 鉤子函式
、 components
、 props
、 methods
等等。合併策略相關的程式碼我們在下一篇進行分析。