上篇文章中咱們講了resolveConstructorOptions,它的主要功能是解析當前實例構造函數上的options,不太明白的同窗們能夠看本系列的前幾篇文章。在解析完構造函數上的options以後,須要把構造函數上的options和實例化時傳入的options進行合併操做,並生成一個新的options。html
這個合併操做就是今天要講的mergeOptions。若是你們不想看枯燥的講解,能夠翻到文章最後,查看整個mergeOptions的流程圖。vue
Merge two option objects into a new one. Core utility used in both instantiation and inheritance.數組
先來看源碼中對mergeOptions方法的註釋。mergeOptions的功能是合併兩個options對象,並生成一個新的對象,是實例化和繼承中使用的核心方法。既然這麼重要,那我就帶你們一行一行的來解析代碼,讓你們看完這篇文章後,能基本上理解mergeOptions。bash
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)
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)
}
}
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
}
複製代碼
首先看傳入的三個參數,parent,child,vm,這三個參數分別表明的是該實例構造函數上的options,實例化時傳入的options,vm實例自己。結合Vue做者寫的註釋,咱們明白了,原來mergeoptions方法是要合併構造函數和傳入的options這兩個對象。接下來往下看app
if (process.env.NODE_ENV !== 'production') {
checkComponents(child) // 檢查組件名稱是否合法
}
複製代碼
這段代碼主要是判斷當前環境是否是生產環境,若是不是,則調用checkComponents方法來檢查組件名稱是不是可用名稱?咱們來看看checkComponents的邏輯ide
function checkComponents (options: Object) {
for (const key in options.components) {
validateComponentName(key)
}
}
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
)
}
}
複製代碼
若是child的options(實例化傳入的options)有components屬性,以下面這種狀況svg
const app = new Vue({
el: '#app',
...
components: {
childComponent
}
...
})
複製代碼
那麼就調用validateComponentName來驗證傳入的組件名稱是否符合如下特徵函數
若是知足第一條,而且第2,3條都是不相同的話,那麼組件名稱可用。 咱們再回到mergeOptions源碼中post
if (typeof child === 'function') {
child = child.options
}
複製代碼
若是child是function類型的話,取其options屬性做爲child。 接下來看這三個以normalize開頭的方法ui
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
複製代碼
這三個方法的功能相似,分別是把options中props,inject,directives屬性轉換成對象的形式。有時候傳入的是數組,以下面這種狀況
Vue.component('blog-post', {
props: ['postTitle'],
template: '<h3>{{ postTitle }}</h3>'
})
複製代碼
咱們先來看props處理的邏輯
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
}
複製代碼
首先明確這兩個方法裏的參數是什麼,options傳入的是child,即實例化時傳入的options,而vm是實例。繼續看源碼
const props = options.props
if (!props) return
const res = {}
let i, val, name
複製代碼
上面的代碼主要是聲明一些變量。res用來存放修改後的props,最後把res賦給新的props。下面的邏輯能夠分爲兩種狀況來考慮
當props是數組的時候,以下面這種狀況
Vue.component('blog-post', {
props: ['postTitle'],
template: '<h3>{{ postTitle }}</h3>'
})
複製代碼
它的處理邏輯是:遍歷props數組,把數組的每一項的值做爲res對象的key,value值等於{type: null}。即把上面例子中的['postTitle']轉換成下面這種形式
{
postTitle: { type: null }
}
複製代碼
當props是對象時,以下面這種狀況
Vue.component('my-component', {
props: {
// 必填的字符串
propC: {
type: String,
required: true
}
}
})
複製代碼
這種狀況的處理邏輯是遍歷對象,先把對象的key值轉換成駝峯的形式。而後再判斷對象的值,若是是純對象(即調用object.prototype.toString方法的結果是[object Object]),則直接把對象的值賦值給res,若是不是,則把{ type: 對象的值}賦給res。最終會轉換成
{
propC: {
type: String,
required: true
}
}
複製代碼
若是傳入的props不是純對象也不是數組,且當前環境也不是生產環境,則拋出警告。
warn(
`Invalid value for option "props": expected an Array or an Object, ` +
`but got ${toRawType(props)}.`,
vm
)
複製代碼
最後,把處理過的props從新賦值給options.props。
這個方法的邏輯和normalizeProps相似,主要是處理inject。inject屬性若是你們平時不是寫庫或者插件的話,可能不多接觸到,能夠點擊inject的使用查看。inject的傳入和props相似,能夠傳入object,也能夠傳入array
// array
var Child = {
inject: ['foo'],
created () {
console.log(this.foo) // => "bar"
}
// ...
}
// object
const Child = {
inject: {
foo: {
from: 'bar',
default: 'foo'
}
}
}
複製代碼
因爲這個方法和normalizeProps邏輯基本同樣,這裏也不具體展開講了。上面的demo最終會被轉換成以下形式
// array
{
foo: { from: 'foo'}
}
// object
{
foo: {
from: 'bar',
default: 'foo'
}
}
複製代碼
這個方法主要是處理一些自定義指令,若是不瞭解自定義指令的同窗能夠點擊自定義指令查看。這裏的方法處理邏輯主要針對自定義指令中函數簡寫的狀況。以下
Vue.directive('color', function (el, binding) {
el.style.backgroundColor = binding.value
})
複製代碼
normalizeDirectives構造函數會把這個指令傳入的參數,最終轉換成下面這種形式
color: {
bind: function (el, binding) {
el.style.backgroundColor = binding.value
},
update: function (el, binding) {
el.style.backgroundColor = binding.value
}
}
複製代碼
講完了三個以normalize開頭的方法,咱們回到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)
}
}
複製代碼
這段代碼的處理的邏輯是,當傳入的options裏有mixin或者extends屬性時,再次調用mergeOptions方法合併mixins和extends裏的內容到實例的構造函數options上(即parent options)好比下面這種狀況
const childComponent = Vue.component('child', {
...
mixins: [myMixin],
extends: myComponent
...
})
const myMixin = {
created: function () {
this.hello()
},
methods: {
hello: function () {
console.log('hello from mixin')
}
}
}
const myComponent = {
mounted: function () {
this.goodbye()
},
methods: {
goodbye: function () {
console.log('goodbye from mixin')
}
}
}
複製代碼
把傳入的mounted, created鉤子處理函數,還有methods方法和parent options作合併處理。繼續看源碼
const options = {}
let key
複製代碼
變量options存儲合併以後的options,變量key存儲parent options和child options上的key值。接下來的部分算是mergeOptions方法的核心處理部分了,像炒菜同樣,前面的代碼至關於把全部的菜都配好了,接下來的部分就是教你怎麼去炒菜了。
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)
}
複製代碼
前兩段for循環代碼大同小異,都是遍歷options上的key值,而後調用mergeField方法來處理options。mergeField方法中出現了一個變量strats和defaultStrat,這兩個變量存儲的就是咱們的合併策略,也就是炒菜的菜譜,咱們先來看看defaultStrat
const defaultStrat = function (parentVal: any, childVal: any): any {
return childVal === undefined
? parentVal
: childVal
}
複製代碼
defaultStrat的邏輯是,若是child上該屬性值存在時,就取child上的屬性值,若是不存在,則取parent上的屬性值。
如今咱們知道默認的合併策略是什麼了,接下來看其餘的合併策略。咱們來看看strats裏都有哪些屬性?
上圖就是strats中全部的策略了,粗略看起來裏面的內容很是的多,若是細細分析會發現,其實總結起來無非就是幾種合併策略。全部關於鉤子函數的策略,其實都是調用mergeHook方法。
function mergeHook (
parentVal: ?Array<Function>,
childVal: ?Function | ?Array<Function>
): ?Array<Function> {
return childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
}
複製代碼
mergeHook採用了一個很是騷的嵌套三元表達式來控制最後的返回值。下面咱們來解析這段三元表達式 (1) child options上不存在該屬性,parent options上存在,則返回parent上的屬性。
(2)child和parent都存在該屬性,則返回concat以後的屬性
(3)child上存在該屬性,parent不存在,且child上的該屬性是Array,則直接返回child上的該屬性
(4) child上存在該屬性,parent不存在,且child上的該屬性不是Array,則把該屬性先轉換成Array,再返回。
上面就是鉤子函數合併策略,結合圖片看應該會比較清晰。介紹完了鉤子函數的合併策略,咱們接下來看props,methods,inject,computed等屬性的合併策略。
strats.props =
strats.methods =
strats.inject =
strats.computed = function ( parentVal: ?Object, childVal: ?Object, vm?: Component, key: string ): ?Object {
if (childVal && process.env.NODE_ENV !== 'production') {
assertObjectType(key, childVal, vm)
}
if (!parentVal) return childVal
const ret = Object.create(null)
extend(ret, parentVal)
if (childVal) extend(ret, childVal)
return ret
}
複製代碼
這個合併方法邏輯很簡單,若是child options上這些屬性存在,則先判斷它們是否是對象。
components/directives/filters這幾個屬性的處理邏輯以下
function mergeAssets ( parentVal: ?Object, childVal: ?Object, vm?: Component, key: string ): Object {
const res = Object.create(parentVal || null)
if (childVal) {
process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
return extend(res, childVal)
} else {
return res
}
}
複製代碼
這裏的處理邏輯和上一種狀況的相似,這裏不作過多講解。
data和provide的策略相對來講複雜一些,咱們先來看代碼
export function mergeDataOrFn ( parentVal: any, childVal: any, vm?: Component ): ?Function {
if (!vm) {
// in a Vue.extend merge, both should be functions
if (!childVal) {
return parentVal
}
if (!parentVal) {
return childVal
}
// when parentVal & childVal are both present,
// we need to return a function that returns the
// merged result of both functions... no need to
// check if parentVal is a function here because
// it has to be a function to pass previous merges.
return function mergedDataFn () {
return mergeData(
typeof childVal === 'function' ? childVal.call(this, this) : childVal,
typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
)
}
} else {
return function mergedInstanceDataFn () {
// instance merge
const instanceData = typeof childVal === 'function'
? childVal.call(vm, vm)
: childVal
const defaultData = typeof parentVal === 'function'
? parentVal.call(vm, vm)
: parentVal
if (instanceData) {
return mergeData(instanceData, defaultData)
} else {
return defaultData
}
}
}
}
複製代碼
這個合併策略能夠分紅兩種狀況來考慮。
第一種狀況,當前調用mergeOptions操做的是vm實例(調用new新建vue實例觸發mergeOptions方法)
return function mergedInstanceDataFn () {
// instance merge
const instanceData = typeof childVal === 'function'
? childVal.call(vm, vm)
: childVal
const defaultData = typeof parentVal === 'function'
? parentVal.call(vm, vm)
: parentVal
if (instanceData) {
return mergeData(instanceData, defaultData)
} else {
return defaultData
}
}
複製代碼
若是新建實例時傳入的options上有data屬性,則調用mergeData方法合併實例上的data屬性和其構造函數options上的data屬性(若是有的話)
第二種狀況,當前調用mergeOptions操做的不是vm實例(即經過Vue.extend/Vue.component調用了mergeOptions方法)
if (!vm) {
// in a Vue.extend merge, both should be functions
if (!childVal) {
return parentVal
}
if (!parentVal) {
return childVal
}
// when parentVal & childVal are both present,
// we need to return a function that returns the
// merged result of both functions... no need to
// check if parentVal is a function here because
// it has to be a function to pass previous merges.
return function mergedDataFn () {
return mergeData(
typeof childVal === 'function' ? childVal.call(this, this) : childVal,
typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
)
}
}
複製代碼
在這種狀況下,其處理邏輯也是相似的。若是當前實例options或者構造函數options上有一個沒有data屬性,則返回另外一個的data屬性,若是二者都有,則一樣調用mergeData方法處理合並。
既然這兩種狀況都調用了mergeData方法,那咱們就繼續來看看mergeData的源碼。
function mergeData (to: Object, from: ?Object): Object {
if (!from) return to
let key, toVal, fromVal
const keys = Object.keys(from)
for (let i = 0; i < keys.length; i++) {
key = keys[i]
toVal = to[key]
fromVal = from[key]
if (!hasOwn(to, key)) {
set(to, key, fromVal)
} else if (isPlainObject(toVal) && isPlainObject(fromVal)) {
mergeData(toVal, fromVal)
}
}
return to
}
複製代碼
mergeData的邏輯是,若是from對象中有to對象裏沒有的屬性,則調用set方法,(這裏的set就是Vue.$set,先能夠簡單理解爲對象設置屬性。以後會細講)若是from和to中有相同的key值,且key對應的value是對象,則會遞歸調用mergeData方法,不然以to的值爲準,最後返回to對象。
這裏咱們就講完了data的合併策略,返回mergeOptions代碼裏,在通過這幾種合併策略合併options後,最終返回合併後的options。
return options
複製代碼
講到這裏,整個mergeOptions的流程也全講完了。這個方法牽扯到的內容比較多,流程也比較複雜,爲了你們更好的理解和記憶,我畫了一張圖來表達整個mergeOptions的過程。
若是你們以爲個人文章寫的還行,請爲我點贊,大家的承認是我最大的動力。