为什么要使用这两个API
我们知道在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就是下标。
请注意,这里的
target不能是Vue实例或者根级属性($data),必须要在初始化实例前声明好所有根级属性,哪怕是一个空值。
解析源码
Vue.set和vm.$set指向的是同一个方法set。它们要做的就是一件事情,就是要将传入的对象的属性变成响应式的。
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)。如果是就抛出警告。
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key);
target.splice(key, 1, val);
return val;
}
第一行判断target是否是一个数组,并且key值是否是合法key。下面是检查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;
var arrayMethods = Object.create(arrayProto);
var methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse',
];
methodsToPatch.forEach(function(method) {
// 缓存原始数组的方法
var original = arrayProto[method];
def(arrayMethods, method, function mutator(...args) {
const result = original.apply(this, args);
...省略部分源码
// 发送更新通知
ob.dep.notify();
return result;
});
});
这是Vue定义的7个对象原型上的方法。
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val;
}
上面代码的意思是,如果target对象上已经存在key,且这个key不是Object原型对象上的属性。这说明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实例。上面说过target不能是根级属性或者Vue实例。
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;
}