$data是Vue实例中的实例属性,表示Vue实例观察的数据对象。实际上在Vue官网对这部分有较为详细的描述,这里就不再赘述了(具体可看官网的描述Vue选项/数据)。本篇文章从源码层次来梳理$data背后的逻辑,实际上就是一个问题:
假设存在属性name,为什么修改vm.$data.name与vm.name可以达到相同效果?
假设存在属性name,为什么修改vm.$data.name与vm.name可以达到相同效果?
以此问题为出发点,假设data中存在name,实际上就是关注于data初始化的源码逻辑。之前的文章(Vue实例创建之data处理和挂载)有关于这边处理逻辑的梳理,实际上data的处理在源码逻辑都在initData函数中,源码具体如下:
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 */);
}
initData函数的具体逻辑可以分为如下几个部分:
我们知道Vue是通过Object.defineProperty来实现数据劫持的,该方法的语法是:
Object.defineProperty(obj, prop, descriptor)
该方法只会对对象属性做数据劫持,这样会导致如下3个问题:
对于问题1,Vue源码中采用递归处理,对所有子对象属性都数据劫持。在源码中实际上就是在defineReactive$$1中多次调用observe函数来实现的,defineReactive$$1就是对data进行数据劫持处理的函数。
针对无法对对象动态添加的属性做数据拦截的问题,Vue内部提供如下的API:
之前一些项目Code Review时看到使用Object.assign来动态添加属性,这种方式也是不行的。
在使用Vue时针对整个对象的重新赋值是生效的,常见的两种做法:
// 方式1
this.obj = {
a : 1};
// 方式2
this.obj = Object.assign({
}, this.obj, {
b: 1});
上面两种做法本质是相同的即对obj对象重新赋值处理。为什么动态添加属性不行而重新赋值就可以呢?这取决于Vue源码中对data的多层代理。
实际上对于数据对象data,Vue实例中存在$data、_data这两种相同意义的属性,而多层代理也是结合这两个属性来实现的。
$data的数据劫持的具体处理在stateMixin中,主要逻辑如下:
var dataDef = {
};
dataDef.get = function () {
return this._data };
dataDef.set = function () {
warn(
'Avoid replacing instance root $data. ' +
'Use nested data properties instead.',
this
);
};
Object.defineProperty(Vue.prototype, '$data', dataDef);
由上面的逻辑可知:vm.$data 会调用原型链上的Vue.prototype.$data,而vm.$data.prop实际上就是调用vm._data.prop。
_data的数据劫持的具体的处理在initData中遍历逻辑中即proxy函数的调用,具体逻辑如下:
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,而关键在于target、sourceKey属性。源码调用如下:
proxy(vm, "_data", key);
由此可知vm.prop会调用vm._data.prop。
源码中选项data实际上在Vue实例中对应_data内部属性,而Vue对vm.$data.prop、vm.prop的做了一层数据劫持,底层都是调用_data对象上的属性。