本文已参与「新人创作礼」活动,一起开启掘金创作之路。
本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
【若川视野 x 源码共读】第23期 | 为什么 Vue2 this 能够直接获取到 data 和 methods 点击了解本期详情一起参与。
本文涉及
this指向
代码调试
bind函数使用
示例代码
<!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>Document</title>
</head>
<body>
<div id="app"></div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.10"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
message: 'Hello JueJin'
},
methods: {
getMessage() {
console.log(this.message)
}
}
})
console.log(vm)
vm.getMessage() // Hello JueJin
</script>
</body>
</html>
我们知道,运行时控制台会执行vm.getMessage(),输出this.message的数据。
我们尝试自己来实现一个对象
const obj = new Object({
data: {
message: 'Hello JueJin'
},
methods: {
getMessage() {
console.log(this.message)
}
}
})
console.log(obj.getMessage()) // TypeError: obj.getMessage is not a function
console.log(obj.methods.getMessage()) // undefined
一般而言我们是不能直接访问到obj里面的属性,而vue的对象可以这样使用,由此可得,是this指向问题。
- 我们来调试下代码
- 在浏览器
Sources中打上断点,进入vue的构造函数中
- 继续进入
_init函数
function initMixin$1(Vue) {
Vue.prototype._init = function (options) {
var vm = this;
/* ... 中间省略 ... */
// expose real self
vm._self = vm;
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook$1(vm, 'beforeCreate', undefined, false /* setContext */);
initInjections(vm); // resolve injections before data/props
// 初始化状态
initState(vm);
initProvide(vm); // resolve provide after data/props
callHook$1(vm, 'created');
/* istanbul ignore if */
if (config.performance && mark) {
vm._name = formatComponentName(vm, false);
mark(endTag);
measure("vue ".concat(vm._name, " init"), startTag, endTag);
}
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
};
}
- 在
initState中打上断点,F8调到该断点,进入函数
- 根据函数名,我们可以得知这两个函数应该就是初始化方法和数据的,加上断点,进去查看一下
function initMethods(vm, methods) {
var props = vm.$options.props;
for (var key in methods) {
{
// 判断是否是函数
if (typeof methods[key] !== 'function') {
warn$2("Method \"".concat(key, "\" has type \"").concat(typeof methods[key], "\" in the component definition. ") +
"Did you reference the function correctly?", vm);
}
// 判断方法名字和props是否冲突
if (props && hasOwn(props, key)) {
warn$2("Method \"".concat(key, "\" has already been defined as a prop."), vm);
}
// 判断方法是否已经存在vue实例中,并且方法名是否有$ _
if (key in vm && isReserved(key)) {
warn$2("Method \"".concat(key, "\" conflicts with an existing Vue instance method. ") +
"Avoid defining component methods that start with _ or $.");
}
}
// 函数绑定 bind
vm[key] = typeof methods[key] !== 'function' ? noop : bind$1(methods[key], vm);
}
}
通过以上判断,将methods属性中的方法赋值给vm,并且通过bind将this指针指向vm,所以可以this.methods===vm.methods
- 断点查看
initData
function initData(vm) {
var data = vm.$options.data;
data = vm._data = isFunction(data) ? getData(data, vm) : data || {};
if (!isPlainObject(data)) {
data = {};
warn$2('data functions should return an object:\n' +
'https://v2.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$2("Method \"".concat(key, "\" has already been defined as a data property."), vm);
}
}
if (props && hasOwn(props, key)) {
warn$2("The data property \"".concat(key, "\" is already declared as a prop. ") +
"Use prop default value instead.", vm);
}
else if (!isReserved(key)) {
proxy(vm, "_data", key);
}
}
// observe data
var ob = observe(data);
ob && ob.vmCount++;
}
通过校验后,会用proxy做了数据代理
- 查看
proxy定义
function proxy(target, sourceKey, key) {
sharedPropertyDefinition.get = function proxyGetter() {
return this[sourceKey][key];
};
sharedPropertyDefinition.set = function proxySetter(val) {
this[sourceKey][key] = val;
};
// 定义vm对象的key值
Object.defineProperty(target, key, sharedPropertyDefinition);
}
定义了get,set方法,我们可以知道,当我们获取数据,通过this.xxx时,实际上访问的是new Vue 实例中的_data对象,实际上就是this._data.xxx
构造自己的简易版实例
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: () => {},
set: () => {}
}
// vue中的代理方法
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)
}
function initMethods(vm, methods) {
for (const key in methods) {
// 省略类型检查
vm[key] = typeof methods[key] !== 'function' ? noop : methods[key].bind(vm)
}
}
function initData(vm) {
const data = (vm._data = vm.$options.data)
const keys = Object.keys(data)
var i = keys.length
while (i--) {
// ... 省略类型检查
var key = keys[i]
proxy(vm, '_data', key)
}
}
function MyVue(options) {
let vm = this
vm.$options = options
const opts = vm.$options
if (opts.methods) {
initMethods(vm, opts.methods)
}
if (opts.data) {
initData(vm)
}
}
const vm = new MyVue({
data: {
message: 'Hello JueJin'
},
methods: {
getMessage() {
console.log(this.message)
}
}
})
vm.getMessage() // Hello JueJin
总结
这个章节,我们尝试去看vue2的源码
- 通过
bind的函数去绑定方法 - 通过自定义
proxy的方式去将this指向vue实例中的_data,从而实现this.xxx访问属性。 - 然后,我们用了一小段代码来实现了这个简易版的
vue功能