详解 Vue.set 和 this.$set 的用法与使用场景
原文主要介绍 Vue 2 中对象、数组的响应式检测限制,以及
Vue.set/this.$set的用法。本文保留 Vue 2 的历史 API 和示例,同时明确 Vue 3 已移除这些 API,并解释 Proxy 为什么不再需要它们。
一、为什么 Vue 2 需要 Vue.set
Vue 2 的响应式系统在初始化时遍历对象已有属性,并使用 Object.defineProperty 转换为 getter/setter。这个机制无法自动发现初始化之后新增或删除的对象属性,也无法拦截数组下标赋值和 length 修改。
const state = {
user: {
name: '小明',
},
items: ['a', 'b', 'c'],
}
// Vue 2 中这些操作不会按预期触发响应式更新
state.user.age = 18
state.items[1] = 'x'
state.items.length = 1
注意:这不是说 Vue 2 完全不能检测对象或数组变化。已有属性的普通赋值,以及被 Vue 改写过的数组变异方法,仍然可以触发更新。
二、Vue 2 数组的变化
1. 使用数组变异方法
Vue 2 对下面七个方法进行了包装:
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
this.items.push({ id: 4 })
this.items.splice(1, 1, 'new value')
this.items.sort(compareItems)
这些方法会通知数组 observer,并且对新增的对象元素继续做响应式观测。

2. 用 splice 替代数组下标赋值
// 不推荐:Vue 2 无法可靠检测
this.items[index] = newValue
// 推荐
this.items.splice(index, 1, newValue)
3. 使用 Vue.set 或 this.$set
Vue.set(this.items, index, newValue)
// 组件实例方法
this.$set(this.items, index, newValue)
两者最终解决的是同一类问题:对数组下标设置值时,转而调用 splice,让 Vue 2 的数组 observer 能够收到通知。
4. 修改数组长度
// Vue 2 中不是响应式的
this.items.length = newLength
// 删除从 index 开始的剩余元素
this.items.splice(index)
// 截取后替换整个数组
this.items = this.items.slice(0, newLength)
使用 Vue.set 并不能把 length 赋值变成响应式;应使用 splice 或替换数组引用。
5. 替换整个数组
非变异方法 filter、concat、slice 会返回新数组,赋值给响应式属性即可:
this.items = this.items.filter(item => item !== 'obsolete')
this.items = this.items.concat(newItem)
Vue 2 的虚拟 DOM 通常可以复用新旧数组中相同的对象和 DOM 节点,因此“替换数组”不等于一定重建整个列表。
三、Vue 2 对象的变化
1. 初始化时声明字段
如果字段以后会使用,最稳妥的方式是在 data 中提前声明:
new Vue({
data() {
return {
user: {
name: '小明',
age: null,
},
}
},
})
已有响应式属性直接修改是可以检测的:
this.user.name = '小红'
this.user.age = 20
2. 使用 Vue.set
给嵌套对象添加新属性:
Vue.set(this.user, 'gender', '女')
this.$set(this.user, 'gender', '女')
完整示例:
<div id="app">
<ul>
<li v-for="(value, name) in object" :key="name">
{{ name }}:{{ value }}
</li>
</ul>
</div>
<script>
const vm = new Vue({
el: '#app',
data: {
object: {
title: 'How to do lists in Vue',
author: 'Jane Doe',
publishedAt: '2016-04-10',
},
},
})
vm.$set(vm.object, 'test', 'newthing')
</script>
原文的示例中把多行代码拼在同一行,并且缺少属性逗号;上面按照 Vue 2 可运行的写法整理。
3. 整体替换对象
也可以创建一个新对象并替换引用:
this.object = {
...this.object,
test: 'newthing',
}
不要期待下面的 Object.assign 对新增字段自动产生 Vue 2 响应式通知:
// 不推荐作为 Vue 2 新增属性的解决方案
Object.assign(this.object, {
test: 'newthing',
})
如果使用 Object.assign,应将结果替换回响应式属性:
this.object = Object.assign({}, this.object, {
test: 'newthing',
})
四、Vue.set 的签名和使用限制
Vue.set(target, propertyNameOrIndex, value)
this.$set(target, propertyNameOrIndex, value)
对象
Vue.set(vm.someObject, 'b', 2)
this.$set(this.someObject, 'b', 2)
数组
Vue.set(vm.items, 1, 'x')
this.$set(this.items, 1, 'x')
不能用来添加根级 data 属性
Vue 2 不允许在实例创建后随意添加新的根级响应式属性:
const vm = new Vue({
data: {
a: 1,
},
})
// 不是可靠的响应式根属性添加方式
vm.b = 2
Vue.set(vm, 'b', 2)
正确做法是在 data 中预先声明:
const vm = new Vue({
data: {
a: 1,
b: null,
},
})
或者替换已存在的嵌套对象:
vm.options = {
...vm.options,
b: 2,
}
原文中的 app.data 是错误访问;Vue 实例上没有一个可以用来替代根 data 的 app.data 对象。根 data 中的字段会代理到 vm,但新增根属性仍应在初始化时声明。
五、Vue 2 中 this.$set 大概做了什么
下面是概念化实现,省略 observer 类型判断、警告和边界处理:
function set(target, key, value) {
if (Array.isArray(target) && Number.isInteger(Number(key))) {
target.length = Math.max(target.length, Number(key) + 1)
target.splice(Number(key), 1, value)
return value
}
if (key in target && !(target._isVue || target.__ob__)) {
target[key] = value
return value
}
const ob = target.__ob__
if (!ob) {
target[key] = value
return value
}
defineReactive(ob.value, key, value)
ob.dep.notify()
return value
}
Vue 2 的真实实现会:
- 对数组下标调用
splice; - 如果属性本来存在,直接赋值;
- 对已观测的嵌套对象调用
defineReactive; - 通过对象 observer 的 dep 通知相关 watcher;
- 拒绝或警告对 Vue 实例根对象添加属性。
Vue.set 不是“让任意普通对象永久变成 Vue 响应式对象”的函数。目标对象最好已经属于 Vue 2 的响应式数据树。
六、Vue 3 为什么不再需要 Vue.set
Vue 3 的 reactive 使用 Proxy,可以拦截对象属性新增、删除、数组下标和相关集合操作:
<script setup>
import { reactive } from 'vue'
const state = reactive({
user: {},
items: ['a', 'b', 'c'],
})
state.user.age = 18
state.items[1] = 'x'
state.items.length = 1
delete state.user.age
</script>
这些操作都可以被 Vue 3 的 Proxy handler 追踪。Vue 3 已移除:
Vue.set
Vue.delete
this.$set
this.$delete
迁移后的写法就是直接修改 reactive 对象:
state.user.age = 18
state.items[index] = newValue
delete state.user.age
如果使用 ref 保存数组或对象,要修改 .value:
import { ref } from 'vue'
const items = ref(['a', 'b', 'c'])
items.value[1] = 'x'
const user = ref({})
user.value.age = 18
对于 shallowReactive、shallowRef、markRaw 或外部状态库,还要根据其浅层/非响应式语义判断是否需要替换引用。
七、Vue 2 与 Vue 3 对照
| 场景 | Vue 2 | Vue 3 |
|---|---|---|
| 已有对象属性赋值 | 直接赋值 | 直接赋值 |
| 新增嵌套对象属性 | Vue.set/this.$set 或替换对象 | 直接赋值 |
| 删除对象属性 | Vue.delete/this.$delete 或替换对象 | delete state.key |
| 数组下标赋值 | Vue.set 或 splice | 直接赋值 |
修改 length | splice 或替换数组 | Proxy 可追踪,但仍需确认业务语义 |
| 根级新属性 | 初始化时声明 | setup/reactive 状态中直接声明或添加 |
| API | Vue.set、vm.$set | 已移除 |
Vue 3 不需要 $set,并不意味着可以不考虑状态设计。推荐在初始化时声明主要字段,使用 TypeScript 类型、明确的状态结构和不可变替换来提高可读性。

