源码学习---为什么 Vue2 this 能够直接获取到 data 和 methods ?


为什么 Vue2 this 能够直接获取到 data 和 methods ?

当我们使用Vue.js开发应用时,经常会使用一些状态,例如props、methods、data、computed和watch。在Vue.js内部,这些状态在使用之前需要进行初始化。这里将介绍methods和data的内部原理,理解为什么data和methods的数据可以通过this访问。

new Vue被调用时发生了什么

1
2
3
4
5
6
7
function Vue(options) {
if ("development" !== 'production' && !(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword');
}
this._init(options);
}

首先来看Vue构造函数,在Vue构造函数中,首先进行安全检查,在非生产环境下,如果没有使用new来调用Vue,则会在控制台抛出错误警告我们:Vue是构造函数,应该使用new关键字来调用。 然后调用this._init(options)来执行初始化流程。下面再来看_init初始化函数

_init初始化函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function initMixin (Vue) {
Vue.prototype._init = function (options) {
var vm = this;
// 省略其他代码
vm._self = vm;
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, 'beforeCreate');
initInjections(vm);
initState(vm);
initProvide(vm);
callHook(vm, 'created');
};
}

可以看到,Vue.js会在初始化流程的不同时期通过callHook函数触发生命周期钩子。在生命周期钩子beforeCreate被触发之前执行了initLifecycle、initEvents和initRender.在初始化的过程中,首先初始化事件与属性,然后触发生命周期钩子beforeCreate。随后初始化provide/inject和状态,这里的状态指的是props、methods、data、computed以及watch。接着触发生命周期钩子created。而我们需要了解的data和methods就在initState这个函数中。

初始化状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function initState (vm) {
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函数中,我们看到了initMethods和initData函数,在接着往下看这两个函数

初始化methods

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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);
}
}

初始化methods时,只需要循环选项中的methods对象,并将每个属性依次挂载到vm上即可,在循环中会校验方法是否合法,合法后最终是通过bind显示绑定this指向为vm,所以这里也就是为什么我们能通过this访问到methods里面函数的原因

初始化data

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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
);
}
// 将data代理到Vue.js实例上
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, true /* asRootData */);
}

简单来说,data中的数据最终会保存到vm._data中。然后在vm上设置一个代理,使得通过vm.x可以访问到vm._data中的x属性。最后由于这些数据并不是响应式数据,所以需要调用observe函数将data转换成响应式数据。于是,data就完成了初始化。

在上述代码中,还有对data类型的判断,如果是函数,则需要执行函数并将返回值赋值给变量data和vm._data。

代码中调用了proxy函数实现代理功能。该函数的作用是在第一个参数上设置一个属性名为第三个参数的属性。这个属性的修改和获取操作实际上针对的是与第二个参数相同属性名的属性。proxy的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function noop (a, b, c) {}
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);
}

这里先声明了一个变量sharedPropertyDefinition作为默认属性描述符。接下来声明了proxy函数,此函数接收3个参数:target、sourceKey和key。随后在代码中设置了get和set属性,相当于给属性提供了getter和setter方法。在getter方法中读取了this[sourceKey][key],在setter方法中设置了this[sourceKey][key] 属性。最后,使用Object.defineProperty方法为target定义一个属性,属性名为key,属性描述符为sharedPropertyDefinition。通过这样的方式将vm._data中的方法代理到vm上。所有属性都代理后,执行observe函数将数据转换成响应式的。

总结

最后其实总的来说在data里通过Object.defineProperty代理方式将vm._data代理到vm上,在methods里通过bind绑定this指向为vm

Vue的这种设计,好处在于便于获取。也有不方便的地方,就是props、methods 和 data三者容易产生冲突。

文章整体难度不大,但非常建议读者朋友们自己动手调试下。调试后,你可能会发现:原来 Vue 源码,也没有想象中的那么难,也能看懂一部分。

启发:我们工作使用常用的技术和框架或库时,保持好奇心,多思考内部原理。能够做到知其然,知其所以然。就能远超很多人。

你可能会思考,为什么模板语法中,可以省略this关键词写法呢,内部模板编译时其实是用了with。有余力的读者可以探究这一原理。

摘抄两个工具方法

  1. hasOwn 是否是对象本身拥有的属性
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Check whether an object has the property.
* 是自己的本身拥有的属性,不是通过原型链向上查找的。
*/
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
  1. isReserved 是否是内部私有保留的字符串$ 和 _ 开头
1
2
3
4
5
6
7
8
9
10
11
/**
* Check if a string starts with $ or _
*/
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

最后用60余行代码实现简化版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
function noop (a, b, c) {}
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);
}
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 initMethods(vm, methods){
for (var key in methods) {
vm[key] = typeof methods[key] !== 'function' ? noop : methods[key].bind(vm);
}
}

function Person(options){
let vm = this;
vm.$options = options;
var opts = vm.$options;
if(opts.data){
initData(vm);
}
if(opts.methods){
initMethods(vm, opts.methods)
}
}

const p = new Person({
data: {
name: 'ZL'
},
methods: {
sayName(){
console.log(this.name);
}
}
});

console.log(p.name);
// 未实现前: undefined
// 'ZL'
console.log(p.sayName());
// 未实现前:Uncaught TypeError: p.sayName is not a function
// 'ZL'

参考文章

为什么 Vue2 this 能够直接获取到 data 和 methods ? 源码揭秘!
掘金