如何调试vue2源码
我们在平时的项目开发中是直接使用脚手架创建vue项目,然后进行开发的,如果只是想测试一下vue2中某个api或者源码的话,不需要这么麻烦。可以直接引入vue.js。下面介绍一下如何使用简单的方法调试vue2代码。
创建一个文件夹testVue,用来存放自己的调试代码
引入vue.js
- 新建一个html文件,我的是index.html 可以使用cdn的方式引入vue.js
<script src="https://unpkg.com/vue@2.6.14/dist/vue.js"></script>
也可以将vue.js下载到本地,然后从本地引入到页面中 index.html代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>调试vue2代码</title>
</head>
<body>
<!-- 通过cdn的方式引入vue.js文件 -->
<script src="https://unpkg.com/vue@2.6.14/dist/vue.js"></script>
<script>
// 创建一个vue实例
const vm = new Vue({
data: {
name: '赵盼儿',
},
methods: {
sayHello(){
console.log(`你好呀, ${this.name}`);
}
}
})
// 打印信息vue实例中的data和method信息
console.log(vm.name); // 赵盼儿
console.log(vm.sayHello()); // 你好呀, 赵盼儿
</script>
</body>
</html>
创建vue实例
<script src="https://unpkg.com/vue@2.6.14/dist/vue.js"></script>
<script>
const vm = new Vue({
data: {
name: '赵盼儿',
},
methods: {
sayHello(){
console.log(`你好呀, ${this.name}`);
}
},
});
console.log(vm.name); // 赵盼儿
console.log(vm.sayHello()); // 你好呀, 赵盼儿
</script>
全局安装项目启动服务
上面的index.html文件我们可以采用在浏览器中打开的方式查看运行,当前我们也可以使用命令去启动项目。
全局安装项目启动服务命令
npm i -g http-server
启动项目
http-server
这时我们可以发现项目地址已经不再是我们本地路径了,而是ip地址http://127.0.0.1:8080/ 全局安装项目启动服务的好处就是可以通过本地ip去访问页面。
调试vue2代码
以上完成之后就可以正常调试代码了,具体浏览器调试代码的方式在我的这篇文章中已经介绍的很详细了,感兴趣的可以去翻阅。 在 new Vue这一行打上断点。然后在浏览器中刷新页面,在开发工具的源代码下我们可以看到代码停在了new Vue这一行,然后按下F11, 进入函数里,我们可以看到函数运行到了function Vue里面,这部分就是我们想要看到的源码。
data中的数据为什么可以用this直接获取到
通过上面的例子,我们可以知道为什么可以直接用this就能获取到data中的数据和methods中的方法是在vue这个构造函数中实现的,在 查看源码之前,我们假设一下,如果是你,会怎样实现呢?下面介绍一下我的实现方式。
手动实现模拟vue的构造函数
function newVue(opt) {
// 将opt的data上面的属性全部绑定到this上
if(opt.data) {
for(let key in opt.data) {
this[key] = opt.data[key];
}
}
// 将opt的methods上面的方法全部绑定到this上
if(opt.methods) {
for(let key in opt.methods) {
this[key] = opt.methods[key];
}
}
}
// 创建一个vue实例
const vm = new newVue({
data: {
name: '赵盼儿',
},
methods: {
sayHello(){
console.log(`你好呀, ${this.name}`);
}
}
})
// 打印信息vue实例中的data和method信息
console.log(vm.name); // 赵盼儿
console.log(vm.sayHello()); // 你好呀, 赵盼儿
查看vue源码
前面我们已经介绍了如何通过调试看到初始化Vue实例到底做了什么,下面我们就重点分析一下初始化后,为什么可以通过this获取到data和methods上面的属性或方法。
我们先把初始化这块的源码搬到这里来,然后具体分析。
- Vue构造函数--判断实例
function Vue (options) {
if (!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword');
}
this._init(options);
}
initMixin(Vue);
stateMixin(Vue);
eventsMixin(Vue);
lifecycleMixin(Vue);
renderMixin(Vue);
从上述代码中我们可以发现正如我们上面考虑的有一个Vue的构造函数,只是这里有一个关于是不是用了new调用构造函数的判断!(this instanceof Vue),如果使用了new调用才会去初始化,调用它的_init方法,否则就会提示'Vue is a constructor and should be called with the new keyword'。
在这里抛出一个小问题哈,为什么会加上这样一个判断,直接调用它的_init方法不行吗? 在回答这个问题之前,我们先来了解一下this和instanceof的用法
this
this是动态绑定,主要取决于它的运行环境,有:作为对象方法调用,作为函数调用,作为构造函数调用,和使用apply或call调用。
- 作为对象方法调用
let point = {
x: 0,
y: 0,
moveTo: (x, y) => {
this.x = this.x + x;
this.y = this.y + y;
}
}
point.moveTo(5, 6);
此时this指的就是point对象。也可以不写成箭头函数的形式,此时this指的也是point对象,箭头函数中this永远指的是他的上一级对象。 2) 作为函数调用
function copyVal(y) {
this.x = y;
}
copyVal(4);
x; // 4
上述代码中的函数是在window的环境下调用的,所以this指的是window对象。
let point = {
x: 0,
y: 0,
moveTo: function(x, y){
// 内部函数
let moveX = function(x) {
this.x = x;
};
let moveY = function(y) {
this.y = y;
};
moveX(x);
moveY(y);
}
};
point.moveTo(5, 6);
point.x; // 0
point.y; // 0
x; // 5
y; // 6
上述代码中的执行point.moveTo(5, 6);其实就是在当前环境调用了moveTo定义的函数体,此时没有明确的调用对象的时候,将 对函数的this使用默认绑定到全局的window对象。所以此时this指的是window对象。 3). 作为构造函数调用
function Point(x, y) {
this.x = x;
this.y = y;
}
let test = new Point(3, 4);
new 运算符主要做了以下几种方式: 第一步:创建一个空的对象{} 第二步:链接该对象(即设置该对象的构造函数)到另一个对象,即o.proto = Point.prototype 第三步:将步骤1新创建的对象作为this的上下文 第四步:如果该函数没有返回对象,则返回this
所以上述代码中的this指的就是test这个对象。 4) 使用apply或call调用
function add(x, y) {
console.log(x + y);
}
function del(x, y) {
console.log(x - y);
}
add.call(del, 3, 2); // 5
call是直接改变this的指向,上述代码中将del的内部指向了call。
instanceof
- instanceof是用来判断一个实例是否属于某种类型
function Foo() {
}
let foo = new Foo();
console.log(foo instanceof Foo);// true
- 在继承关系中用来判断一个实例是否属于它的父类型
function Aoo() {
}
function Foo() {
}
Foo.prototype = new Aoo(); // JavaScript原型继承
let foo = new Foo();
console.log(foo instanceof Foo);// true
console.log(foo instanceof Aoo); // true
如果在调用构造函数的时候没有使用new,那么this instanceof Vue中的this指的就是window, 如果使用了new,this指的就是上下文,才能调用到_init方法。
我们继续说this._init(options)里面的内部实现哈,在这句代码上打个断点,进入函数内部我们会发现,执行了initMixin中绑定在vue原型上的方法。代码如下:
- initMixin方法给Vue的原型绑定_init初始化函数
function initMixin (Vue) {
Vue.prototype._init = function (options) {
var vm = this;
// a uid
vm._uid = uid$3++;
var startTag, endTag;
/* istanbul ignore if */
if (config.performance && mark) {
startTag = "vue-perf-start:" + (vm._uid);
endTag = "vue-perf-end:" + (vm._uid);
mark(startTag);
}
// a flag to avoid this being observed
vm._isVue = true;
// 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
);
}
/* istanbul ignore else */
{
initProxy(vm);
}
// expose real self
vm._self = vm;
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, 'beforeCreate');
initInjections(vm); // resolve injections before data/props
initState(vm);
initProvide(vm); // resolve provide after data/props
callHook(vm, 'created');
/* istanbul ignore if */
if (config.performance && mark) {
vm._name = formatComponentName(vm, false);
mark(endTag);
measure(("vue " + (vm._name) + " init"), startTag, endTag);
}
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
};
}
从上面的代码中我们会发现初始化的时候主要做了以下几件事:
- initLifecycle(vm); :初始化生命周期
- initEvents(vm); : 初始化事件
- initRender(vm); : 初始化render(渲染)
- callHook(vm, 'beforeCreate'); : 注册beforeCreate钩子函数
- initInjections(vm); :初始化inject注入数据,这个步骤发生在初始化data/props之前
- initState(vm); : 初始化state
- initProvide(vm); :初始化provider, 这个步骤发生在初始化data/props之后
- callHook(vm, 'created'); :注册created钩子函数
可以看出初始化这个步骤对应的就是生命周期中关于beforeCreate和created两个阶段,我们大致可以猜到data和methods的初始化应该发生在initState()中。
我们可以在initState()这一行打断点,进入函数内部看一下内部实现逻辑。
- initState初始化状态
function initState (vm) {
vm._watchers = [];
var opts = vm.$options;
if (opts.props) { initProps(vm, opts.props); }
if (opts.methods) { initMethods(vm, opts.methods); }
if (opts.data) {
initData(vm);
} else {
observe(vm._data = {}, true /* asRootData */);
}
if (opts.computed) { initComputed(vm, opts.computed); }
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}
initState方法代码不多,我们可以很清楚地发现它主要做了以下几件事: 1)初始化props 2)初始化methods 3) 初始化data 4) 初始化computed计算属性 5) 初始化watch监听
既然是要知道为什么可以用this直接获取data中的属性,那么就在initData函数打个断点,进入函数内部看一下具体做了写什么?
- initData初始化data
function initData (vm) {
var data = vm.$options.data;
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {};
if (!isPlainObject(data)) {
data = {};
warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
);
}
// proxy data on instance
var keys = Object.keys(data);
var props = vm.$options.props;
var methods = vm.$options.methods;
var i = keys.length;
while (i--) {
var key = keys[i];
{
if (methods && hasOwn(methods, key)) {
warn(
("Method "" + key + "" has already been defined as a data property."),
vm
);
}
}
if (props && hasOwn(props, key)) {
warn(
"The data property "" + key + "" is already declared as a prop. " +
"Use prop default value instead.",
vm
);
} else if (!isReserved(key)) {
proxy(vm, "_data", key);
}
}
// observe data
observe(data, true /* asRootData */);
}
以上代码主要做了以下几个事情: 1)给data赋值,如果是函数,就执行getData方法,将返回的数据赋值给data,否则就直接赋值 2)判断data,如果是空对象就直接警告 3)遍历data里面的key(属性) 1) 如果属性在methods中存在,则直接警告 2) 如果属性在props中存在,则直接警告 3) 判断一下如果不是内部私有保留属性(isReserved方法判断属性key是不是以$或者_开头),则使用proxy封装一层代理 4) 监听data,使之成为响应式的数据
我们看看上面会用到的其他方法
// 判断是是否是真实的对象
var _toString = Object.prototype.toString;
function isPlainObject (obj) {
return _toString.call(obj) === '[object Object]'
}
isPlainObject({}); // true
isPlainObject(null); // false
isPlainObject({a: 1}); // true
isPlainObject(undefined); // false
// 判断对象上是否具有某属性,从hasOwn({ }, 'hasOwnProperty');可以看出不会通过原型链去查找
var hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn (obj, key) {
return hasOwnProperty.call(obj, key)
}
hasOwn({ a: undefined }, 'a'); // true
hasOwn({ }, 'a'); // false
hasOwn({ }, 'hasOwnProperty'); // false
hasOwn({ }, 'toString'); // false
// 判断是否是以_或者$开头
function isReserved (str) {
var c = (str + '').charCodeAt(0);
return c === 0x24 || c === 0x5F
}
isReserved('_data'); // true
isReserved('$options'); // true
isReserved('data'); // false
isReserved('options'); // false
// 判断是否是对象
function isObject (obj) {
return obj !== null && typeof obj === 'object'
}
isObject({ }); // true
isObject(null); // false
isObject(undefined); // false
isObject({ a: 1 }); // true
接下来在getData这一行打上断点,我们看一下getData内部是如何实现的。 4.1 getData获取数据
function getData (data, vm) {
// #7573 disable dep collection when invoking data getters
pushTarget();
try {
return data.call(vm, vm)
} catch (e) {
handleError(e, vm, "data()");
return {}
} finally {
popTarget();
}
}
如果data是函数,就调用函数,执行后获取到的对象就会赋值给initData方法中的data。 比如,我们在vue中写的时候也是按照function来写的。
data() {
return {
}
}
说到这里我们来提出一个小问题:为什么在组件开发中我们定义data的时候都是函数,而不是使用对象的形式。 在解答这个问题之前,我们先来看一个小例子:
// 如果是对象
let Component = function () {
this.data = this.data;
}
Component.prototype.data = {
a: '1',
b: '2'
}
let component1 = new Component();
let component2 = new Component();
component1.data.a = '12';
console.log(component1);
// {
// data: {a: '12', b: '2'}
// }
console.log(component2);
// {
// data: {a: '12', b: '2'}
// }
// 如果是函数
let Component = function () {
this.data = this.data();
}
Component.prototype.data = function (){
return {
a: '1',
b: '2'
}
}
let component1 = new Component();
let component2 = new Component();
component1.data.a = '12';
console.log(component1);
// {
// data: {a: '12', b: '2'}
// }
console.log(component2);
// {
// data: {a: '1', b: '2'}
// }
从上面的例子我们可以看出,如果是对象,每个实例就相当于是对原型上的对象的引用拷贝了一份,其中一个实例修改了原型上的数据,另一个实例也会被修改。而如果是函数,那么函数执行完是会返回一个对象的,创建不同的实例是相当于创建了一个新的对象,所以修改其中一个是不会对另外的实例产生影响的。放在vue.js中,也就是说组件可能会被不同的父组件使用,如果data是对象而不是函数的话,其中一个调用修改了组件的data,那么其他的组件的data的数据也会被污染。
4.2 proxy封装代理
var sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
};
function proxy (target, sourceKey, key) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
};
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val;
};
Object.defineProperty(target, key, sharedPropertyDefinition);
}
从上面代码可以看出来,主要用到的就是Object.defineProperty来定义对象。Object.defineProperty有三个参数:第一个参数是属性所在的对象,第二个参数是属性的名字,第三个参数是描述符对象(包括configurable, enumerable, writable, value). 这个方法也是双向数据绑定的核心。 在vue中,根据代理实现了this.xxx则是访问的this._data.xxx。
4.3 observe监听数据变化
function observe (value, asRootData) {
if (!isObject(value) || value instanceof VNode) {
return
}
var ob;
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__;
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value);
}
if (asRootData && ob) {
ob.vmCount++;
}
return ob
}
上述代码主要是监听数据变化,这块比较复杂,我们下次讲到vue源码之实现数据监听的时候会重点讲解
methods中的方法为什么可以用this直接获取到
上面已经讲完了为什么data可以用this直接获取到,我们接着将为什么可以用this直接获取methods中的方法,前面的过程是一样的,只是在执行initState函数的时候,在initMethods这一行代码上打断点,我们看一下这里面是如何实现的
function initMethods (vm, methods) {
var props = vm.$options.props;
for (var key in methods) {
{
if (typeof methods[key] !== 'function') {
warn(
"Method "" + key + "" has type "" + (typeof methods[key]) + "" in the component definition. " +
"Did you reference the function correctly?",
vm
);
}
if (props && hasOwn(props, key)) {
warn(
("Method "" + key + "" has already been defined as a prop."),
vm
);
}
if ((key in vm) && isReserved(key)) {
warn(
"Method "" + key + "" conflicts with an existing Vue instance method. " +
"Avoid defining component methods that start with _ or $."
);
}
}
vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm);
}
}
以上代码主要做了以下几个事情:
- 遍历methos里面的属性,看是不是函数,如果不是给出警告
- 判断props里面的数据和methos里面的属性是否相同,存在冲突,如果冲突了给出警告
- 判断methos中的属性是不是已经在 new Vue实例 vm 上存在并且以$或者_开头,如果是则给出警告
通过以上代码可以看出主要就是将methos上面的对象通过bind绑定函数的指向为this,这样就可以通过this直接调用methods上面的方法了。
方法绑定
function polyfillBind (fn, ctx) {
function boundFn (a) {
var l = arguments.length;
return l
? l > 1
? fn.apply(ctx, arguments)
: fn.call(ctx, a)
: fn.call(ctx)
}
boundFn._length = fn.length;
return boundFn
}
function nativeBind (fn, ctx) {
return fn.bind(ctx)
}
var bind = Function.prototype.bind
? nativeBind
: polyfillBind;
function noop (a, b, c) {}
总结
从这次源码中,我们看出之所以能通过this直接访问data和methods主要是请将数据和方法中所有的属性都绑定在vm这个实例上,通过bind指向改变this的指向,遍历属性,然后再通过Object.defineProperty方法来绑定data上面的属性,实现双向数据绑定和监听。