在 Vue 2 开发中,为响应式对象动态添加新属性或修改数组时,经常出现数据已变更但视图未更新的现象。本文通过实际代码示例,分析 Vue 2 响应式系统的底层限制,解释 this.$set 的必要性与使用场景,帮助开发者彻底理解并避免常见的响应式陷阱。
假设我们有一个表格组件,每一行(nodeParamScope.row)需要动态添加一个 dataSource 数组属性,用于存储该行关联的数据。常见的错误写法如下:
javascript// ❌ 错误写法:Vue 2 无法响应式检测到变化
const ds = nodeParamScope.row.dataSource || []
ds.push({ label: '', value: '' })
nodeParamScope.row.dataSource = ds
表面上看,dataSource 被成功赋值了,数组也增加了新元素,但视图不会更新。为什么?
Vue 2 的响应式系统基于 Object.defineProperty 实现。在组件初始化时,Vue 会递归遍历 data 中返回的对象,为每个已有属性添加 getter 和 setter。只有初始化时就存在的属性,Vue 才能将其变为响应式。
javascript// 初始化时 data 中定义了 dataSource: []
data() {
return {
row: {
dataSource: [] // ✅ 响应式
}
}
}
但如果 dataSource 一开始不存在,后续动态添加,Vue 无法自动追踪:
javascript// 初始化时没有定义 dataSource
data() {
return {
row: {} // dataSource 一开始不存在
}
}
// 后续动态添加
row.dataSource = [] // ❌ 非响应式,视图不更新
错误写法分三步:
javascript// 第1步:取出已有数组(或新建空数组)
const ds = nodeParamScope.row.dataSource || [] // ds 是一个普通变量
// 第2步:修改这个普通变量(数组本身被修改了)
ds.push({ label: '', value: '' }) // 修改的是 ds,不是 row.dataSource
// 第3步:将修改后的数组重新赋值给 row.dataSource
nodeParamScope.row.dataSource = ds // 赋值操作本身不会触发视图更新
问题的本质是:
| 操作 | 是否响应式 | 原因 |
|---|---|---|
| ds.push() | ❌ 不触发更新 | ds 只是一个普通变量,不是响应式对象的属性 |
| row.dataSource = ds | ❌ 不触发更新 | dataSource 是新增属性,初始化时不存在,Vue 没有为其设置 getter/setter |
javascript// ✅ 正确写法:使用 this.$set
if (!nodeParamScope.row.dataSource) {
this.$set(nodeParamScope.row, 'dataSource', [])
}
如果需要添加新元素到数组中:
javascript// 方法一:直接 push(前提:dataSource 已经是响应式属性)
nodeParamScope.row.dataSource.push({ label: '', value: '' })
// 方法二:使用 $set 添加元素(适用于通过索引修改)
this.$set(nodeParamScope.row.dataSource, index, newValue)
vue<template> <div> <div v-for="(item, index) in row.dataSource" :key="index"> {{ item.label }}: {{ item.value }} </div> <button @click="wrongAdd">错误添加(不生效)</button> <button @click="rightAdd">正确添加(生效)</button> </div> </template> <script> export default { data() { return { row: {} // 注意:dataSource 一开始不存在 } }, methods: { // ❌ 错误方式:视图不更新 wrongAdd() { const ds = this.row.dataSource || [] ds.push({ label: '测试', value: '123' }) this.row.dataSource = ds console.log('row.dataSource:', this.row.dataSource) // 数据确实变了 // 但视图没有变化! }, // ✅ 正确方式:视图正常更新 rightAdd() { if (!this.row.dataSource) { this.$set(this.row, 'dataSource', []) } this.row.dataSource.push({ label: '测试', value: '123' }) } } } </script>
this.$set(target, key, value) 做了什么?
javascript// Vue 2 源码简化版
function set(target, key, val) {
// 1. 如果 target 是响应式对象,且 key 已存在,直接赋值
// 2. 如果 target 是数组,调用 splice 方法(数组变异方法)
// 3. 如果 key 是新增属性,手动将新属性转换为响应式,并触发依赖更新
defineReactive(target, key, val) // 为新属性添加 getter/setter
target.__ob__.dep.notify() // 手动触发视图更新
}
| 场景 | 是否响应式 | 解决方案 |
|---|---|---|
| 为对象添加新属性 | ❌ | this.$set(obj, key, value) |
| 删除对象属性 | ❌ | this.$delete(obj, key) |
| 通过索引修改数组元素 | ❌ | this.$set(array, index, newValue) |
| 直接修改数组长度 | ❌ | array.splice(newLength) |
数组的 push/pop/shift/unshift/splice/sort/reverse | ✅ | 直接调用即可 |
| 修改已有对象属性的值 | ✅ | 直接赋值 |
Vue 3 使用 Proxy 替代 Object.defineProperty,不再存在上述限制:
javascript// Vue 3 中直接赋值即可触发响应式
row.dataSource = [] // ✅ 生效
row.dataSource.push({}) // ✅ 生效
delete row.someProperty // ✅ 生效


本文作者:Odboy
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 CC 4.0 BY-SA 许可协议。转载请注明出处!