由于 JavaScript 的限制,Vue 不能检测数组和对象的变化。尽管如此还是有一些办法来回避这些限制并保证它们的响应性,那就是 Vue.set
。
实际上 Vue 也可以使用 Object.defineProperty
来做到对数组的监听,但是出于性能考虑 ,Vue 没有选择这个做法。
#实际应用
var vm = new Vue({
el: '#app',
template: '<div @click="addItem">{{list}}</div>',
data: {
list: [1, 2, 3]
},
methods: {
addItem() {
this.list[0] = 'a'
}
}
})
上述代码点击 div
之后会将 list
中的第一位索引换成 a
字符,打印一下 vm._data
已经可以看到这个时候 list
中的第一项已经变成了 a
,但页面视图却没有更新,可见通过数组索引修改内容并不能触发页面更新,所以这个时候 Vue.set
就该登场了:
var vm = new Vue({
el: '#app',
template: '<div @click="addItem">{{list}}</div>',
data: {
list: [1, 2, 3]
},
methods: {
addItem() {
this.$set(this.list, 0, 'a')
}
}
})
这个时候点击 div
页面视图也随之更新为 ['a', 2, 3]
。
#思考
顺着这个思路,我想到在生命周期 created
中通过下标修改数组的值,那么此时视图会变吗?
var vm = new Vue({
el: '#app',
template: '<div>{{list}}</div>',
data: {
list: [1, 2, 3]
},
created() {
this.list[0] = 'a'
}
})
这段代码执行完毕时,这个时候视图竟然跟着更新了!😅 奇怪了,不是说 Vue 不能检测到数组通过索引来修改 data 中值的情况吗?为什么这时候视图却更新了?
解释:由于
created
阶段data
数据已经初始化完成,可以修改其数据,以至于到后面的渲染函数拿到的也是修改后的list:['a', 2 ,3]
,所以渲染的时候并不是视图发生了更新变化,而是第一次render
拿到的list
已经是修改后的数据,这样也就解释得通了。
#set 源码
function set(target, key, val) {
if (process.env.NODE_ENV !== 'production' && (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
}
const ob = target.__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' &&
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(ob.value, key, val)
ob.dep.notify()
return val
}
我们可以看到,除去开发环境下的报错提示也就 10 多行代码,其核心逻辑就是触发 dep.notify
。
#数组处理
function set(target, key, val) {
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
// ... ignore
}
通过 Array.isArray
来判断 target
是否是数组,并验证 key
是否是数组下标,可能你会对 splice
之前对 target.length
赋值的操作感到困惑,这里举一个小案例
const key = 5
const arr1 = [1, 2, 3]
arr1.splice(key, 1, 666)
console.log(arr1) // [1, 2, 3, 666]
const arr2 = [1, 2, 3]
arr2.length = key
arr2.splice(key, 1, 666)
console.log(arr2) // [1, 2, 3, empty x 2, 666]
🧐 各位是不是看出什么"端倪"了?
这里的 key
代表将要改变的数组下标,当 key
大于数组的 length
时,splice
只会向数组的末尾新增值(也可理解为向数组 push 了一个值)那么这样跟我们的预期也就不同了,所以要对 key
和 length
取最大值。
在 Vue
的响应式系统中,已经对 data
选项中 Array
类型的原型上进行了一层代理,之后执行 splice
时,会触发 dep.notify
方法去通知 Watcher
进行视图更新。
#对象处理
function set(target, key, val) {
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = target.__ob__
if (!ob) {
target[key] = val
return val
}
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
首先检测 key
是否已经存在在 target
上了,那么直接赋值就会触发 setter
函数来对视图进行更新。
之后获取了 ob
,这是一个响应式数据的的标志,若目标对象不是响应式数据,那么直接赋值返回,这段逻辑在官网对 Vue.set
中的介绍中可以找到
最后,如果上述情况都不符合,说明:
- target 不是数组
- key 不在 target 上
- target 是一个响应式数据
那么这个时候就需要通过 defineReactive
将 target
的 key
值转换成响应式数据,并通过 ob.dep.notify
通知 Watcher
进行视图更新。
#总结
Vue.set
对数组和对象分别坐了不同处理:
- 对于数组,通过使用
splice
方法触发视图更新 - 通过对象,通过检测目标对象是否存在
key
、 是否是响应式数据和调用ob.dep.notify
来触发视图更新