本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
这是源码共读的第23期,链接:为什么 Vue2 this 能够直接获取到 data 和 methods ? 源码揭秘!。
一、说明
本文主要分析在Vue中,methods内定义的方法如何通过this访问到data中的数据。
看下图:
提示:
- 读者应自行准备vue.js文件,可从github的Vue源码处的dist打包文件中取vue.js,或者打开某Vue的cdn链接复制vue.js源码至本地新建的js文件中,注意不要使用.min压缩文件;
- 调试工具可以使用vscode的插件
live Server,直接以静态服务器方式打开index.html,右键=>选择open in live server,即可打开; - 或者使用
http-server插件,如下:
全局安装插件
npm i -g http-server
静态服务器打开文件
http-server .
或者指定静态服务器端口打开文件
http-server -p 8081 .
二、事前分析
其实我们可以思考下,如果我们自己定义一个构造函数,传递进去option对象参数,怎么才能实现调用methods上的方法,并通过this访问到data中的数据呢?我们先写个demo实验下。
下面我们简单的实现下这个需求。
// 构造函数
function Foo(option) {
// 解构参数
const { data, methods } = option;
// data数据挂到实例
for (const key in data) {
this[key] = data[key];
}
// 方法数据挂到实例
for (const key in methods) {
this[key] = methods[key];
}
}
// 参数
let op = {
data: {
name: "阿离",
age: 18,
},
methods: {
sayName() {
console.log(this.name); // 阿离
},
},
};
const foo = new Foo(op);
console.log(foo);
foo.sayName();
如此一来,我们就自己动手实现了上面提到的需求了。那么Vue是怎么做到的呢?
其实,Vue本质上也是把数据存储到实例上,不过Vue做了很多很多其他的事情,那么我们就开始看看Vue具体都是怎么做的。
三、源码分析
1.部分工具函数
因为需要分析的源码中有用到部分工具函数,为了方便等下分析时,读者不知道这个工具函数是什么,因此先把这些简单的工具函数罗列出来,留个大致的印象,知道是干什么的即可。
// hasOwn函数:判断obj里面是否有key属性
var hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn(obj, key) {
return hasOwnProperty.call(obj, key);
}
// isReserved:判断str是否以$ 或_ 开头的,因为两者开头是Vue内置方法会使用的,用户不能定义
function isReserved(str) {
var c = (str + "").charCodeAt(0);
return c === 0x24 || c === 0x5f;
}
// bind: 使用bind指定this,如果没有bind,则使用call代替
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;
2.断点调试
按上面【说明】提到的方式,用浏览器打开了index.html文件,打开控制台,选择sources进入源码处,在new Vue实例化处打上断点,刷新页面,进入调试功能。
提示:关于断点调试使用,此处不再过多介绍,读者可自行查阅。
步进到Vue构造函数,可以看到Vue构造函数本质。Vue构造函数本身很简单,就几句代码,调用了_init方法,而这个方法是在下面的initMixin方法中定义的,我们在initMixin(Vue);打上断点,然后步进看看里面有什么。
注意:这里使用的是this调用的_init,this是Vue实例,而在_init函数内部会定义变量vm = this,即后面的vm指的都是Vue实例对象,这点要记住。
3.initMixin
进入到initMixin函数,我们可以看到,上面提到的_init方法挂载在了原型对象上,在_init内部,创建变量vm = this,上面提到了_init调用者是实例,即vm = this = 实例对象,后面会一直用到vm。
下面定义了其他变量等处理一些其他逻辑,如实例化传进来的参数options是否有子组件组件等,如果是会进行一些其他处理,否则会继续处理mergeOptions函数,这个函数里面主要涉及到对构造函数Vue/当前实例化参数/实例对象等上的数据合并操作,我们暂时不考虑,里面比较复杂。
我们把注意力转移到initState(vm)函数上,里面涉及到props/data/methods/computed/watch等的处理。
打上断点,继续步进。
4.initState
进到initState函数,上面提到了有些暂时不需要关注的处理合并数据的逻辑,我们现在只需要知道,vm.$options里面包含了我们实例化时传进来的数据。
下面就分别对参数中是否有methods和data做判断,有的话就分别处理,我们把initMethods(vm, opts.methods);和initData(vm);分别打上断点,步进调试,先看对methods的处理。
本文暂时不会分析computed和watch的处理,读者有兴趣可自行断点调试查看。
5.initMethods
initMethods函数的处理逻辑其实很简单,判断三种情况(不是函数、和props传的属性冲突、键名使用了Vue内置方法开头的$和_)进行警告提示,然后把方法挂载到vm上,并把方法this指向改成vm实例对象。
【涉及工具函数:hasOwn、isReserved、bind】
6.initData
接下来我们跟着断点进入到初始化data的函数initData。
- 定义data变量接收实例化时参数data,注意这里同时把该data备份到了
vm._data上,等下会用到; - 根据data是函数类型还是对象类型做判断,如果是函数就执行getData函数,获取data函数返回的对象;
- 遍历data中属性,判断键名是否和props及methods中数据是否冲突;
- 最后使用proxy函数做了一层代理,等下我们会解释这块内容;
可以看到,初始化的initData函数内部逻辑也是很清晰的,通过上面的分析我们大概也知道了data的处理逻辑,那么我们针对上面的getData和proxy函数再进一步的解释下。
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();
}
}
pushTarget()和popTarget()涉及到响应式依赖收集的内容,暂时忽视,我们看到下面的return data.call(vm,vm),因此执行getData函数,就是对当data为函数时执行该函数,拿到返回值的对象。
proxy函数:
// Object.defineProperty的描述符
var sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop,
};
// 注意此处target是vm, sourceKey是'_data'即前面提到的备份data, key是每一个data中key
function proxy(target, sourceKey, key) {
// 设置get
sharedPropertyDefinition.get = function proxyGetter() {
return this[sourceKey][key];
};
// 设置set
sharedPropertyDefinition.set = function proxySetter(val) {
this[sourceKey][key] = val;
};
// 把所有的data的属性添加到vm上
Object.defineProperty(target, key, sharedPropertyDefinition);
}
分析:
- 可以看到proxy函数内执行给Vue实例vm上设置所有的data上的属性,至于属性值,并不是直接赋值为对应的键值,而是通过defineProperty使用了一层get和set的代理,即从
vm._data上得到对应key的值及设置对应的值; vm._data前面提到了就是传进来的data数据的备份,那么为什么要做一层代理呢?
举个例子: 为什么不按照下面的来实现,反而要做层代理呢?
// 以我们传递的data = {name: '阿离', age: 18} 为例
vm.name = '阿离';
vm.age = 18
vm._data = {name: '阿离', age: 18};
是因为这样就不能保证两处的数据一致,_data 中的数据就和vm.name及vm.age没有一点关系了,因为vm.name及vm.age保存的是单独的值,所以加一层代理之后,就保证了数据的一致性,因此只需要维护一份数据。
7.汇总
我们提取下前面讲到的Vue部分源码内容,里面剔除了警告、判断、对部分代码转换及其余不涉及当前讲解的代码,只保留针对methods和data处理的代码逻辑。
// 构造函数Vue
function Vue(options) {
this._init(options);
}
// 初始函数:
function initMixin(Vue) {
Vue.prototype._init = function (options) {
var vm = this;
vm.$options = options;
initState(vm);
};
}
// 初始化methods和data:
function initState(vm) {
var opts = vm.$options;
initMethods(vm, opts.methods);
initData(vm);
}
// 初始化methods:
function initMethods(vm, methods) {
for (var key in methods) {
vm[key] = methods[key].bind(vm);
}
}
// 描述符
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);
}
// 初始化data:
function initData(vm) {
var data = vm._data = vm.$options.data;
var keys = Object.keys(data);
var i = keys.length;
while (i--) {
var key = keys[i];
proxy(vm, "_data", key);
}
}
// 立即调用初始化函数
initMixin(Vue)
下面我们用上面的代码来测试,看看Vue处理methods和data的过程:
// 获取Vue实例
const vm = new Vue({
data: {
name: "阿离",
age: 18,
},
methods: {
sayName() {
console.log(this.name);
},
},
});
console.log(vm.name);
vm.sayName();
console.log(vm)
我们看下打印结果:
以上就是关于分析Vue中在methods的方法内使用this是如果访问到data中的数据的。
四、结语和收获
1.结语
说实话,这一期的内容我看了两天,来来回回打断点走了很多遍,其实从上面分析来看,如果简单化上面整个过程,逻辑性并不复杂,上面简化后的部分源码,也就是几十行,逻辑很清晰。但是,毕竟我们中间忽略了很多东西,就像上面提到的关于忽略合并数据的模块,等我们拿到vm.$options时,其实里面已经有很多东西了,Vue已经做过很多的逻辑处理。在调试的时候自然就会想看看到底做了什么,然后跟着看过去就会发现又涉及到很多的逻辑,头就会很大,毕竟实力尚浅,所以及时打住。
说了这么多,其实想表达的是:浅尝辄止,过犹不及。
有时候就需要量力而行,积跬步才能至千里。每次就针对某个部分去了解,去熟读,最后连成一个整体,水到渠成。
2.收获
- 再次加深对于调试的操作;
- 啃下Vue这片海洋中的一滴水;
3.寄语
有时候脑海中会闪过某个念头:费这么时间、精力去学习,去写笔记值得吗?学那么多用处大吗?学一点和学两点区别大吗?网上有很多笔记,需要自己记录吗?
我依然记得前年准备自学前端的时候,想着多了解前端,就从网上查资料,结果听到了很多的负面信息,比如:很多人自学失败,入行太晚了没希望,35岁危机等等。
但是,我不想我人生中第一个目标就此夭折,于是我立马买了笔记本电脑,进行了为期八个月的边工作边学习的历程。现在回想起来,真的是一段值得纪念的时光。
所以,现在为什么要问值得吗?你永远不知道未来会怎样,你唯一能知道的是,你在努力,没有辜负自己。
所以,只要心怀希望,有了方向,那么就持续奔跑吧,因为沿途的风景也很精彩。