八、常见误解
误解 1:Vue 2 所有数组直接赋值都不响应
错误。push、splice 等七个变异方法会触发更新;直接设置下标和 length 才是经典限制。
误解 2:只要使用 Vue.set 就能添加根属性
错误。Vue 2 实例的根级属性需要在初始化时声明;Vue.set 主要用于已观测对象的嵌套属性和数组下标。
误解 3:Vue 3 完全不需要关注新增字段
Vue 3 可以追踪 Proxy 对象的新增属性,但如果对象不是 reactive、被 markRaw、来自 shallowRef,或被错误解构成普通值,仍可能没有你期望的响应式行为。
误解 4:替换数组一定会重建所有 DOM
Vue 的 renderer 会根据 vnode 和 key 尽量复用节点。数据引用被替换与 DOM 节点全部重建不是一回事。
总结
- Vue 2 的限制来自
Object.defineProperty无法拦截新增属性、删除属性、数组下标和 length; - Vue 2 对象新增属性用
Vue.set/this.$set,数组下标可用$set或splice; - Vue 2 根级 data 属性应在初始化时声明,不能靠
$set事后补救; Object.assign直接给 Vue 2 对象增加新字段不可靠,推荐替换对象引用;- Vue 3 使用 Proxy,通常直接新增、删除和修改数组下标即可,不再提供
Vue.set/this.$set; - Vue 3 的 shallow/非响应式 API 和错误解构仍然可能影响响应式行为。
参考资料
原文作者:Dax1。原文关于 Vue 2 数组变异方法、对象新增属性、Vue.set、this.$set 和使用场景的内容予以保留;错误的根属性示例、拼接损坏的代码、app.data 访问、Vue 3 仍需 $set 等表述已修正,并补充 Vue 3 Proxy 方案。