开端
最近为了给自己充电,在腾讯课堂报了一个vue源码学习的课程,跟着课堂上的老师学习vue源码以此来增加自己对于vue的理解。这篇文章主要是记录一下自己学习vue源码时,学习到了什么,并把自己对vue源码的理解通过语音的形式给描述出来。这里并不会一字一句的解释vue中的每一句源码的意思,因为有些我自己还不解其意,但不妨碍从中学习到知识。
vue版本:2.5.1
源码范围:
1.实例化vue时参数的合并策略以及规范。
2.vue通过Vue.extend方法进行组件参数扩展时的参数合并策略。
3.vue的mixins字段的参数合并方式。
使用vue创建一个实例时,传递给Vue的构造函数的参数会变成什么?
var vm = new Vue({
el: '#app',
data: {
message: 'hello world!',
},
});
这里我们不探究new Vue时,其他地方发生了什么,本篇文章我们只关心vue源码中是怎么处理Vue构造函数接收过来的参数的。我们不妨通过控制台打印一下 vm.$options 看一下是什么:

这$options属性就是vue实例存储的合并Vue参数后的对象,有el,components组件,data数据,directives指令等。首先我们上面的例子中并没有传递components和directives这两个参数,可实例中却有值,然后data却是一个函数,这又是为什么呢?vue中他究竟是怎么合并这些参数的呢?下面我们就从简单到复杂慢慢来学习一下vue的源码中究竟做了什么事吧。
Vue的初始化函数 —— _init函数
首先,Vue对象是一个构造函数,我们先隐匿掉其他部分,只留下和参数合并相关的代码,以便让我们进行关注点分离,更容易学习,精简后的Vue构造函数长这样:
function Vue (options) {
this._init(options);
}
哦豁,真的是简单的不能再简单了,其实也确实如此,_init函数就是Vue实例化的入口,我们来找到_init函数,看他又做了什么:
Vue.prototype._init = function (options) {
var vm = this;
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
};
上面的代码看起来只有一个作用,就是通过mergeOptions函数合并vm实例的参数,当然整个_init方法远不止这么简单,但我们的目的很明确:学习vue中的对于参数的合并处理是怎么做的,其他vue中对于一些其他操作我们可以忽略,事实上我也确实是把_init方法给删的只有一个功能了。你此时只需要关系vue的参数合并功能即可。这也是这篇文章的目的。
mergeOptions方法
重点在于这个mergeOptions方法,他的作用就是合并用户传递的参数的,看这一句:
mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
我们给mergeOptions方法传递了三个参数,第二个第三个好理解,一个就是用户传递参数,没传则为{}对象,vm就是当前new Vue时创建的那个实例,而这一句是什么? resolveConstructorOptions(vm.constructor) ,我们知道Vue中是有默认参数的,因为上一个例子中我们并没有向Vue构造函数中传递components参数,可vm实例中却还是有一个components属性,说明这是vue给他的默认值,那vue的默认值在哪呢?由于此处vue中的源码不是那么简单就可以拿出来的,所以省略为如下代码:
Vue.options = {
components: {},
directives: {},
_base: Vue,
};
也就是说vue的默认参数是创建在Vue这个构造函数中的,当然还有其他的,只是简化成这样,实际要复杂的多,不过正如我之前所说的:我们的关注点不在这里,则隐藏它对于我们来说就很有必要了。
那么 resolveConstructorOptions(vm.constructor) 其实就是合并构造函数中的默认参数的,此处你可以直接变成这样:
mergeOptions(
vm.constructor,
options || {},
vm
)
那么mergeOptions的作用就很明显了,合并Vue中的默认参数和用户传递过来的参数。
ps:为什么 resolveConstructorOptions 方法可以忽略?好吧,因为那个课堂老师没讲,然后我自己去看了一下也没看太懂,应该是合并父子类构造函数的默认参数的。没事,问题不大。
那mergeOptions方法内部究竟做了什么呢?我们来看一下:
function mergeOptions (parent, child, vm) {
checkComponents(child);
normalizeProps(child, vm);
normalizeInject(child, vm);
normalizeDirectives(child);
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm);
}
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
}
这个mergeOptions做的事不少,大致有以下几件:
1.checkComponents函数,校验child对象中组件参数components中的组件名是否符合规范
2.normalizeXXX函数,用来统一参数的形式,比如props支持数组和对象的写法,normalizeXXX函数的作用就是将他们统一转换为同一种形式,以便后续使用。
3.extends和mixins混入对象和合并,如果他们有extends或者mixins字段,则先进行遍历递归合并他们。
4.将child对象,扩展到parent对象上。
这里面,child和parent都是参数对象,而且,child的优先级要搞,也就是如果出现相同的属性参数,则以child中的为准,记住这个设定。后面的其他函数的设置都是后面的参数覆盖前面参数。
那么我们就是来讲讲他们的作用吧。
normalizeXXX函数
首先,checkComponents这个函数就不用讲了,校验组件名是否合法的,各位自行查看源码即可。现在看来一下类似的normalizeXXX函数做了什么事吧,我们拿normalizeDirectives做参考即可,这是代表指令的参数:
function normalizeDirectives (options) {
var dirs = options.directives;
if (dirs) {
for (var key in dirs) {
var def = dirs[key];
if (typeof def === 'function') {
dirs[key] = { bind: def, update: def };
}
}
}
}
代码不多,首先我们都知道,我们注册局部时,directives参数是一个对象,里面每一个指令属性值既可以接受一个函数,也可以指定一个对象用于设置指令的钩子函数。如果指令属性的值是一个函数,则它是同时作为bind和updata钩子函数进行绑定的,那么这时候,你就需要处理这两种不同的写法,将其转换为同一种写法了:
他会遍历options.directives,然后他会判断这个对象上面的每一个值,如果是函