- 学习教材:Vue源码解析、Vue源码
1.Vue的引入
1.1构造器
- Vue的==本质是一个构造器==
- 遵循UMD规范
- function Vue内部做了两件事
- 禁止直接调用,只能通过new的形式创建实例
- 调用 this._init(options)方法初始化
function Vue (options) {
// 保证了无法直接通过Vue()去调用,只能通过new的方式去创建实例
if (!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword');
}
this._init(options);
}
1.2 定义原型上的属性方法(简介)
initMixin
- 内部给Vue原型添加 _init 方法,在_init内部定义初始化时vue内部的初始化操作
stateMixin
- 在其内部完成与数据相关的属性方法,如this.$data等代理数据访问方法
eventsMixin
- 定义事件相关方法,如vm.$on
- 共实现了4个事件方法
- $on,绑定事件和处理函数
- $off,取消绑定函数
- $once,一次性的事件绑定
- $emit,发射,相当于手动触发事件
lifecycleMixin
- 定义生命周期相关方法
- 共实现了3个方法
- _update更新
- $forceUpdate强制更新
- $destroy销毁
rednerMixin
- 定义与渲染相关的方法
- $nextTick 将回调延迟到下一轮循环
- _render 渲染
1.3静态属性和方法的定义
- 在initGlobalAPI中定义了丰富的全局API方法
- vue.config的读取
- 工具类(如warn)配置(不暴露给公共使用)
- vue.set vue.del vue.nextTick
- 对
Vue.components,Vue.directive,Vue.filter的定义,这些是默认的资源选项构造函数options选项 - Vue.use Vue.mixin Vue.extend
2.构造器的默认选项
- 为components提供内置组件 KeepAlive、transition、transitionGroup
- 为directives提供内置指令 v-model、v-show
- filter配置项(没有默认值)
- 构造器自身 _base
extend方法
- 功能:对象合并
- 参数:对象1 to、对象2 _from
- 操作:将_from合并到to,若其中要相同属性,则覆盖to的属性
- 返回值:to
// 将_from对象合并到to对象,属性相同时,则覆盖to对象的属性
function extend (to, _from) {
for (var key in _from) {
to[key] = _from[key];
}
return to
}
- Vue中构造的上面那些内置组件、指令通过extend 方法合并到components和directives中
总结:Vue构造器的默认配置
Vue.options = {
components: {
KeepAlive: {}
Transition: {}
TransitionGroup: {}
},
directives: {
model: {inserted: ƒ, componentUpdated: ƒ}
show: {bind: ƒ, update: ƒ, unbind: ƒ}
},
filters: {},
_base
}
3.合并前的选项校验
-
初始化的第一步就是选项合并,将默认配置和用户输入的配置合并起来,并挂在到$options上(vue实例中可以访问该变量)
-
而合并的第一步就是校验。因为用户的输入是不可控的,所以要设置严格的规范限制,并在合并前检查用户是否遵从了这些规范
-
检查的重点在 props、inject、directives
// props,inject,directives的校验和规范化
normalizeProps(child, vm);
normalizeInject(child, vm);
normalizeDirectives(child);
- 除此以外还有其他选项需要校验
components规范校验
-
封装在方法checkComponents中,在合并选项方法mergeOptions中调用
-
功能:遍历每个components对象的属性值,对其进行校验
for (var key in options.components) {
validateComponentName(key);
}
validateComponentName方法
- 功能:校验标签合法性
- 命名是否符合规则,如不能使用数字开头
- 不能使用vue预制的组件名或html的保留字
props规范校验
props的形式
-
props有两种书写形式
- 数组形式:props:['a','b']
- 对象形式:props:{a:{type:'String',default:''},b:{...}}
-
实际最终都会在内部统一成对象形式
props校验:normalizeProps
一些用到的工具方法
- camelize:统一命名形式为驼峰形式,即将a-b改为aB
- isPlainObject:判断是否为简单对象,即使用{}或new Object创建
校验流程
- 判断是数组形式还是对象形式:Array.isArray判断数组,isPlainObject判断对象
- 是数组
- 倒序遍历,拿出每个数组元素,判断是否为string类型。数组形式的传值必须为string
- 是string类型,调用camelize统一形式,然后改写成对象形式,保存在res中
- 不是string,弹出错误警告
- 是对象
- 遍历props中的每个对象
- 将其key规范化
- 将type规范化
- 保存到res中
- 不是数组也不是对象:弹出错误警告
inject规范校验
provide/inject简介
- 作用:用于父组件向【后代】组件传递数据
- 特点:
- 后代不局限于子代,所有后代都可以使用其提供的数据。后代不需要知道数据来自哪里
- 非响应式:即之后这些值的修改不会在后代中被实时更新
改为响应式
官方文档说明:刻意设计成不可响应式,但如果传入可监听对象,那么还是可响应的
- 方法1
- 将值转为函数,要用箭头函数保证this指向,在return中使用this.val传值
- 使用时变成调用函数的形式
- **缺点:函数调用频繁。**比如下面代码,使用了两个地方用到了color,就会调用两次此函数
//提供
provide(){
return {
color:()=>{
return this.color
}
}
}
//使用
<template>
<div :style="{'color':color()}">传下来的颜色{{color()}}</div>
</template>
- 方法2
- 改为传递Vue实例
- 后代中实际获得的是实例对象,要通过 .xxx的形式获取该属性
- 这是很多UI组件库会用到的解决方法
- 更进一步可以直接在root上设置provide
//提供
provide(){
return {
color:this
}
}
//使用
<template>
<div :style="{'color':color.color}">传下来的颜色{{color.color}}</div>
</template>
链式查找
- provide像原型链一样,是会向上查找,找到就停止的
- 即:祖父提供color,父亲提供color,子孙组件只会找到父亲提供的color
内存泄漏
子组件图表可以利用provide提供的方法一直保持在内存中,导致内存占用,页面卡顿
inject校验:normalizeInject
- inject和props一样有对象和数组两种形式,且基本的书写规范也是一致的,所以校验流程也是一样的。详见上面props
directive规范校验
directive简介
- 功能:自定义指令
- 特点:五个钩子函数bind, inserted, update, componentUpdated, unbind
- 钩子函数用于在对应的时间节点触发,调用绑定在其上的函数方法
用法
定义可以分为两种版本,完整版和简写版
- 完整版:手动指定在何时触发方法
directives: {
focus: {
// 指令的定义
inserted: function (el) {
el.focus()
}
}
}
- 简写版:方法自动被绑定在bind和update上
directives: {
'focus': function(el, binding) {
el.style.backgroundColor = binding.value
}
}
directives校验:normalizeDirectives
- 流程:遍历对象,将简写版改成对象形式
4.子类构造器
- 子类构造器即Vue的extend
- 作用:生成一个Vue实例的子类
- 通常用在生成组件类,在挂载全局组件和设置了components属性的时候会使用到。在生成DOM的时候会new 实例化挂载
合并规则
- 重复的,子类覆盖父类
- 缺少的,使用父类上的
extend的实现
Vue.extend = function (extendOptions) {
extendOptions = extendOptions || {};
var Super = this;
var name = extendOptions.name || Super.options.name;
if (name) {
validateComponentName(name); // 校验子类的名称是否符合规范
}
// 创建子类构造器
var Sub = function VueComponent (options) {
this._init(options);
};
Sub.prototype = Object.create(Super.prototype); // 子类继承于父类
Sub.prototype.constructor = Sub;
Sub.cid = cid++;
// 子类和父类构造器的配置选项进行合并
Sub.options = mergeOptions(
Super.options,
extendOptions
);
return Sub // 返回子类构造函数
};
5.合并策略总览
Vue针对每个规定的选项都有定义好的合并策略,例如data,component,mounted等。如果合并的子父配置都具有相同的选项,则只需要按照规定好的策略进行选项合并即可。- 由于
Vue传递的选项是开放式的,所有也存在传递的选项没有自定义选项的情况,这时候由于选项不存在默认的合并策略,所以处理的原则是有子类配置选项则默认使用子类配置选项,没有则选择父类配置选项。
- 在完成上面讲过的规则校验后,合并方法mergeOptions的后续逻辑
实现方法
function mergeField (key) {
// 如果有自定义选项策略,则使用自定义选项策略,否则选择使用默认策略,每个策略都一个函数。
var strat = strats[key] || defaultStrat;
options[key] = strat(parent[key], child[key], vm, key);
}
调用
//parent和child是mergeOptions方法的参数,别忘了这些代码都在mergeOptions内
var options = {};
var key;
//以自定义选项优先,没有则使用默认策略
for (key in parent) {
mergeField(key);
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key);
}
}
默认策略defaultStrat
- 子存在则用子,不存在则用父
// 用户自定义选项策略
var defaultStrat = function (parentVal, childVal) {
// 子不存在则用父,子存在则用子配置
return childVal === undefined
? parentVal
: childVal
};
6.具体合并I:常规选项的合并
(1)el的合并
-
el是用于将vue实例挂载在已有的dom上,因此==子类和子组件上不能设定el==
-
合并策略
- 判断不是子类或子组件
- 使用默认策略进行合并
strats.el = function (parent, child, vm, key) {
if (!vm) { // 只允许vue实例才拥有el属性,其他子类构造器不允许有el属性
warn(
"option \"" + key + "\" can only be used during instance " +
'creation with the `new` keyword.'
);
}
// 默认策略
return defaultStrat(parent, child)
};
(2)data的合并
- vue的data合并可以分三层来看
- data的合并方法有三个参数
- parentVal----父类值
- childVal----子类值
- vm----实例
x1 基本判断
-
众所周知data在组件中必须以函数return的形式存在
-
所以data合并的第一步就是判断输入的是否为Vue实例,根据vm是否存在来判断
- 不是,则再判断是否为函数形式
- 是,进行合并
- 否,警告,返回父类值
- 是,直接进行合并
- 不是,则再判断是否为函数形式
// strats合并策略中data的合并策略
strats.data = function (parentVal, childVal, vm) {
// vm代表是否为Vue创建的实例,vm不存在则说明是子父类的关系
if (!vm) {
if (childVal && typeof childVal !== 'function') { // 必须保证子类的data类型是一个函数而不是一个对象
warn('The "data" option should be a function ' + 'that returns a per-instance value in component ' + 'definitions.',vm);
return parentVal
}
return mergeDataOrFn(parentVal, childVal)
}
return mergeDataOrFn(parentVal, childVal, vm); // 是vue实例时需要传递vm作为函数的第三个参数
};
x2 mergeDataOrFn
-
01-三个参数
- parentVal
- childVal
- vm
-
02-流程:判断是否是实例,仍然通过参数的vm是否被传入来判断
-
不是实例,则判断父子类是否都存在
-
子类不存在,则return父类parentVal
-
父类不存在,则return子类childVal
-
父子都存在,则return一个函数mergedDataFn。函数参数为父子类的data。(判断parentVal和childVal是否是函数形式,是则调用函数获取return的data;否则说明其就是data,直接传入)
-
//mergeData实现在后面讲,这里只需要知道它是用来合并的就好 return mergeData( typeof childVal === 'function' ? childVal.call(this, this) : childVal, typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal )
-
-
是Vue实例
- 同上判断实例传入的data形式获取实际data
- 获取vm构造函数默认data选项
- 若实例未传data,则直接return默认data
- 若实例传data,则
return mergeData(instanceData,defaultData)- mergeData的实现在后面讲
-
-
03-注意!上述流程不是直接在mergeDataOrFn中进行的
- 在此函数中只做了一件事:判断传入的是否是实例,根据情况返回不同的函数闭包。
- 不是则
return mergedDataFn - 是则
return mergedInstanceDataFn
- 不是则
- 前面02中的实现都是在这两个函数中,return也是指这两个函数的return而不是mergeDataOrFn的return
- 在此函数中只做了一件事:判断传入的是否是实例,根据情况返回不同的函数闭包。
-
04-结构示意
function mergeDataOrFn (parentVal,childVal,vm){
if(!vm){ //是父子类
if(){ //子类data选项不存在
return parentVal
}
if(){ //父类data选项不存在
return childVal
}
//上面都不符合,即父子都有data,需要合并
return function mergedDataFn(){
return mergeData(...)
}
}else{ //是Vue实例
var instanceData=vm实例配置的data选项
var defaultData=vm构造函数默认的data选项
if(){ //vm实例有配置data,与默认data合并
return mergeData(...)
}else{ //vm实例没有配置data,返回默认data
return defaultData
}
}
}
- ps:provide的默认策略也是这个,inject的则在后面讲
x3 mergeData
-
01-功能:合并data。mergeData才是实际进行合并的地方
-
02-参数
- to:子类data(实例data)
- from:父类data(默认data)
-
03-流程:从父类data拿出key,按照key遍历父子data选项
- 将父类中有而子类中没有的值添加到子类
if (!hasOwn(to, key)) { // set(to, key, fromVal); }- 判断父子类是否为复杂数据类型,是则要递归调用mergeData实现深度拷贝
- 最后处理完的子类data(参数to)即为合并结果,
return to
-
04-补充
- 获得key的方法
var keys = hasSymbol ? Reflect.ownKeys(from) : Object.keys(from); -
总结:mergeData的策略是:将父类整合到子类,其中父子都有则用子,子没有则用父
7.具体合并2:自带资源选项合并
- 01-资源选项和默认合并策略
// 资源选项
var ASSET_TYPES = [
'component',
'directive',
'filter'
];
// 定义资源合并的策略
ASSET_TYPES.forEach(function (type) {
strats[type + 's'] = mergeAssets; // 定义默认策略
});
- 02-mergeAssets的实现
- 功能:资源选项合并
- 流程
- 以父类资源选项为原型创建空对象
- 若子类的资源选项存在,则将其合并到该空对象上,将结果return(合并前还要判断保证资源选项是一个对象的形式)
- 子类没有资源选项,则return该空对象
// 资源选项自定义合并策略
//参数:1父类资源选项,2子类资源选项,3vm实例,4要合并的值的名称(比如components,directives等)
function mergeAssets (parentVal,childVal,vm,key) {
//create的第一个参数是要创建的对象的原型
var res = Object.create(parentVal || null); // 创建一个空对象,其原型指向父类的资源选项。
if (childVal) {
assertObjectType(key, childVal, vm); // components,filters,directives选项必须为对象
return extend(res, childVal) // 子类选项赋值给空对象
} else {
return res
}
}
- 合并结果示例:如下可见,子类必须通过原型链才能找到父类上的资源
var vm = new Vue({
components: {
componentA: {}
},
directives: {
'v-boom': {}
}
})
console.log(vm.$options.components)
// 根实例的选项和资源默认选项合并后的结果
{
components: {
componentA: {},
__proto__: {
KeepAlive: {}
Transition: {}
TransitionGroup: {}
}
},
directives: {
'v-boom': {},
__proto__: {
'v-show': {},
'v-model': {}
}
}
}
8.具体合并3:生命周期钩子函数合并
- 01-生命周期钩子和默认合并策略
var LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured',
'serverPrefetch'
];
LIFECYCLE_HOOKS.forEach(function (hook) {
strats[hook] = mergeHook; // 对生命周期钩子选项的合并都执行mergeHook策略
});
-
02-mergeHook实现
-
功能:合并父子类的钩子函数选项
-
参数:parentVal,childVal
-
注意,钩子函数==有先后顺序,且父类的应该在子类的前面调用。==所以钩子函数应该以数组的形式存储,按序调用
-
流程:通过判断父子选项的情况进行合并
- 父子都有,则用concat合并数组,返回合并结果
- 父无子有,则保证子为一个数组,返回子
- 父有子无,则返回父的数组
var res = childVal ? parentVal ? parentVal.concat(childVal) : Array.isArray(childVal) ? childVal : [childVal] : parentVal- 上述判断结果并非直接返回,而是先赋值给变量res。通过对res进行去重后再返回
return res ? dedupeHooks(res) : res
-
-
**03-去重方法dedupeHooks **
- 功能:给钩子函数数组去重,避免多个组件的钩子函数相互影响
- 参数:hooks钩子函数数组
- 流程:就是简单的数组去重流程,将结果赋给一个新数组res,
return res
function dedupeHooks (hooks) {
var res = [];
for (var i = 0; i < hooks.length; i++) {
if (res.indexOf(hooks[i]) === -1) {
res.push(hooks[i]);
}
}
return res
}
- 04-总结:合并的结果是一个数组,数组中包含父类和子类的钩子函数。在执行时先执行父类的钩子函数再执行子类的钩子函数
9.具体合并4:watch选项合并
-
01-合并策略:watch合并和钩子函数类似,也需要先执行父类再执行子类,因此也是以数组形式存储。区别是钩子函数结果必须是函数。watch选项的结果允许是对象、函数、方法名
-
02-流程
- 对火狐浏览器上object自带watch的兼容(略)
- 父有子无,return 以父为原型的空对象
- 确保watch是一个对象
- 子有父无,return 子
- 父子都有
- 声明空对象ret,将ret与父值合并
- 以子的属性名为key进行循环,分别获取父子的该属性值
- 获取的父属性转为数组形式
- 若父存在,则父concat子;若不存在,则将子转为数组。结果赋值给ret对象作为该key的属性值
return ret
-
03-例子:先执行父的watch,再执行子的watch
var Parent = Vue.extend({
watch: {
'test': function() {
console.log('parent change')
}
}
})
var Child = Parent.extend({
watch: {
'test': {
handler: function() {
console.log('child change')
}
}
},
data() {
return {
test: 1
}
}
})
var vm = new Child().$mount('#app');
vm.test = 2;
// 输出结果
parent change
child change
- 04-总结:最终结果是将父子选项合并成一个数组。数组的值可以是对象、函数、函数名
10.具体合并5:props、methods等相同合并策略选项合并
-
属性:props、methods、inject、computed
-
策略:
- 子不存在用父
- 子存在用子
- 相同选项子优先,子覆盖父
//四个连等,都是一个方法
strats.props =
strats.methods =
strats.inject =
strats.computed = function (
parentVal,
childVal,
vm,
key
) {
if (childVal && "development" !== 'production') {
assertObjectType(key, childVal, vm);
}
//父不存在则直接用子
if (!parentVal) { return childVal }
var ret = Object.create(null);
extend(ret, parentVal);
//子覆盖父
if (childVal) { extend(ret, childVal); }
return ret
};