Vue本質是上來講是一個函數,在其經過new關鍵字構造調用時,會完成一系列初始化過程。經過Vue框架進行開發,基本上是經過向Vue函數中傳入不一樣的參數選項來完成的。參數選項每每須要加以合併,主要有兩種狀況:html
一、Vue函數自己擁有一些靜態屬性,在實例化時開發者會傳入同名的屬性。
二、在使用繼承的方式使用Vue時,須要將父類和子類上同名屬性加以合併。
vue
Vue函數定義在 /src/core/instance/index.js中。
ios
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
initMixin(Vue)
複製代碼
在Vue實例化時會將選項集 options 傳入到實例原型上的 _init 方法中加以初始化。 initMixin 函數的做用就是向Vue實例的原型對象上添加 _init 方法, initMixin 函數在 /src/core/instance/init.js 中定義。
在 _init 函數中,會對傳入的選項集進行合併處理。
web
// merge options
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
複製代碼
在開發過程當中基本不會傳入 _isComponent 選項,所以在實例化時走 else 分支。經過 mergeOptions 函數來返回合併處理以後的選項並將其賦值給實例的 $options 屬性。 mergeOptions 函數接收三個參數,其中第一個參數是將生成實例的構造函數傳入 resolveConstructorOptions 函數中處理以後的返回值。
npm
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
}
複製代碼
resolveConstructorOptions 函數的參數爲實例的構造函數,在構造函數的沒有父類時,簡單的返回構造函數的 options 屬性。反之,則走 if 分支,合併處理構造函數及其父類的 options 屬性,如若構造函數的父類仍存在父類則遞歸調用該方法,最終返回惟一的 options 屬性。在研究實例化合並選項時,爲行文方便,將該函數返回的值統一稱爲選項合併的父選項集合,實例化時傳入的選項集合稱爲子選項集合。
json
在合併選項時,在沒有繼承關係存在的狀況,傳入的第一個參數爲Vue構造函數上的靜態屬性 options ,那麼這個靜態屬性到底包含什麼呢?爲了弄清楚這個問題,首先要搞清楚運行 npm run dev 命令來生成 /dist/vue.js 文件的過程當中發生了什麼。
在 package.json 文件中 scripts 對象中有:api
"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev",
複製代碼
在使用rollup打包時,依據 scripts/config.js 中的配置,並將 web-full-dev 做爲環境變量TARGET的值。
數組
// Runtime+compiler development build (Browser)
'web-full-dev': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.js'),
format: 'umd',
env: 'development',
alias: { he: './entity-decoder' },
banner
},
複製代碼
上述文件路徑是在 scripts/alias.js 文件中配置過別名的。由此可知,執行 npm run dev 命令時,入口文件爲 src/platforms/web/entry-runtime-with-compiler.js ,生成符合 umd 規範的 vue.js 文件。依照該入口文件對Vue函數的引用,按圖索驥,逐步找到Vue構造函數所在的文件。以下圖所示:
瀏覽器
Vue構造函數定義在 /src/core/instance/index.js中。在該js文件中,經過各類Mixin向 Vue.prototype 上掛載一些屬性和方法。以後在 /src/core/index.js 中,經過 initGlobalAPI 函數向Vue構造函數上添加靜態屬性和方法。
框架
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'
initGlobalAPI(Vue)
複製代碼
在initGlobalAPI 函數中有向Vue構造函數中添加 options 屬性的定義。
Vue.options = Object.create(null)
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
// this is used to identify the "base" constructor to extend all plain-object
// components with in Weex's multi-instance scenarios.
Vue.options._base = Vue
extend(Vue.options.components, builtInComponents)
複製代碼
通過這段代碼處理之後,Vue.options 變成這樣:
Vue.options = {
components: {
KeepAlive
},
directives: Object.create(null),
filters: Object.create(null),
_base: Vue
}
複製代碼
在 /src/platforms/web/runtime/index.js 中,經過以下代碼向 Vue.options 屬性上添加平臺化指令以及內置組件。
import platformDirectives from './directives/index'
import platformComponents from './components/index'
// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
複製代碼
最終 Vue.options 屬性內容以下所示:
Vue.options = {
components: {
KeepAlive,
Transition,
TransitionGroup
},
directives: {
model,
show
},
filters: Object.create(null),
_base: Vue
}
複製代碼
合併選項的函數 mergeOptions 在 /src/core/util/options.js 中定義。
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)
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
}
複製代碼
合併選項時,在非生產環境下首先檢測聲明的組件名稱是否合乎標準:
if (process.env.NODE_ENV !== 'production') {
checkComponents(child)
}
複製代碼
checkComponents 函數是 對子選項集合的 components 屬性中每一個屬性使用 validateComponentName 函數進行命名有效性檢測。
function checkComponents (options: Object) {
for (const key in options.components) {
validateComponentName(key)
}
}
複製代碼
validateComponentName 函數定義了組件命名的規則:
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
)
}
}
複製代碼
由上述代碼可知,有效性命名規則有兩條:
一、組件名稱可使用字母、數字、符號 _、符號 - ,且必須以字母爲開頭。
二、組件名稱不能是Vue內置標籤 slot 和 component;不能是 html內置標籤;不能使用部分SVG標籤。
傳入Vue的選項形式每每有多種,這給開發者提供了便利。在Vue內部合併選項時卻要把各類形式進行標準化,最終轉化成一種形式加以合併。
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
複製代碼
上述三條函數調用分別標準化選項 props 、inject 、directives 。
props 選項有兩種形式:數組、對象,最終都會轉化成對象的形式。
若是props 選項是數組,則數組中的值必須都爲字符串。若是字符串擁有連字符則轉成駝峯命名的形式。好比:
props: ['propOne', 'prop-two']
複製代碼
該props將被規範成:
props: {
propOne:{
type: null
},
propTwo:{
type: null
}
}
複製代碼
若是props 選項是對象,其屬性有兩種形式:字符串、對象。屬性名有連字符則轉成駝峯命名的形式。若是屬性是對象,則不變;若是屬性是字符串則轉變成對象,屬性值變成新對象的 type 屬性。好比:
props: {
propOne: Number,
"prop-two": Object,
propThree: {
type: String,
default: ''
}
}
複製代碼
該props將被規範成:
props: {
propOne: {
type: Number
},
propTwo: {
type: Object
},
propThree: {
type: String,
default: ''
}
}
複製代碼
props對象的屬性值爲對象時,該對象的屬性值有效的有四種:
一、type:基礎的類型檢查。
二、required: 是否爲必須傳入的屬性。
三、default:默認值。
四、validator:自定義驗證函數。
inject 選項有兩種形式:數組、對象,最終都會轉化成對象的形式。
若是inject 選項是數組,則轉化爲對象,對象的屬性名爲數組的值,屬性的值爲僅擁有 from 屬性的對象, from 屬性的值爲與數組對應的值相同。好比:
inject: ['test']
複製代碼
該 inject 將被規範成:
inject: {
test: {
from: 'test'
}
}
複製代碼
若是inject 選項是對象,其屬性有三種形式:字符串、symbol、對象。若是是對象,則添加屬性 from ,其值與屬性名相等。若是是字符串或者symbol,則轉化爲對象,對象擁有屬性 from ,其值等於該字符串或symbol。好比:
inject: {
a: 'value1',
b: {
default: 'value2'
}
}
複製代碼
該 inject 將被規範成:
inject: {
a: {
from: 'value1'
},
b: {
from: 'b',
default: 'value2'
}
}
複製代碼
自定義指令選項 directives 只接受對象類型。通常具體的自定義指令是一個對象。 directives 選項的寫法較爲統一,那麼爲何還會有這個規範化的步驟呢?那是由於具體的自定義指令對象的屬性通常是各個鉤子函數。可是Vue提供了一種簡寫的形式:在 bind 和 update 時觸發相同行爲,而不關心其它的鉤子時,能夠直接定義自定義指令爲一個函數,而不是對象。
Vue內部合併 directives 選項時,要將這種函數簡寫,轉化成對象的形式。以下:
directive:{
'color':function (el, binding) {
el.style.backgroundColor = binding.value
})
}
複製代碼
該 directive 將被規範成:
directive:{
'color':{
bind:function (el, binding) {
el.style.backgroundColor = binding.value
}),
update: function (el, binding) {
el.style.backgroundColor = binding.value
})
}
}
複製代碼
mixins 選項接受一個混入對象的數組。這些混入實例對象能夠像正常的實例對象同樣包含選項。以下所示:
var mixin = {
created: function () { console.log(1) }
}
var vm = new Vue({
created: function () { console.log(2) },
mixins: [mixin]
})
// => 1
// => 2
複製代碼
extends 選項容許聲明擴展另外一個組件,能夠是一個簡單的選項對象或構造函數。以下所示:
var CompA = { ... }
// 在沒有調用 `Vue.extend` 時候繼承 CompA
var CompB = {
extends: CompA,
...
}
複製代碼
Vue內部在處理選項extends或mixins時,會先經過遞歸調用 mergeOptions 函數,將extends對象或mixins數組中的對象做爲子選項集合與父選項集合中合併。這就是選項extends和mixins中的內容與並列的其餘選項有衝突時的合併規則的依據。
選項的數量比較多,合併規則也不盡相同。Vue內部採用策略模式來合併選項。各類策略方法在 mergeOptions 函數外實現,環境對象爲 strats 對象。
strats 對象是在 /src/core/config.js 文件中的 optionMergeStrategies 對象的基礎上,進行一系列策略函數添加而獲得的對象。環境對象接受請求,來決定委託哪個策略來處理。這也是用戶能夠經過全局配置 optionMergeStrategies 來自定義選項合併規則的緣由。
環境對象 strats 上擁有的屬性以及屬性對應的函數以下圖所示:
選項 el、 propsData以及圖中沒有的選項都採用默認策略函數 defaultStrat 進行合併。
const defaultStrat = function (parentVal: any, childVal: any): any {
return childVal === undefined
? parentVal
: childVal
}
複製代碼
默認策略比較簡單:若是子選項集合中有相應的選項,則直接使用子選項的值;不然使用父選項的值。
選項 data 與 provide 的策略函數雖然都是 mergeDataOrFn,可是選項 provide 合併時是向 mergeDataOrFn函數中傳入三個參數:父選項、子選項、實例。選項 data 的合併分兩種狀況:經過Vue.extends()處理子組件選項時、正常實例化時。前一種狀況沒有實例 vm,向 mergeDataOrFn函數傳入兩個參數:父選項和子選項;後一種狀況則跟選項 provide 傳入的參數同樣。
mergeDataOrFn函數代碼以下所示,只有在合併 data 選項,且是經過Vue.extends()處理子組件選項時,纔會走 if 分支。處理正常的實例化選項 data 、 provide 時,都是走 else 分支。
export function mergeDataOrFn (parentVal: any,childVal: any,vm?: Component): ?Function {
if (!vm) {
if (!childVal) {
return parentVal
}
if (!parentVal) {
return childVal
}
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 () {
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
}
}
}
}
複製代碼
在實例 vm 不存在的狀況下,有三種狀況:
一、子選項不存在,則返回父選項。
二、父選項不存在,則返回子選項。
三、若是父子選項都存在,則返回函數 mergedDataFn 。
函數 mergedDataFn將分別提取父子選項函數的返回值,將該純對象傳入 mergeData 函數,最終返回 mergeData 函數的返回值。若是父子選項都不存在,則不會走到這個函數中,所以不加以考慮。
爲何前面說在 if 分支中的父子選項都爲函數呢?由於走該分支,只能是經過Vue.extends()處理子組件 data 選項時。而當一個組件被定義時, data 必須聲明爲返回一個純對象的函數,這樣能防止多個組件實例共享一個數據對象。定義組件時, data 選項是一個純對象,在非生產環境下,Vue會有錯誤警告。
在 else 分支中,返回函數 mergedInstanceDataFn ,在該函數中,若是子選項存在則分別提取父子選項函數的返回值,將該純對象傳入 mergeData 函數;不然,將返回純對象形式的父選項。
在該場景下 mergeData 函數的做用是將父選項對象中有而子選項對象沒有的屬性,經過 set 方法將該屬性添加到子選項對象上並改爲響應式數據屬性。
分析完各類狀況,發現選項 data 與 provide 策略函數是一個高階函數,返回值是一個返回合併對象的函數。這是爲何呢?這個緣由前面說過,是爲了保證各組件實例有惟一的數據副本,防止組件實例共享同一數據對象。
選項 data 或 provide選項合併處理的結果是一個函數,並且該函數在合併階段並無執行,而是在初始化的時候執行的,這又是爲何呢?在 /src/core/instance/init.js 進行初始化時有以下代碼:
initInjections(vm)
initState(vm)
initProvide(vm)
複製代碼
函數 initState 有以下代碼:
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
複製代碼
由上述代碼可知: data 與 provide 的初始化是在 inject 與 props 以後進行的。在初始化時執行合併函數的返回函數,可以使用 inject 與 props 的值來初始化 data 與 provide 的值。
生命週期鉤子選項使用 mergeHook 函數合併。
function mergeHook ( parentVal: ?Array<Function>, childVal: ?Function | ?Array<Function> ): ?Array<Function> {
const res = childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
return res
? dedupeHooks(res)
: res
}
複製代碼
Vue官方API文檔上說生命週期鉤子選項只能是函數類型的,從這段源碼中能夠看出,開發者能夠傳入函數數組類型的生命週期選項。由於能夠將數組中各函數加以合併,所以傳入函數數組實用性不大。
還有一個點比較有意思:若是父選項存在,一定是一個數組。雖然生命週期選項能夠是數組,可是開發者通常傳入的都是函數,那麼爲何這裏父選項一定是數組呢?
這是由於生命週期父選項存在的狀況有兩種:Vue.extends()、Mixins。在上面 選項extends、mixins的處理 部分已經說過,處理這兩種狀況時,會將其中的選項做爲子選項遞歸調用 mergeOptions 函數進行合併。也就說聲明週期父選項都是通過 mergeHook 函數處理以後的返回值,因此若是生命週期父選項存在,一定是函數數組。
函數 mergeHook 返回值若是存在,會將返回值傳入 dedupeHooks 函數進行處理,目的是爲了剔除選項合併數組中的重複值。
function dedupeHooks (hooks) {
const res = []
for (let i = 0; i < hooks.length; i++) {
if (res.indexOf(hooks[i]) === -1) {
res.push(hooks[i])
}
}
return res
}
複製代碼
生命週期鉤子數組按順序執行,所以先執行父選項中的鉤子函數,後執行子選項中的鉤子函數。
組件 components ,指令 directives ,過濾器 filters ,被稱爲資源,由於這些均可以做爲第三方應用來提供。
資源選項經過 mergeAssets 函數進行合併,邏輯比較簡單。
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
}
}
複製代碼
先定義合併後選項爲空對象。若是父選項存在,則以父選項爲原型,不然沒有原型。若是子選項爲純對象,則將子選項上的各屬性複製到合併後的選項對象上。
前面說過 Vue.options 屬性內容以下所示:
Vue.options = {
components: {
KeepAlive,
Transition,
TransitionGroup
},
directives: {
model,
show
},
filters: Object.create(null),
_base: Vue
}
複製代碼
KeepAlive 、 Transition 、 TransitionGroup 爲內置組件,model , show 爲內置指令,不用註冊就能夠直接使用。
選項 watch 是一個對象,可是對象的屬性卻能夠是多種形式:字符串、函數、對象以及數組。
// work around Firefox's Object.prototype.watch...
if (parentVal === nativeWatch) parentVal = undefined
if (childVal === nativeWatch) childVal = undefined
/* istanbul ignore if */
if (!childVal) return Object.create(parentVal || null)
if (process.env.NODE_ENV !== 'production') {
assertObjectType(key, childVal, vm)
}
if (!parentVal) return childVal
const ret = {}
extend(ret, parentVal)
for (const key in childVal) {
let parent = ret[key]
const child = childVal[key]
if (parent && !Array.isArray(parent)) {
parent = [parent]
}
ret[key] = parent
? parent.concat(child)
: Array.isArray(child) ? child : [child]
}
return ret
複製代碼
由於火狐瀏覽器 Object 原型對象上擁有watch屬性,所以在合併前須要檢查選項集合 options 上是否有開發者添加的 watch屬性,若是沒有,不作合併處理。
若是子選項不存在,則返回以父選項爲原型的空對象。
若是父選項不存在,先檢查子選項是否爲純對象,再返回子選項。
若是父子選項都存在,則先將父選項各屬性複製到合併對象上,而後檢查子選項上的各個屬性。
在子選項上而不在父選項上的屬性,是數組則直接添加到合併對象上。若是不是數組,則填充到新數組中,將該數組添加到合併對象上。
父子選項上都存在的屬性,將父選項上該屬性變成數組格式,再向數組中添加子選項上的對應屬性。
選項 props 、 methods 、 inject 、 computed 採用相同的合併策略。選項 methods 與 computed 傳入時只接受對象形式,而選項 props 與 inject 通過前面的標準化以後也是純對象的形式。
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
複製代碼
首先檢查子選項是否爲純對象,若是不是純對象,在非生產環境報錯。
若是父選項不存在,則直接返回子選項。
若是父選項存在,先建立一個沒有原型的空對象做爲合併選項對象,將父選項上的各屬性複製到合併選項對象上。若是子選項存在,則將子選項對象上的所有屬性複製到合併對象上,所以父子選項上有相同屬性則以取子選項上該屬性的值。最後返回合併選項對象。
一、el 、 propsData 以及採用默認策略合併的選項:有子選項就選用子選項的值,不然選用父選項的值。
二、選項 data 、 provide :返回一個函數,該函數的返回值是合併以後的對象。以子選項對象爲基礎,若是存在子選項上沒有而父選項上有的屬性,則將該屬性轉變成響應式屬性後加入到子選項對象上。
三、生命週期鉤子選項:合併成函數數組,父選項排在子選項以前,按順序執行。
四、資源選項(components、directives、filters):定義一個沒有原型的空合併對象,子選項存在,則將子選項上的屬性複製到合併對象;父選項存在,則以父選項對象爲原型。
五、選項 watch :子選項不存在,則返回以父選項爲原型的空對象;父選項不存在,返回子選項;父子選項都存在,則和生命週期合併策略相似,以子選項屬性爲主,轉化成數組形式,父選項也存在該屬性,則推入數組中。
六、選項props、methods、inject、computed:將父子選項上的屬性添加到一個沒有原型的空對象上,父子選項上有相同屬性的則取子選項的值。
七、子選項中 extends 、 mixins :將這兩項的值做爲子選項與父選項合併,合併規則依照上述規則合併,最後再分項與子選項的同名屬性按上述規則合併。
在合併選項前,先對選項 inject 、 props 和 directives 進行標準化處理。而後將子選項集合中的extends、mixins做爲子選項遞歸調用合併函數與父選項合併。最後使用策略模式合併各個選項。
如需轉載,煩請註明出處:www.cnblogs.com/lidengfeng/…