vue实例中$options
是一个很关键的属性,组件在渲染的过程中需要查找的component、filter等都从这里查找。而$options的内容大多是通过mergeOptions这个方法得到的。mergeOptions主要的调用地方大概有以下几处
1、vue初始化函数_init中,将,所有的vue组件渲染都会走_init方法
Vue.prototype._init = function (options) {
var vm = this;
// a uid
vm._uid = uid++;
// 其他代码省略……
// 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
);
}
// 其他代码省略……
}
这里的mergeOptions函数大概干了哪些事情?对什么进行了合并,这里举个简单的例子。
const options = {
data: {},
methods: {}
}
new Vue(options)
针对上面这段代码,_init
中mergeOptions
合并的是Vue.options
和自己定义的options。具体两个对象如何合并,比如两个options中都有data,都有methods,那合并后的options对象是什么样的?这里就涉及到本文说的一个合并策略的问题了,后面再具体研究。
那么Vue.options
里面是什么呢?在哪里初始化的?
这里的Vue就是构造函数,其初始化可以从源码中*initGlobalAPI*
方法里查看
function initGlobalAPI (Vue) {
// 其他代码省略……
// exposed util methods.
// NOTE: these are not considered part of the public API - avoid relying on
// them unless you are aware of the risk.
Vue.util = {
warn: warn,
extend: extend,
mergeOptions: mergeOptions,
defineReactive: defineReactive
};
Vue.set = set;
Vue.delete = del;
Vue.nextTick = nextTick;
Vue.options = Object.create(null);
//ASSET_TYPES 指 ['component','directive','filter']
ASSET_TYPES.forEach(function (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;
//全局组件 builtInComponents keepAlive
extend(Vue.options.components, builtInComponents);
initUse(Vue); //Vue.use方法初始化
initMixin$1(Vue); //Vue.mixin方法初始化
initExtend(Vue); //Vue.extend方法初始化
initAssetRegisters(Vue); //Vue.component等全局组件注册函数初始化
}
根据这一步,至少可以知道Vue.options最初包含哪些属性
{
components: {},
directives: {},
filters: {},
_base: Vue
}
注意:构造函数options中的components中存放的是全局组件。全局注册函数Vue.component(其他两个directive,filter同component)注册的组件存储在构造函数的options里(Vue.options),具体想了解的这块可以参考initAssetRegisters
方法。
此外,如果引用了vue相关的插件,例如vuex、vue-router等,这些插件在安装的时候会通过mixin混入beforeCreate钩子函数的方式进行注册,同时也可能混入一些其他的例如destroyed等.
所以,引入了vuex的Vue.options中还包含钩子函数
{
components: {},
directives: {},
filters: {},
_base: Vue,
beforeCreate: [vuexInit],
destroyed: [fn]
}
这里需要特别提出的是initGlobalAPI
中最后面调用的initMixin$1
、initExtend
方法内部都调用了mergeOptions
方法
2、在resolveConstructorOptions
方法中调用
function resolveConstructorOptions (Ctor) {
var options = Ctor.options;
if (Ctor.super) {
var superOptions = resolveConstructorOptions(Ctor.super);
var 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)
var 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
}
3、在Vue.mixin
中调用,该方法在前面提到过的initGlobalAPI
中调用
function initMixin$1 (Vue) {
Vue.mixin = function (mixin) {
this.options = mergeOptions(this.options, mixin);
return this
};
}
4、在initExtend
中调用,该方法在前面提到过的initGlobalAPI
中调用
Vue.extend = function (extendOptions) {
extendOptions = extendOptions || {};
var Super = this;
var SuperId = Super.cid;
var cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {});
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
// 此处省略其他代码……
var Sub = function VueComponent (options) {
this._init(options);
};
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
Sub.cid = cid++;
//将父类构造函数options和子类做合并
Sub.options = mergeOptions(
Super.options,
extendOptions
);
Sub['super'] = Super;
// 此处省略其他代码……
};
}
这里值得一提的是,当执行Vue.component
时,其内部会执行Vue.extend
方法。这里提一个思考,为什么Vue.component可以注册全局组件,而其内部调用的Vue.extend却不能注册全局组件?
首先,来看下mergeOptions的具体内容,看看都干了些什么
function mergeOptions (
parent,
child,
vm
) {
if (process.env.NODE_ENV !== 'production') {
checkComponents(child);
}
if (typeof child === 'function') {
child = child.options;
}
normalizeProps(child, vm);
normalizeInject(child, vm);
normalizeDirectives(child);
var extendsFrom = child.extends;
if (extendsFrom) {
parent = mergeOptions(parent, extendsFrom, vm);
}
// 这一步与后面循环mergeField的执行顺序很关键,决定了是mixin中覆盖当前option还是当前option覆盖mixin
if (child.mixins) {
for (var i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm);
}
}
var options = {};
var key;
for (key in parent) {
mergeField(key);
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key);
}
}
function mergeField (key) {
var strat = strats[key] || defaultStrat;
options[key] = strat(parent[key], child[key], vm, key);
}
return options
}
这个函数内容并不是很长,但是通过前面的介绍,也可以看出这个函数在整个vue的执行过程中很重要。
该方法首先执行了几个normalize方法,这几个方法主要的作用是将options中某些属性进行统一格式管理。
首先是normalizeProps
方法,该方法将props属性统一格式化成一个对象,其格式如下
{
prop1Name: {type: String}
}
常见的props写法有两种,一种是数组形式,一种是对象形式
{
props: ['prop-name1','propName2']
}
//经过normalizeProps格式化后,将变为 {propName1: {type:null},propName2: {type:null}}
或者
{
props: {
propName1: {type: String},
propName2: {type: Number}
}
}
normalizeInject
顾名思义,是用来格式化inject格式的。常见的inject的写法也有两种:
{
inject: ['message']
}
{
inject: {
localMessage: {from: 'message'}
}
}
格式化后的格式统一按照第二种对象格式来
normalizeDirectives
用来格式化指令,指令支持两种格式,一种是直接定义函数,一种是定义指令对象
{
focus: function(el,binding,vnode) {}
}
//格式化后为 { focus: { bind: function(el,binding,vnode) {}, update: function(el,binding,vnode) {}}}
{
focus: {
bind: function(el,binding,vnode) {},
update: function() {}
}
}
normalizeDirectives
是将第一种格式格式化成第二种格式。
接下来,继续看mergeOptions中紧接着递归执行了merge操作。
function mergeOptions (
parent,
child,
vm
) {
//此处省略其他代码
var extendsFrom = child.extends;
if (extendsFrom) {
parent = mergeOptions(parent, extendsFrom, vm);
}
// 这一步与后面循环mergeField的执行顺序很关键,决定了是mixin中覆盖当前option还是当前option覆盖mixin
if (child.mixins) {
for (var i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm);
}
}
//此处省略其他代码
}
这里有两种,如果options里面有extends属性或者mixins属性,会递归合并。从上面的源码中可以看出,下面两种书写的形式是等价的
{
data: {},
extends: mixin1
}
//等价于
{
data: {},
mixins: [mixin1]
}
最后面就是真正的合并相关代码了
function mergeOptions (
parent,
child,
vm
) {
//此处省略其他代码
var options = {};
var key;
for (key in parent) {
//parent中所有key值属性和child的key值属性进行合并
mergeField(key);
}
for (key in child) {
if (!hasOwn(parent, key)) {
//parent中没有,child中有的
mergeField(key);
}
}
function mergeField (key) {
//strats中定义了一些关键key的合并策略,根据key取对应的合并策略,进行合并。
var strat = strats[key] || defaultStrat;
options[key] = strat(parent[key], child[key], vm, key);
}
return options
}
看着就这么点代码,是不是感觉这个源码读起来就没那么费劲了。简单直白的说这个函数的最终目的是合并两个options对象,返回合并后的options。
defaultStrat
首先看下源码中的默认合并策略源码,这段源码内容很简单,如果childVal有值,取childVal,否则取parentVal。是一个覆盖的合并策略,如果parent和children同时拥有该属性,也返回child。
var defaultStrat = function (parentVal, childVal) {
return childVal === undefined
? parentVal
: childVal
};
源码中有段代码定义了el
和propsData
使用该合并策略
strats.el = strats.propsData = function (parent, child, vm, key) {
return defaultStrat(parent, child)
};
也即是说如果有以下两个options
const option1 = {
el: '#app'
}
const option2 = {
el: '#main'
}
//mergeOptions(option1,option2)后
const options = {el: '#main'}
strats.data = function (
parentVal,
childVal,
vm
) {
if (!vm) {
if (childVal && typeof childVal !== 'function') {
return parentVal
}
return mergeDataOrFn.call(this, parentVal, childVal)
}
return mergeDataOrFn(parentVal, childVal, vm)
};
大家都晓得,data的写法有两种形式,一种是plain object,一种是函数。在做合并的时候,会对类型进行判断,如果是函数,将函数执行的结果作为合并的对象进行合并。具体的合并可以参考mergeData
方法
function mergeData (to, from) {
if (!from) { return to }
var key, toVal, fromVal;
var keys = Object.keys(from);
for (var i = 0; i < keys.length; i++) {
key = keys[i];
toVal = to[key];
fromVal = from[key];
if (!hasOwn(to, key)) {
//设置响应式属性
set(to, key, fromVal);
} else if (isPlainObject(toVal) && isPlainObject(fromVal)) {
mergeData(toVal, fromVal);
}
}
return to
}
从上面的代码中可以看出,这是带有一个递归合并的函数,当data中某一属性值为plainObject时,会进行递归合并。下面以实际的例子来具体说明究竟是怎么合并的
const option1 = {
data: {
a: 1,
b: 2,
o: {
c: 3
}
}
}
const option2 = {
data() {
return {
a: 'a',
o: {
c: 'c',
d: 'd'
}
}
}
}
假设有以上两个option,在执行mergeOptions(option1,option2)时,首先执行
option2`中的data函数,得到对应的执行结果对象
{
a: 'a',
o: {
c: 'c',
d: 'd'
}
}
然后就是将该对象与option1进行合并,执行mergeData
方法。
首先遍历option1的key值,如果有option1中有,option2中没有的属性,则执行set(to, key, fromVal)
操作,两者都有的属性,取option2,因为最后返回的是to
,遍历过程中的操作是对to
对象进行的操作,而不是返回一个新对象。这点要注意。所以option1.a和option2.a合并后,返回的是option2对象,自然a的取值是’a’。
接着遇到属性o时,这是一个plain object。会递归执行mergeData
操作,即执行mergeData(option2.o,option1.o)
。
最终合并后,会得到以下的结果
{
a: 'a',//相同属性取option2
b: 2, //option2没有的属性,新增
o: {
c: 'c',
d: 'd'
}
}
function mergeHook (
parentVal,
childVal
) {
return childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
}
//LIFECYCLE_HOOKS钩子函数
LIFECYCLE_HOOKS.forEach(function (hook) {
strats[hook] = mergeHook;
});
钩子函数有哪些,相信大家应该不陌生。这里还是简单列举下,便于对照上面的代码
var LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured'
];
从上面的代码中可以看出,这些钩子函数的合并策略是一样的。具体如何执行合并呢?无外乎那么几种情况,a有b没有,a没有b有,和a、b都有。这里也以具体的对象进行举例。
const opt1 = {
beforeCreate() {console.log('opt1 beforeCreate')},
mounted() {console.log('opt1 mounted')}
}
const opt2 = {
created() {console.log('opt2 created')},
mounted() {console.log('opt2 mounted')}
}
当执行beforeCreate
的合并时,就出现了a有b没有的情况,即opt1中有beforeCreate
而opt2中没有。这种合并后的beforeCreate就取opt1中的。
当执行created
合并时,就出现了a没有b有的情况,合并后取opt2的created
前面两种很符合我们日常的合并思维,很容易理解记忆。我们应当注意的是第三种,a有b也有的情况。当我们合并mountd
的时候,合并后的mounted
是什么?根据源码执行可知道,会将该属性值转换成数组,进行追加。及合并后的mounted
是个数组,数组的内容为[opt1.mounted, opt2.mounted]
最终mergeOptions(opt1,opt2)
后会得出以下结果
{
beforeCreate() {console.log('opt1 beforeCreate')},
created() {console.log('opt2 created')},
//这里注意数组里方法的顺序,决定了合并后,执行的属性
mounted: [function() {console.log('opt1 mounted')},function(){console.log('opt2 mounted')}]
}
合并后,所有的钩子函数都会重新执行。注意执行顺序
function mergeAssets (
parentVal,
childVal,
vm,
key
) {
// [components, directives,filters]通过原型链接的方式进行合并
var res = Object.create(parentVal || null);
if (childVal) {
process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm);
return extend(res, childVal)
} else {
return res
}
}
ASSET_TYPES.forEach(function (type) {
strats[type + 's'] = mergeAssets;
});
Vue中的Assets主要指以下3种属性
var ASSET_TYPES = [
'component',
'directive',
'filter'
];
如源码中注释的那样,两个options中assets属性合并,主要是通过原型继承的方式,最终返回一个新的对象,该对象包含childVal,并且原型指向parentVal。举个简单的例子
const opt1 = {
components: {ComponetA}
}
const opt2 = {
components: {ComponentB}
}
执行mergeOptions(opt1,opt2)
后,其options中component的属性如下,这样合并后通过components依然能查找到opt1中的ComponentA
{
components: {
ComponentB
[[prototype]]: {ComponentA}
}
}
一般情况下,以new Vue({components: {ComponentA}})来说,会执行
mergeOptions(Vue.options, {components: {ComponentA}})`。合并后的components里ownProperty是局部组件,原型(components.proto)中是全局组件。
strats.watch = function (
parentVal,
childVal,
vm,
key
) {
// work around Firefox's Object.prototype.watch... firefox58+版本已经移除
if (parentVal === nativeWatch) { parentVal = undefined; }
if (childVal === nativeWatch) { childVal = undefined; }
/* istanbul ignore if */
// childVal没值,返回空对象 原型继承parent watch
if (!childVal) { return Object.create(parentVal || null) }
if (process.env.NODE_ENV !== 'production') {
assertObjectType(key, childVal, vm);
}
//parentVal无值,直接返回childval
if (!parentVal) { return childVal }
// 合并parent child,相同key值,value转换成数组合并
var ret = {};
extend(ret, parentVal);
for (var key$1 in childVal) {
var parent = ret[key$1];
var child = childVal[key$1];
if (parent && !Array.isArray(parent)) {
parent = [parent];
}
ret[key$1] = parent
? parent.concat(child)
: Array.isArray(child) ? child : [child];
}
return ret
};
累了,不多说了,具体合并策略,看代码中的中文注释。以实际例子为例
1、opt1有watch属性,opt2没有,直接返回`Object.create(opt1.watch)
const opt1 = {
watch: {message() {}}
}
const opt2 = {}
//mergeOptions(opt1,opt2) 合并后
const opt = mergeOptions(opt1,opt2) = {
watch: {
[[prototype]]: {message() {}}
}
}
2、opt1没watch属性,opt2有,合并后的watch值,为opt2.watch
const opt1 = {}
const opt2 = {
watch: {message() {}}
}
//mergeOptions(opt1,opt2) 合并后
const opt = mergeOptions(opt1,opt2) = {
watch: {message() {}}
}
3、opt1和opt2都有watch属性
const opt1 = {
watch: {
message() {}
}
}
const opt2 = {
watch: {
message() {}
other() {}
}
}
//mergeOptions(opt1,opt2) 合并后
const opt = {
watch: {
message: [Function,Function],//opt1.watch.message opt2.watch.message
other: [Function]//opt2.watch.other
}
}
strats.props =
strats.methods =
strats.inject =
strats.computed = function (
parentVal,
childVal,
vm,
key
) {
if (childVal && process.env.NODE_ENV !== 'production') {
assertObjectType(key, childVal, vm);
}
// 无parentVal,返回childVal
if (!parentVal) { return childVal }
//注意,这里原型指向null
var ret = Object.create(null);
//拷贝parentVal属性
extend(ret, parentVal);
// 对象合并,同名属性,child覆盖parent
// 思考,{ mixins: [mixin1,mixin2], methods: {fun1() {}} }
// mixin1中也有fun1的定义,合并后的method中是哪个fun1
// 解:在mergeOptions中先mergeOptions了parent和child.mixin1,在后面才处理child自身。
if (childVal) { extend(ret, childVal); }
return ret
};
依旧有三种情况,这里不具体一一展开了。需要留意的是第二种当child无值时,不是直接返回parent,也非原型链接。而是将parent属性拷贝到一个新对象。
这里重点关注下parent和child都有值时的场景,两者都有值时,child中对应属性的值覆盖parent属性的值。举例说明
const opt1 = {
methods: {
fun1() { console.log('opt1')}
fun3() {}
}
}
const opt2 = {
methods: {
fun1() { console.log('opt2')},
fun2() {}
}
}
//mergeOptions(opt1,opt2) 合并后
const opt = {
methods: {
fun1() { console.log('opt2')},
fun2() {}
fun3() {}
}
}
strats.provide = mergeDataOrFn;
与data的合并策略一样,参考data吧。
以上就是源码中定义的一些option关键属性的合并策略,理解这些合并策略,可以帮助你更好的理解Vue的执行机制。