我们知道在Vue
中,对象和数组在某些情况下无法触发响应式数据更新。比如:
const vm = new Vue({
el: '#root',
data: {
price: 10,
},
});
vm.price = 20; // 重新渲染视图
vm.discount = 10; // 并不是响应式的数据
或者另一种情况,直接通过数组的下标修改数组的某一项:
const vm = new Vue({
el: '#root',
data: {
list: ['cat', 'dog', 'pig'],
},
});
vm.list[1] = 'fish'; // 不会重新渲染视图
为了解决上面的问题,Vue
给出了两个Api
,分别是Vue.set
和vm.$set
这两个API的区别是Vue.set
是定义在构造函数上的,而vm.$set
是定义在原型对象上的。使用方式如下:
Vue.set(target, key, value);
// 或者
vm.$set(target, key, value)
修改数组也是同样的方法,key就是下标。
不允许动态地添加根级响应的属性,必须要在初始化实例前声明好所有根级属性,哪怕是一个空值。
Vue.set
和vm.$set
指向的是一个方法set
。它们要做的就是一件事情,就是要将传入的对象的属性变成响应式的。
/**
* Set a property on an object. Adds the new property and
* triggers change notification if the property doesn't
* already exist.
*/
// 这句话的意思就是: 在一个对象上设置属性。当这个属性不存在当前对象上,
// 那么就添加这个属性和变更通知。
function set(target, key, val) {
if (isUndef(target) || isPrimitive(target)) {
warn(
'Cannot set reactive property on undefined, null, or primitive value: ' +
target
);
}
}
首先set
方法会进行判断,传入的target
是否是null、undefined
或是原始类型(string
, number
, boolean
, symbol
)。如果是就抛出异常—— 无法将undefined,null
或者原始类型target
设置为响应式属性。
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key);
target.splice(key, 1, val);
return val;
}
第一行判断target
是否是一个数组,并且key
值是否是合法key
。下面是检查的方法,相信都能看懂。
/**
* Check if val is a valid array index.
*/
function isValidArrayIndex(val) {
var n = parseFloat(String(val));
return n >= 0 && Math.floor(n) === n && isFinite(val);
}
第二行将target.length
设置为target.length
和key
最大的值。这是为了防止某些情况下会报错,比如: 设置的key
值,大于数组的长度。
new Vue({
el: '#root',
data: {
list: [1, 2]
}
})
Vue.set(vm.list, 10, 'error');
第三行是一个splice
方法,将key
位置的值替换为val
。注意:当调用splice
的时候就会重新渲染新的试图。因为这是一个特殊的splice
方法,Vue
将其改写了,看下面源码:
var arrayProto = Array.prototype;
// 创建了一个以arrayProto为原型的对象。
// 也就是 arrayMethods.prototype = arrayProto
var arrayMethods = Object.create(arrayProto);
var methodsToPatch = ['push','pop','shift','unshift','splice','sort','reverse'];
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// 缓存原始数组的方法
var original = arrayProto[method];
// def就是Object.definePropery 给对象定义上面7个方法
def(arrayMethods, method, function mutator() {
...省略部分代码(感兴趣可以看源码)
// 发送更新通知
ob.dep.notify();
return result;
});
});
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val;
}
上面代码的意思是如果target
对象上已经存在key
,且这个key
不是原型对象上的属性。则直接将val
赋值给这个属性。
var ob = target.__ob__;
if (target._isVue || (ob && ob.vmCount)) {
warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
);
return val;
}
__ob__指的是Observer
对象,vmCount
用来表示当前对象是否是根级属性。_isVue
用来表示是否是Vue
实例,用来避免被observed
,存在于Vue
实例上。那么这个if
就是用来判断target
是否是根级属性或是Vue
实例。
/**
* Observer class that is attached to each observed
* object. Once attached, the observer converts the target
* object's property keys into getter/setters that
* collect dependencies and dispatch updates.
*/
// 翻译: Observer这个类依"触摸"每个被观察的对象。一旦"触摸",
// observer将每个目标对象的属性转成getter/setter,收集所有依赖触发更新。
var Observer = function Observer(value) {
...省略
this.vmCount = 0;
def(value, '__ob__', this);
...省略
};
if (!ob) {
target[key] = val;
return val;
}
defineReactive$$1(ob.value, key, val);
ob.dep.notify();
return val;
第1行到第4行的意思是,如果target
上不存在Observer
对象(这说明target
只是一个普通的对象,不是一个响应式数据),则直接赋值给key
属性。
第5行,ob.value
其实就target
,只不过它是Vue
实例上data
里的已经被追踪依赖的对象。然后在这个被observed
的对象上增加key
属性。让key
属性也设置getter/setter
。
第6行,让dep
通知所有watcher
重新渲染组件。
function set(target, key, val) {
if (isUndef(target) || isPrimitive(target)) {
warn(
'Cannot set reactive property on undefined, null, or primitive value: ' +
target
);
}
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key);
target.splice(key, 1, val);
return val;
}
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val;
}
var ob = target.__ob__;
if (target._isVue || (ob && ob.vmCount)) {
warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
);
return val;
}
if (!ob) {
target[key] = val;
return val;
}
defineReactive$$1(ob.value, key, val);
ob.dep.notify();
return val;
}