Vue2 的关于 $set 的反直觉行为:为什么先赋值再 $set 不会触发 watch?

Vue set 的反直觉行为:为什么先赋值再 set 不会触发 watch?

核心问题

javascript 复制代码
watch: {
  c(val) {
    console.log('watch 触发了')
  }
}

// 路径 1:直接 $set
this.$set(this.c, 'b', 1000)
// ✅ 触发 watch

// 路径 2:先赋值再 $set  
this.c.b = 1000
this.$set(this.c, 'b', 1000)
// ❌ 不触发 watch

为什么同样用了 $set,结果却不同?


原因:$set 内部有属性存在性检查

$set 源码简化

javascript 复制代码
function set(target, key, val) {
  // 关键判断:属性是否已存在
  if (key in target && !(key in Object.prototype)) {
    target[key] = val  // ← 只做普通赋值
    return val         // ← 直接返回,不触发通知
  }
  
  // 属性不存在才走这里
  const ob = target.__ob__
  defineReactive(target, key, val)  // 定义响应式
  ob.dep.notify()  // ← 触发 watch!
  return val
}

关键点

$set 只在属性不存在时才调用 dep.notify()


两条路径的详细分析

路径 1:直接 $set(✅ 触发)

javascript 复制代码
// 初始状态
c = { a: 1 }  // b 不存在

// 执行
this.$set(this.c, 'b', 1000)

// 内部逻辑
'b' in c  // → false(属性不存在)
↓
添加响应式属性 b
↓
c.__ob__.dep.notify()  // ← 触发 watch!

路径 2:先赋值再 $set(❌ 不触发)

javascript 复制代码
// 初始状态
c = { a: 1 }  // b 不存在

// 第 1 步:普通赋值
this.c.b = 1000
// 现在:c = { a: 1, b: 1000 }
// 但 b 不是响应式的!

// 第 2 步:$set
this.$set(this.c, 'b', 1000)

// 内部逻辑
'b' in c  // → true(属性已存在!)
↓
只执行:c.b = 1000(普通赋值)
↓
不调用 dep.notify()  // ← 不触发 watch!

Vue 为什么这样设计?

Vue 的设计逻辑:

  1. 新属性 = 对象结构变化 → 需要通知所有观察者
  2. 已存在的属性 = 只是值变化 → 应该通过属性自己的 setter 触发

但问题是:路径 2 中的 b 虽然存在,但不是响应式的(没有 setter),所以:

  • $set 认为它已存在,不调用 notify()
  • 但它没有 setter,值变化不会触发更新
  • 结果就是:既不通知对象的观察者,也不触发属性的 setter

完整验证代码

javascript 复制代码
data() {
  return {
    c: { a: 1 }
  }
},

watch: {
  c(val) {
    console.log('watch 触发了!', val)
  }
},

mounted() {
  console.log('=== 测试 1:直接 $set ===')
  this.$set(this.c, 'b', 1000)
  // ✅ 打印 "watch 触发了!{a: 1, b: 1000}"
  
  setTimeout(() => {
    console.log('=== 测试 2:先赋值再 $set ===')
    this.c.d = 2000  // 先创建普通属性
    this.$set(this.c, 'd', 3000)  // 再用 $set
    // ❌ 不打印任何东西
  }, 1000)
  
  setTimeout(() => {
    console.log('=== 测试 3:$set 两次 ===')
    this.$set(this.c, 'e', 4000)  // 第一次
    // ✅ 打印 "watch 触发了!"
    
    this.$set(this.c, 'e', 5000)  // 第二次
    // ✅ 也会触发(因为 e 已经是响应式的)
  }, 2000)
}

dep.notify() 触发的是什么?

Vue 的两层依赖收集

javascript 复制代码
const c = {
  a: 1,
  __ob__: {
    dep: new Dep()  // ← 对象自己的 dep
  }
}

// 同时,每个响应式属性也有自己的 dep(通过闭包)
Object.defineProperty(c, 'a', {
  get() {
    // 收集依赖到 a 的 dep
  },
  set(val) {
    // 通知 a 的 dep
  }
})

区别

操作 触发的 dep watch: c watch: 'c.a'
c.a = 2 a 的 dep ❌ 不触发 ✅ 触发
$set(c, 'b', 1) c.__ob__.dep ✅ 触发 -
c = {} 对象替换 ✅ 触发 -

为什么不需要 deep 也能触发?

javascript 复制代码
watch: {
  c(val) {  // 没有 deep: true
    console.log('触发了')
  }
}

this.$set(this.c, 'b', 1000)
// ✅ 会触发

原因:

  • $set 触发的是 c.__ob__.dep(对象自己的 dep)
  • 监听 c 的 watcher 收集的就是这个 dep
  • 不需要 deep,因为是对象结构变化

deep 的作用:

javascript 复制代码
watch: {
  c: {
    handler(val) {},
    deep: true  // ← 递归收集所有属性的 dep
  }
}

// 现在修改已有属性也会触发
this.c.a = 2  // ✅ 触发(因为收集了 a 的 dep)

最佳实践

❌ 永远不要这样做

javascript 复制代码
this.c.b = 111
this.$set(this.c, 'b', 1000)  // 不会触发 watch

✅ 正确做法

javascript 复制代码
// 方案 1:只用 $set
this.$set(this.c, 'b', 1000)

// 方案 2:在 data 中预先声明
data() {
  return {
    c: {
      a: 1,
      b: null  // 预先声明
    }
  }
}
// 现在可以直接赋值
this.c.b = 1000  // ✅ 会触发

// 方案 3:整体替换对象
this.c = { ...this.c, b: 1000 }

// 方案 4:分两步(如果需要不同值)
this.$set(this.c, 'b', 111)  // 第一次触发
this.c.b = 1000              // 第二次触发(因为 b 已是响应式)

总结表格

场景 属性状态 $set 行为 是否触发 watch
$set(c, 'b', 1) 不存在 添加响应式 + notify ✅ 触发
c.b = 1; $set(c, 'b', 2) 存在(非响应式) 只赋值,不 notify ❌ 不触发
$set(c, 'b', 1); $set(c, 'b', 2) 存在(响应式) 触发 setter ✅ 触发
$set(c, 'b', 1); c.b = 2 存在(响应式) 触发 setter ✅ 触发

记住一句话

$set 只在属性不存在时才调用 dep.notify()
先赋值再 $set = 属性已存在 = 不会触发 watch


关键点:

  • $set 有属性存在性检查:key in target
  • 普通赋值创建的属性不是响应式的
  • 已存在的属性(即使非响应式)会让 $set 跳过 notify()
  • 永远不要在 $set 前先赋值!
相关推荐
消失的旧时光-19434 小时前
Kotlinx.serialization 对多态对象(sealed class )支持更好用
java·服务器·前端
少卿4 小时前
React Compiler 完全指南:自动化性能优化的未来
前端·javascript
广州华水科技4 小时前
水库变形监测推荐:2025年单北斗GNSS变形监测系统TOP5,助力基础设施安全
前端
广州华水科技4 小时前
北斗GNSS变形监测一体机在基础设施安全中的应用与优势
前端
七淮4 小时前
umi4暗黑模式设置
前端
8***B4 小时前
前端路由权限控制,动态路由生成
前端
军军3605 小时前
从图片到点阵:用JavaScript重现复古数码点阵艺术图
前端·javascript
znhy@1235 小时前
Vue基础知识(一)
前端·javascript·vue.js
terminal0075 小时前
浅谈useRef的使用和渲染机制
前端·react.js·面试
我的小月月5 小时前
🔥 手把手教你实现前端邮件预览功能
前端·vue.js