在上一篇文章的結尾,咱們提到在 _init
的最初階段執行的就是 merge options
的邏輯:vue
// src/core/instance/init.js
// merge options
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),
options || {},
vm
);
}
複製代碼
能夠看到,合併 options
分兩種狀況,它們的區別是什麼呢。node
區別就是在執行用戶編寫的 new Vue(options)
時就會執行 else
邏輯,而執行內部的 new Vue(options)
(好比建立子組件實例)時就會走 if
邏輯。ios
這一節咱們就圍繞下面這個例子來研究這兩種狀況下合併 options
分別是怎麼執行的:web
import Vue from "vue";
let childComponent = {
template: "<div>{{msg}}</div>",
created() {
console.log("child created");
},
mounted() {
console.log("child mounted");
},
data() {
return {
msg: "Hello Vue"
};
}
};
Vue.mixin({
created() {
console.log("parent created");
}
});
let app = new Vue({
el: "#app",
render: h => h(childComponent)
});
複製代碼
例子中使用了Vue.mixin
函數,是由於mixin
自己就是合併 options
的過程,來看 Vue.mixin
的定義:api
// src/core/global-api/mixin.js
import { mergeOptions } from "../util/index";
export function initMixin(Vue: GlobalAPI) {
Vue.mixin = function(mixin: Object) {
this.options = mergeOptions(this.options, mixin);
return this;
};
}
複製代碼
能夠看到 Vue.mixin
的內部實現就是調用了 mergeOptions
函數,把 mixin
中的內容合併到 Vue.options
上。數組
咱們先分析執行外部 new Vue(options)
時的狀況,這時會走 else
邏輯:app
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
複製代碼
這裏調用了 resolveConstructorOptions
函數並傳遞了 vm.constructor
做爲參數。resolveConstructorOptions
函數定義在 src/core/instance/init.js
文件中:編輯器
// 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;
}
複製代碼
這裏的 if
語句經過 Ctor.super
判斷 Ctor
是 Vue
仍是 Vue 的子類
,顯然在咱們的例子中是 Vue
,所以 if
中的邏輯不會執行。因此 resolveConstructorOptions
函數直接返回 Vue.options
。ide
那這個 Vue.options
又是從哪裏來的呢,實際上它在 initGlobalAPI
函數內被定義:函數
// src/core/global-api/index.js
export function initGlobalAPI(Vue: GlobalAPI) {
// ...
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
後遍歷 ASSET_TYPES
數組往 Vue.options
添加屬性,ASSET_TYPES
定義以下:
export const ASSET_TYPES = ["component", "directive", "filter"];
複製代碼
以後又添加了 _base
屬性。此時 Vue.options
大概是這個樣子的:
Vue.options = {
components: {},
directives: {},
filters: {},
_base: function Vue(options) {}
};
複製代碼
最後經過 extend(Vue.options.components, builtInComponents)
把一些內置組件擴展到 Vue.options.components
上,Vue
的內置組件目前有 <keep-alive>
、<transition>
和 <transition-group>
組件,這也就是爲何咱們在其它組件中使用 <keep-alive>
組件不須要註冊的緣由,這塊兒後續咱們介紹 <keep-alive>
組件的時候會詳細講。
瞭解完 resolveConstructorOptions
後,咱們分段來分析 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);
// ...
}
複製代碼
mergeOptions
函數的 child
參數對應的就是用戶編寫的 options
。這裏首先調用 checkComponents(child)
來檢查 options.components
組件名稱是否合法:
/**
* Validate component names
*/
function checkComponents(options: Object) {
for (const key in options.components) {
validateComponentName(key);
}
}
複製代碼
而後執行一系列 normalize
函數進行規範化操做。這一段代碼不是本節重點,在這裏不會細講。接着看下一段:
export function mergeOptions(
parent: Object,
child: Object,
vm?: Component
): Object {
// ...
// Apply extends and mixins on the child options,
// but only if it is a raw options object that isn't
// the result of another mergeOptions call.
// Only merged options has the _base property.
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);
}
}
}
// ...
}
複製代碼
最外層的 if
語句代表這是對未合併的 options
的處理,由於註釋提到了只有已合併的 options
纔有 _base
屬性。
if
中的邏輯就是遞歸調用 mergeOptions
函數,將 parent
分別和 child.extends
、child.mixins
合併,最後的結果賦給 parent
。
能夠看到上面這兩段代碼都是在處理 parent
和 child
參數,mergeOptions
函數核心邏輯是接下來這一段:
export function mergeOptions(
parent: Object,
child: Object,
vm?: Component
): Object {
// ...
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
對象的屬性並調用 mergeField
函數,而後又遍歷了 child
對象的屬性,若是 child
對象的屬性在 parent
中沒有定義,一樣也要調用 mergeField
函數。
mergeField
函數首先定義了 strat
, strat
實際上也是個函數,它的取值有兩個來源,咱們先看這個 defaultStrat
的定義:
// src/core/util/options.js
const defaultStrat = function(parentVal: any, childVal: any): any {
return childVal === undefined ? parentVal : childVal;
};
複製代碼
defaultStrat
的邏輯很簡單,有 childVal
就用 childVal
,沒有就用 parentVal
。咱們再來看 strats
的定義:
/**
* Option overwriting strategies are functions that handle
* how to merge a parent option value and a child option
* value into the final value.
*/
const strats = config.optionMergeStrategies;
複製代碼
這裏 strats
的值是全局配置對象 config
的 optionMergeStrategies
屬性,其實就是個空對象。從註釋咱們能夠看出來,strats
就是各類選項合併策略函數的集合,用來合併父 options
和子 options
。
咱們先來分析一下生命週期函數的合併策略:
// src/shared/constants.js
export const LIFECYCLE_HOOKS = [
"beforeCreate",
"created",
"beforeMount",
"mounted",
"beforeUpdate",
"updated",
"beforeDestroy",
"destroyed",
"activated",
"deactivated",
"errorCaptured",
"serverPrefetch"
];
複製代碼
// src/core/util/options.js
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;
}
LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeHook;
});
複製代碼
能夠看到,LIFECYCLE_HOOKS
定義了全部生命週期函數名,這些都會做爲 strats
的屬性名,全部屬性對應的屬性值都是 mergeHook
這個函數。
在mergeHook
的最後對res
還調用dedupeHooks
進行了處理,來看下dedupeHooks
函數:
// src/core/util/options.js
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;
}
複製代碼
其實就是數組去重處理,也就是將res
中相同的鉤子函數去掉。
到這裏也就印證了咱們上面的猜想:strats
就是各類選項合併策略函數的集合。回到 mergeOptions
函數:
export function mergeOptions(
parent: Object,
child: Object,
vm?: Component
): Object {
// ...
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;
}
複製代碼
mergeField
函數的下一步操做是將 parent
和 child
中的 key
合併到 options
中,值是調用對應的合併策略返回的結果。合併完成後 mergeOptions
函數將 options
返回出去。
這樣咱們就把合併 options
的 else
邏輯走了一遍。回顧咱們在本節中舉的例子,在通過合併操做後大概是這樣子的:
vm.$options = {
components: {},
created: [
function created() {
console.log("parent created");
}
],
directives: {},
filters: {},
_base: function Vue(options) {
// ...
},
el: "#app",
render: function(h) {
//...
}
};
複製代碼
而咱們例子中的組件 childComponent
的 options
合併處理走的是 if
邏輯,接下來咱們就來分析這種狀況。
來看 if 邏輯的代碼:
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options);
複製代碼
if
邏輯直接調用了 initInternalComponent
函數,看看它是怎麼定義的:
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 = Object.create(vm.constructor.options)
,這裏的 vm.constructor.options
也就是子構造函數的 options
屬性,它是何時定義的呢。
回顧Vue源碼探祕(九)(createComponent),子構造函數是經過 Vue.extend
建立的:
// src/core/global-api/extend.js
Vue.extend = function(extendOptions: Object): Function {
// ...
const Sub = function VueComponent(options) {
this._init(options);
};
Sub.options = mergeOptions(Super.options, extendOptions);
// ...
};
複製代碼
能夠看到,Sub.options
就是由 Vue.options
和 組件 options
經過 mergeOptions
合併的結果。
接着又把實例化子組件傳入的子組件父 VNode
實例 parentVnode
、子組件的父 Vue
實例 parent
保存到 vm.$options
中,另外還保留了 parentVnode
配置中的 propsData
、listeners
等屬性。
因此initInternalComponent
函數的邏輯其實很簡單,就是作了一層對象賦值而已。對應咱們的例子,在執行了這個 if
邏輯後大概是這樣子的:
vm.$options = {
parent: app,
_parentVnode: VNode,
propsData: undefined,
_componentTag: undefined,
_renderChildren: undefined,
__proto__: {
components: {},
directives: {},
filters: {},
_base: function Vue(options) {},
_Ctor: {},
created: [
function created() {
console.log("parent created");
},
function created() {
console.log("child created");
}
],
mounted: [
function mounted() {
console.log("child mounted");
}
],
data() {
return {
msg: "Hello Vue"
};
},
template: "<div>{{msg}}</div>"
}
};
複製代碼
那麼到這裏,Vue
初始化階段對於 options
的合併過程就介紹完了,options
的合併有兩種方式:
new Vue
時,會調用
mergeOptions
函數,並根據不一樣的選項調用不一樣的合併策略函數
initInternalComponent
函數進行合併
下一節咱們一塊兒來看下生命週期
部分的源碼實現。