编辑
2025-01-01
前端-Vue2
00
请注意,本文编写于 462 天前,最后修改于 0 天前,其中某些信息可能已经过时。

目录

一、问题场景
二、Vue 2 响应式原理回顾
三、错误写法为什么无效?
四、正确写法:使用 this.$set
五、完整示例对比
六、this.$set 的原理
七、Vue 2 响应式系统的主要限制
八、Vue 3 中的改进

在 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 响应式原理回顾

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

四、正确写法:使用 this.$set

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 的原理

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() // 手动触发视图更新 }

七、Vue 2 响应式系统的主要限制

场景是否响应式解决方案
为对象添加新属性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 中的改进

Vue 3 使用 Proxy 替代 Object.defineProperty,不再存在上述限制:

javascript
// Vue 3 中直接赋值即可触发响应式 row.dataSource = [] // ✅ 生效 row.dataSource.push({}) // ✅ 生效 delete row.someProperty // ✅ 生效
如果对你有用的话,可以打赏哦
打赏
ali pay
wechat pay

本文作者:Odboy

本文链接:

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