Vue本質是上來說是一個函式,在其通過new關鍵字構造呼叫時,會完成一系列初始化過程。通過Vue框架進行開發,基本上是通過向Vue函式中傳入不同的引數選項來完成的。引數選項往往需要加以合併,主要有兩種情況:
1、Vue函式本身擁有一些靜態屬性,在例項化時開發者會傳入同名的屬性。
2、在使用繼承的方式使用Vue時,需要將父類和子類上同名屬性加以合併。
Vue函式定義在 /src/core/instance/index.js中。
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 函式中,會對傳入的選項集進行合併處理。
// merge options
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
複製程式碼
在開發過程中基本不會傳入 _isComponent 選項,因此在例項化時走 else 分支。通過 mergeOptions 函式來返回合併處理之後的選項並將其賦值給例項的 $options 屬性。 mergeOptions 函式接收三個引數,其中第一個引數是將生成例項的建構函式傳入 resolveConstructorOptions 函式中處理之後的返回值。
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 屬性。在研究例項化合並選項時,為行文方便,將該函式返回的值統一稱為選項合併的父選項集合,例項化時傳入的選項集合稱為子選項集合。
一、Vue建構函式的靜態屬性options
在合併選項時,在沒有繼承關係存在的情況,傳入的第一個引數為Vue建構函式上的靜態屬性 options ,那麼這個靜態屬性到底包含什麼呢?為了弄清楚這個問題,首先要搞清楚執行 npm run dev 命令來生成 /dist/vue.js 檔案的過程中發生了什麼。
在 package.json 檔案中 scripts 物件中有:
"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
合併選項的函式 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
}
複製程式碼
1、元件命名規則
合併選項時,在非生產環境下首先檢測宣告的元件名稱是否合乎標準:
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
)
}
}
複製程式碼
由上述程式碼可知,有效性命名規則有兩條:
1、元件名稱可以使用字母、數字、符號 _、符號 - ,且必須以字母為開頭。
2、元件名稱不能是Vue內建標籤 slot 和 component;不能是 html內建標籤;不能使用部分SVG標籤。
2、選項規範化
傳入Vue的選項形式往往有多種,這給開發者提供了便利。在Vue內部合併選項時卻要把各種形式進行標準化,最終轉化成一種形式加以合併。
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
複製程式碼
上述三條函式呼叫分別標準化選項 props 、inject 、directives 。
(一)、props選項的標準化
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物件的屬性值為物件時,該物件的屬性值有效的有四種:
1、type:基礎的型別檢查。
2、required: 是否為必須傳入的屬性。
3、default:預設值。
4、validator:自定義驗證函式。
(二)、inject選項的標準化
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 只接受物件型別。一般具體的自定義指令是一個物件。 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
})
}
}
複製程式碼
3、選項extends、mixins的處理
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中的內容與並列的其他選項有衝突時的合併規則的依據。
4、使用策略模式合併選項
選項的數量比較多,合併規則也不盡相同。Vue內部採用策略模式來合併選項。各種策略方法在 mergeOptions 函式外實現,環境物件為 strats 物件。
strats 物件是在 /src/core/config.js 檔案中的 optionMergeStrategies 物件的基礎上,進行一系列策略函式新增而得到的物件。環境物件接受請求,來決定委託哪一個策略來處理。這也是使用者可以通過全域性配置 optionMergeStrategies 來自定義選項合併規則的原因。
三、選項合併策略
環境物件 strats 上擁有的屬性以及屬性對應的函式如下圖所示:
1、選項el、propsData以及strats物件不包括的屬性物件的合併策略
選項 el、 propsData以及圖中沒有的選項都採用預設策略函式 defaultStrat 進行合併。
const defaultStrat = function (parentVal: any, childVal: any): any {
return childVal === undefined
? parentVal
: childVal
}
複製程式碼
預設策略比較簡單:如果子選項集合中有相應的選項,則直接使用子選項的值;否則使用父選項的值。
2、選項data、provide的合併策略
選項 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 不存在的情況下,有三種情況:
1、子選項不存在,則返回父選項。
2、父選項不存在,則返回子選項。
3、如果父子選項都存在,則返回函式 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 的值。
3、生命週期鉤子選項的合併策略
生命週期鉤子選項使用 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
}
複製程式碼
生命週期鉤子陣列按順序執行,因此先執行父選項中的鉤子函式,後執行子選項中的鉤子函式。
4、資源選項(components、directives、filters)的合併策略
元件 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 為內建指令,不用註冊就可以直接使用。
5、選項watch的合併策略
選項 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屬性,如果沒有,不做合併處理。
如果子選項不存在,則返回以父選項為原型的空物件。
如果父選項不存在,先檢查子選項是否為純物件,再返回子選項。
如果父子選項都存在,則先將父選項各屬性複製到合併物件上,然後檢查子選項上的各個屬性。
在子選項上而不在父選項上的屬性,是陣列則直接新增到合併物件上。如果不是陣列,則填充到新陣列中,將該陣列新增到合併物件上。
父子選項上都存在的屬性,將父選項上該屬性變成陣列格式,再向陣列中新增子選項上的對應屬性。
6、選項props、methods、inject、computed的合併策略
選項 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
複製程式碼
首先檢查子選項是否為純物件,如果不是純物件,在非生產環境報錯。
如果父選項不存在,則直接返回子選項。
如果父選項存在,先建立一個沒有原型的空物件作為合併選項物件,將父選項上的各屬性複製到合併選項物件上。如果子選項存在,則將子選項物件上的全部屬性複製到合併物件上,因此父子選項上有相同屬性則以取子選項上該屬性的值。最後返回合併選項物件。
7、選項合併策略總結
1、el 、 propsData 以及採用預設策略合併的選項:有子選項就選用子選項的值,否則選用父選項的值。
2、選項 data 、 provide :返回一個函式,該函式的返回值是合併之後的物件。以子選項物件為基礎,如果存在子選項上沒有而父選項上有的屬性,則將該屬性轉變成響應式屬性後加入到子選項物件上。
3、生命週期鉤子選項:合併成函式陣列,父選項排在子選項之前,按順序執行。
4、資源選項(components、directives、filters):定義一個沒有原型的空合併物件,子選項存在,則將子選項上的屬性複製到合併物件;父選項存在,則以父選項物件為原型。
5、選項 watch :子選項不存在,則返回以父選項為原型的空物件;父選項不存在,返回子選項;父子選項都存在,則和生命週期合併策略類似,以子選項屬性為主,轉化成陣列形式,父選項也存在該屬性,則推入陣列中。
6、選項props、methods、inject、computed:將父子選項上的屬性新增到一個沒有原型的空物件上,父子選項上有相同屬性的則取子選項的值。
7、子選項中 extends 、 mixins :將這兩項的值作為子選項與父選項合併,合併規則依照上述規則合併,最後再分項與子選項的同名屬性按上述規則合併。
四、總結
在合併選項前,先對選項 inject 、 props 和 directives 進行標準化處理。然後將子選項集合中的extends、mixins作為子選項遞迴呼叫合併函式與父選項合併。最後使用策略模式合併各個選項。
如需轉載,煩請註明出處:www.cnblogs.com/lidengfeng/…