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