首先,通过源码知道Vue和jQuery、underscore一样都是写在一个立即执行函数里,并且最终都是给当前的global环境添加一个名为Vue的属性
(function(global,factory){
//…
global.Vue = factory();
})(this,function(){})
当然Vue也不是直接给global添加Vue属性,期间还做了一层判断,根据判断当前的环境决定以什么样的方式导出,如果当前环境有exprots、有module那么以commonjs的规范导出,如果有define这个函数,define上amd这个属性,那么以requirejs规范导出,如果都没有,那么就直接给global添加Vue这个属性(这个就是常用的浏览器环境,global就是window对象)
//判断是否以commonjs的规范导出
typeof exports === ‘object’ && typeof module !== ‘undefined’ ? module.exports = factory() :
//判断是否以requirejs的规范导出
typeof define === ‘function’ && define.amd ? define(factory) :
//直接添加
(global.Vue = factory());
到这里可以明确,Vue给window添加了一个名为Vue的属性,值是一个的函数,这个函数是立即执行函数中的第二个参数,其值是一个匿名函数;
在匿名函数中,定义了名为Vue的函数,并且返回出去
function Vue(params) {
//检测当前的this指向的是不是Vue这个函数
//如果是以普通函数调用的,那么this指向的是window
//如果是以构造函数调用的,那么this指向的就是Vue这个构造函数
if(!(this instanceof Vue)){
console.error(“Vue必须作为构造函数调用”);
return;
}
//开始初始化
this._init(params);
}
//返回函数
return Vue;
这样就能明白new Vue()的时候到底是new的哪一个函数,同时,Vue在这里还使用instanceof进行了一次检测,它检测了this这个值到底指向的是谁,如果this指向的是window,那么就说明vue并不是以构造函数的形式被调用的,那么这种情况就应该被禁止,因此直接报错返回;
注:关于instanceof可以看MDN的解释《MSD上instanceof的解释》,另外关于this的具体可以看另外一篇博文《JavaScript》彻底理解this指向
一旦是以构造函数的方式调用的,那么此时正式进入初始化的阶段;
=============================================================
在这个阶段,Vue其实做了很多事情,其中很重要的一件事就是参数的合并;它把传入的对象上的data,钩子函数,methods等等与内置的属性component,directive等属性进行了合并,并且赋值到了$options这个对象上,具体就是:
var vm = new Vue({
el:“#app” ,
data:{
name:“Oliver尹”
},
beforeCreate() {},
})
console.log(vm.$options)
传入的参数明明没有comopnents,directivers等属性,但结果是有的,这就是因为进行了参数的合并;
当在Vue函数内启动初始化程序后
//开始初始化
this._init(params)
vue并没有给直接给函数Vue的prototype加上_init,而是在执行了initMixin()这个函数,在这个函数中给Vue的prototype添加了一个_init方法
//执行合并参数的初始化
function initMixin(Vue){
//给函数Vue的原型添加一个_init的方法
Vue.prototype._init = function(params) {
//this指向的就是Vue
console.log(this);
//存储this
var vm = this;
//对vue这个构造函数再扩展一个$options属性
//mergeOptions用于合并传入的参数和需要添加的默认参数
vm.$options = mergeOptions(Vue.options, params || {}, vm);
//执行beforeCreate钩子函数,也就是为什么执行beforeCreate的时候,data中以及有值了,因为以及进行了参数合并
callHook(vm, ‘beforeCreate’);
}
}
/**
-
把函数Vue作为参数传递进去,进行第一次初始化,
-
初始化的结果就是将传入的参数上的属性与默认的属性进行一次合并
-
*/
initMixin(Vue);
到这里发现,在初始化的_init函数中合并参数这个功能其实是靠mergeOptions()这个函数完成,当合并参数完成后将值赋值给了vm. o p t i o n s , 到 这 就 解 释 了 , 为 什 么 上 文 例 子 中 , 在 外 界 的 时 候 可 以 使 用 使 用 实 例 化 v m . options,到这就解释了,为什么上文例子中,在外界的时候可以使用使用实例化vm. options,到这就解释了,为什么上文例子中,在外界的时候可以使用使用实例化vm.options访问到vue根实例上的众多属性;
==============================================================
那么,接下来的重点就是mergeOptions()这个函数了,搜索源码查询到(这里剔除了与本文无关的内容,实际上在这些过程中还进行了众多处理,比如命名规范的检测等等,但这些都不在本次学习的内容内,因此暂时剔除)
/**
-
@param {*} parentVal Vue中需要默认带上的对象属性,比如component,directive
-
@param {*} childVal 传入的参数,比如el,data,computed等等
-
@param {*} vm this
*/
function mergeOptions(parentVal, childVal, vm){
var options = {};
var key;
for(key in parentVal){
mergeField(key)
}
for(key in childVal){
/**
- 假如parentVal和childVal都有相同的属性,那么为了保证相同的属性只被添加一次,执行检测
*/
if(!hasOwn(parentVal,key)){
//对于传入的对象上没有保留字的属性,那么执行合并
mergeField(key);
}
}
function mergeField(key){
/**
- strats其实是暴露给使用者的接口,Vue允许自定义一个函数处理传入对象内还没有被定义的方法
*/
var start = strats[key] || defaultStrat;
//执行合并
options[key] = start(parentVal[key], childVal[key], vm, key)
}
return options;
}
/**
-
obj,传入的对象
-
key,传入的属性名
-
判断obj上是否有名为key的属性
*/
var hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn(obj, key) {
return hasOwnProperty.call(obj, key)
}
在这个函数中分别对parentVal和childVal进行了for…in循环,并将属性名作为参数传递到了mergeField函数中进行处理;
乍一看这个mergeField有点懵,这里面的strats和defaultStrat都是一个跨作用域的属性,到这里甚至这两个属于什么类型都不知道,不慌,搜一下,先搜一下strats,
var config = ({
/**
- Option merge strategies (used in core/util/options)
*/
optionMergeStrategies: Object.create(null)
})
//获得空对象引用 strats === {}
var strats = config.optionMergeStrategies;
发现有点不明所以,定义了一个config,其中有一个属性optionMergeStrategies,这是一个空对象,然后将这个空对象赋值给了strats,那么其实这个strats就是一个空对象,那为什么不直接赋值一个空对象的?
搜索官网得知,这个optionMergeStrategies是暴露给外界的一个接口,它允许用户在外界定义合并策略,具体看optionMergeStrategies合并策略;假如直接赋值一个空对象,那么这里将无法暴露给用户使用;
也就是说,Vue其实是允许用户自定义属性的,比如除去常规属性外,假如用户定义在传入的参数上定义了一个特殊的属性count,可以通过optionMergeStrategies为其添加策略,具体如下例
//传入的参数多了一个count,这个属性不是官方提供的
var vm = new Vue({
el:“#app” ,
data:{
name:“Oliver尹”
},
beforeCreate() {},
count:1
})
//为count扩展策略
Vue.config.optionMergeStrategies.count = function (parent, child, vm) {
return child + 1
}
//合并结束后count的值就会变成2,不再是1
回到主题,那么到这里可以知道strats[key],就是去暴露的接口上查有没有自定义方法,如果有,那么将其对应的方法给到start;
然后再看一下defaultStrat
var defaultStrat = function(parentVal, childVal) {
return childVal === undefined ? parentVal : childVal;
};
根据前面的strats[key]可以猜到,既然前面的是合并的自定义策略,那这个肯定就是默认策略了,这是一个函数表达式,就是判断在传入的参数childVal有没有,如果有,返回childVal,如果没有就返回parentVal;
到这里可以明白
var start = strats[key] || defaultStrat;
这一行代码就是做了个判断,判断自定义的属性有没有对于的合并策略,如果有,那么按用户定义的合并策略进行,如果没有,那么就按默认的策略进行,并且不管哪种策略,都是一个函数;
接着就是执行合并了
//合并,并且将值赋值给了options[key]
options[key] = start(parentVal[key], childVal[key], vm, key)
options是一开始定义的空对象,并且该函数最终也是将options作为返回值返回出去的;
到这里函数基本弄清楚了,**mergeOptions()**这个函数就是在检测判断当前属性是内置的,还是自定义的,如果是自定义的,那么判断有没有自定义的合并策略,如果有,那么就按自定义的合并策略进行合并,如果没有,那么按默认的合并策略进行合并;
接着就是理解参数了,调用的时候一共传了3个参数(其中第一个参数Vue.options,实际上Vue不是这么写的,实际上远复杂的多,这里只是简化了)
vm.$options = mergeOptions(Vue.options, params || {}, vm);
其中params和vm都好理解,params就是实例化时传入的参数,vm指的是this,第一个参数Vue.options则是一个跨作用域的属性
Vue.options = Object.create(null); //等同于Vue.options = {}
通过查询知道,其实Vue.options默认的状态也是一个空对象,之后
//定义属性名,也就是vue上的component等
var ASSET_TYPES = [
‘component’,
‘directive’,
‘filter’
];
ASSET_TYPES.forEach(el=>{
Vue.options[el+“s”] = Object.create(null);
})
通过内置的ASSET_TYPES对其进行进行循环,将每个属性值添加到空对象上,这里就又有一个疑问,为什么要这么写,直接加s不好吗,为什么要通过遍历再加s,最好发现其实这个是为了保证属性的统一,因为component等这些有的上面是components,有的则是component,因此宁可多写几行代码,也要保证属性的高度统一;
因此,Vue.options就是一个内置属性的集合;
通过mergeOptions()这个函数将内置属性和传入的参数进行合并,并且合并的时候通过hasOwn()函数判断指定对象上是否有指定属性,假如有就跳过,从而达到相同属性只执行一次的效果,优化了合并速度;
之后判断传入对象上是否有自定义属性和自定义合并策略,如果有自定义合并策略,那么合并按自定义的规定合并,如果没有就按默认策略合并;
=================================================================
在合并策略中叙述到了一个自定义策略的问题,既然自定义策略是暴露给外界用户的接口,那么如果被篡改了怎么办,比如
//传入的参数多了一个count,这个属性不是官方提供的
var vm = new Vue({
el:“#app” ,
data:{
name:“Oliver尹”
},
beforeCreate() {},
count:1
})
//为count扩展策略
//Vue.config.optionMergeStrategies.count = function (parent, child, vm) {
// return child + 1
//}
//不对config.optionMergeStrategies进行扩展,而进程篡改,比如
Vue.config.optionMergeStrategies = 1;
直接将暴露的对象给修改掉怎么办,在Vue中的处理是这样的
//还是对自定义策略做一个监听,监听暴露的接口是不是有被修改,如果是被修改,那么报错,如果是访问直接将config返回出去
function initGlobalAPI(Vue){
var configDef = Object.create(null);
configDef.get = function () {
return config
}
configDef.set = function (params) {
console.error(“请不要尝试修改Vue.config”);
return;
}
Object.defineProperty(Vue, “config”, configDef)
}
Vue并没有直接去给对象扩展属性,而是采用监听属性的方式,如果你是访问的方式去获得属性,那么访问哪个,就直接通过get返回哪个,但如果是对其进set行设置,则不行,直接报错,不允许进行修改(新技能获得);
因此,Vue中是通过get和set属性对其进行监听的,而没有直接对一个对象进行修改;
================================================================
在2.5.1的版本中,Vue一共设置了11个生命周期函数,其原理是相同的,就是在不同的阶段执行函数callHook(),具体请看下例
//执行合并参数的初始化
function initMixin(Vue){
//给函数Vue的原型添加一个_init的方法
Vue.prototype._init = function(params) {
console.log(this);
//存储this
var vm = this;
/**
-
初始化前,对vue这个构造函数再扩展一个$options属性,
-
mergeOptions用于合并传入的参数和需要添加的默认参数
-
*/
vm.$options = mergeOptions(Vue.options, params || {}, vm);
/**
-
执行beforeCreate钩子函数,
-
也就是为什么执行beforeCreate的时候,data中以及有值了,因为已经进行了参数合并
-
*/
callHook(vm, ‘beforeCreate’);
}
文末
从转行到现在,差不多两年的时间,虽不能和大佬相比,但也是学了很多东西。我个人在学习的过程中,习惯简单做做笔记,方便自己复习的时候能够快速理解,现在将自己的笔记分享出来,和大家共同学习。
个人将这段时间所学的知识,分为三个阶段:
第一阶段:HTML&CSS&JavaScript基础
第二阶段:移动端开发技术
第三阶段:前端常用框架
-
推荐学习方式:针对某个知识点,可以先简单过一下我的笔记,如果理解,那是最好,可以帮助快速解决问题;
-
大厂的面试难在,针对一个基础知识点,比如JS的事件循环机制,不会上来就问概念,而是换个角度,从题目入手,看你是否真正掌握。所以对于概念的理解真的很重要。