Vue的mergeOptions函數的主要做用是用於合併選項(將倆個選項對象合併成一個),它是用於實例化和繼承的核心函數。這也是爲何咱們要去分析它。而且與函數相關的選項合併策略也都在一個文件裏,定義在/src/core/util/options.js
文件中。javascript
由於Vue的核心代碼都是放在src文件夾下,因此咱們能夠在src目錄下全局搜索下mergeOptions
的使用場景,能夠發現函數在Vue.extend
、Vue.mixin
、實例化
都有用到。(只考慮web平臺)html
// 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.
。vue
mergeOptions函數被定義在/src/core/util/options.js
文件中,源代碼以下:html5
/** * 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參數來肯定是實例化選項合併仍是繼承選項合併,從而作不一樣的處理,這個後面會詳細講到。java
函數第一行,檢查非生產環境下,執行checkComponents
函數,該函數定義在同一文件下,主要是檢查組件的名字是否符合規範。能夠看到核心函數是validateComponentName
,並且它被暴露出去,由於在Vue.component()
、Vue.extend()
函數中都有用到。web
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
文件中)api
if (typeof child === 'function') {
child = child.options
}
複製代碼
再日後看有三個函數,分別是normalizeProps
、normalizeInject
、normalizeDirectives
,它們的做用是規範化選項,用過Vue的同窗應該都知道,咱們在寫props
、inject
既能夠是字符串數組,也能夠是對象。directives
既能夠是一個函數,也能夠是對象。Vue對外提供了便捷的寫法,但內部處理要把他們規範成同樣,才更方便處理。其實三個函數都是將選項轉換對象的形式,接下來咱們會逐個分析。數組
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
}
複製代碼
normalizeProps函數仍是比較簡單的,如上圖。當傳入是字符串數組時(例如:
['name', 'age']
),說明只指定了key值,只須要將數組遍歷,轉成對象形式,把type屬性設置null,當傳入的是對象時,又分爲倆種狀況,一種是key值對應的是對象,那直接賦值就好。不然那表明只指定了類型(例如:
{ sex: String, }
),一樣轉成對象形式。
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
)
}
}
複製代碼
其實normalizeInject和normalizeProps函數很類似,都是分爲對象和字符串數組倆種大狀況,對象又分爲倆種小狀況。這裏判斷key值對應的是對象時,多作了一步處理就是用
extend
合併對象。由於from屬性不能爲空,因此若是對象中沒有from屬性,默認仍是賦予同名的from。不然就會被覆蓋。例如:如上圖中的age屬性的from值
parentAge
就會覆蓋默認的age,而job屬性沒有指定from,因此會賦予同名的from屬性。
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。ide
咱們回到mergeOptions
函數繼續往下看,這裏判斷沒有_base屬性的話(被合併過再也不處理,只有合併過的選項會帶有_base屬性),處理子選項的extend、mixins,處理方法就是將extend和mixins再經過mergeOptions
函數與parent合併,由於mergeOptions函數合併後會返回新的對象,因此這時parent已是個嶄新的對象啦。svg
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
等等。合併策略相關的代碼咱們在下一篇進行分析。