合併配置
通過之前的原始碼學習,我們已經瞭解到了new Vue主要有兩種場景,第一種就是在外部主動呼叫new Vue建立一個例項,第二個就是程式碼內部建立子元件的時候自行建立一個new Vue例項。但是無論那種new Vue方式,我們都需要進入了Vue._init,執行mergeOptions函式合併配置。為了更直觀,我們整個demo除錯耍耍。
// src\main.js
let childComp = {
template:"<div>{{msg}}</div>",
data(){
return{
msg:"childComp"
}
},
created(){
console.log("childComp created");
},
mounted(){
console.log("childComp mounted");
}
}
Vue.mixin({
created(){
console.log("mixin");
}
})
let app = new Vue({
el:"#app",
render: h => h(childComp)
})
我用的時vue-cli3,這裡有個小細節需要注意一下,vue-cli3開發環境預設使用的是runtime版本(node_modules\vue\dist\vue.runtime.esm.js),這個版本是不支援編譯template的,需要用Compiler版本,這個在vue.config.js中配置一下即可,配置程式碼如下:
module.exports = {
runtimeCompiler: true
}
準備工作搞好了,那麼我們現在開始進入_init函式,看看合併配置是怎麼一個說法。
// src\core\instance\init.js
Vue.prototype._init = function (options?: Object) {
...
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor), //vue.options
options || {}, //new Vue中的options
vm
)
}
...
}
外部呼叫場景
上述程式碼中可明顯看出兩中合併配置的情況,我們一開始進入的肯定時非元件模式,也就是else情況。mergeOptions傳入了3個入參,我們先看第一個入參的resolveConstructorOptions方法做了什麼。
// src\core\instance\init.js
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
}
入參Ctor = vm.constructor = Vue,Vue沒有父級,所以不會進入到if邏輯,因此這裡返回的就是Vue.options的配置。Vue.options則在初始化的時候就做了定義和配置。
// src\core\global-api\index.js
Vue.options = Object.create(null)
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
Vue.options._base = Vue //createComponent時用到,之前提及過。
extend(Vue.options.components, builtInComponents) //擴充套件一些內建元件
這裡ASSET_TYPES在src\shared\constants.js有定義
// src\shared\constants.js
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]
然後我們再返回去_init函式分析一下mergeOptions函式:
// src\core\util\options.js
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
...
const options = {}
let key
、、
for (key in parent) {
mergeField(key)
}
for (key in child) {
// key沒在parent定義時
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(Vue.options)和child(new Vue中的options),然後遍歷的過程中呼叫了mergeField方法。而該方法先去拿到一個strat函式,這個函式首先是再strats中去找,沒找到就使用defaultStrat預設函式(defaultStrat可自行查閱原始碼),我們主要看strats:
// src\core\util\options.js
const strats = config.optionMergeStrategies
strats是定義在config中,所以說我們是可以隨意改動strats的。然後在options.js中,strats擴充套件了很多屬性,每個屬性(key)都是一種合併策略,有興趣的可以一個個研究,因為我們例子是生命週期的合併,所以我們先挑生命週期的合併策略來分析,後面遇到其他的再做分析。
// src\core\util\options.js
LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeHook
})
LIFECYCLE_HOOKS定義在src\shared\constants.js
// src\shared\constants.js
export const LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured'
]
遍歷這些值,然後定義它們的合併策略,其實都mergeHook方法,都是一樣的合併策略,下面我們看看mergeHook函式:
// src\core\util\options.js
function mergeHook (
parentVal: ?Array<Function>,
childVal: ?Function | ?Array<Function>
): ?Array<Function> {
return childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
}
這個多層巢狀的三元表示式看著複雜,其實不難,我們可以分段理解:
①:childVal有值:進入②,
childVal沒值:賦值parentVal;
②:parentVal有值:parentVal和childVal陣列合並,
parentVal沒值:進入③;
③:childVal是個陣列:賦值childVal,
childVal不是陣列:賦值[childVal];
最終我們return了一個陣列到mergeOptions函式。
現在我們回過頭來demo中的Vue.mixin定義,其原始碼其實也呼叫了mergeOptions,我們看看原始碼:
// src\core\global-api\mixin.js
export function initMixin (Vue: GlobalAPI) {
Vue.mixin = function (mixin: Object) {
this.options = mergeOptions(this.options, mixin)
return this
}
}
mixin的原始碼很簡單,其實就是呼叫了mergeOptions對Vue.options做了合併。有個小細節需要留意,就是demo中Vue.mixin和new Vue的程式碼順序,必須先對Vue.mixin做出定義,不然在new Vue的時候Vue.options和new Vue的options合併時,是會丟失掉Vue.mixin的,因為那時候Vue.mixin並沒有執行mergeOptions把options合併到Vue.options上。
元件場景
接下來我們看另一種情況,元件合併配置。也就是在_inti方法中執行了initInternalComponent函式,我們來分析一下它做了什麼?
// src\core\instance\init.js
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
const opts = vm.$options = Object.create(vm.constructor.options)
// doing this because it's faster than dynamic enumeration.
const parentVnode = options._parentVnode
opts.parent = options.parent
opts._parentVnode = parentVnode
const vnodeComponentOptions = parentVnode.componentOptions
opts.propsData = vnodeComponentOptions.propsData
opts._parentListeners = vnodeComponentOptions.listeners
opts._renderChildren = vnodeComponentOptions.children
opts._componentTag = vnodeComponentOptions.tag
if (options.render) {
opts.render = options.render
opts.staticRenderFns = options.staticRenderFns
}
}
子元件的合併就相對簡單很多了,vm.$options去繼承了子元件構造器vm.constructor.options,然後再把一些配置掛載到上面。我們主要看看vm.constructor.options是怎麼來的。
// src\core\global-api\extend.js
Vue.extend = function (extendOptions: Object): Function {
const Super = this
...
const Sub = function VueComponent (options) {
this._init(options)
}
// 構造器指向自己
Sub.prototype.constructor = Sub
// 合併配置
Sub.options = mergeOptions(
Super.options,
extendOptions
)
...
}
其實Vue.extend的時候對子元件的構造器進行了定義了,還對Vue.options(Super.options)和子元件的options(extendOptions)做了合併。
所以initInternalComponent中的vm.$options其實就是一個已經把Vue.options和子元件的options合併好的配置集合了。
總結
至此Vue的options合併就告一段落了,我們需要知道它有兩個場景,外部呼叫場景和元件場景。
其實一些庫、框架的設計也是類似的,都會有自身的預設配置,同時又允許在初始化的時候讓開發者自定義配置,之後再合併兩個配置來達到應付各種場景需求,這種設計思想也是我們寫元件或做架構的時候必不可少的思維模式。