误把“回调里改值”当响应式更新?一段真实案例讲清 Vue 的更新时机

小助手
小助手 揽星漫步繁花
社区会员
教程 25 浏览 0 回复

先看一段我刚接手项目时遇到的代码。需求很简单:用户点击按钮后,修改一个数组,同时把页面上的“已修改”状态置为 true。同事的写法是这样的:

export default {
  data() {
    return {
      list: [1, 2, 3],
      modified: false
    }
  },
  methods: {
    handleClick() {
      this.list.forEach((item, index) => {
        this.list[index] = item * 2
      })
      this.modified = true
      console.log(this.list)      // 输出 [2, 4, 6]
      console.log(this.modified)  // 输出 true
    }
  }
}

控制台里数据确实变了,但页面上列表纹丝不动,modified 状态也没任何反馈。他反复确认“数据明明改了”,最后甚至怀疑是 CSS 遮挡了更新。

问题出在哪?不是数据没改,而是 Vue 的响应式系统根本没“看见”这次修改。Vue 3 用 Proxy 拦截属性读取和赋值,但 this.list[index] = item * 2 这种通过索引直接赋值的方式,虽然能触发 Proxy 的 set 陷阱,可问题在于:forEach 循环里连续多次同步修改同一个数组,Vue 会把依赖收集和派发更新的过程合并到下一个微任务里执行。而这段代码在同一个事件循环里同步把 modified 也改了,等真正派发更新时,组件已经“认为”自己不需要重新渲染了——因为 modified 的 set 和 list 的 set 发生在同一轮,Vue 的调度器只保留最后一次变更。

更隐蔽的是,如果 list 是 props 传递下来的数组,或者来自 Vuex,这种索引赋值甚至根本不会触发任何响应。因为 props 是只读的,Vuex 的 state 也要求通过 mutation 修改。

正确写法其实很直白。要么用可响应的方法替换整个数组或使用 splice:

handleClick() {
  // 方式一:生成新数组整体替换
  this.list = this.list.map(item => item * 2)
  this.modified = true
}
handleClick() {
  // 方式二:splice 触发响应
  this.list.splice(0, this.list.length, ...this.list.map(item => item * 2))
  this.modified = true
}

关键差异在于:this.list = newArray 会触发 Proxy 对 list 这个属性的 set,Vue 能明确知道“整个数组变了”,从而重新渲染。而索引赋值虽然也走了 set 陷阱,但 Vue 内部对数组的依赖追踪是基于 length 和遍历操作的,直接改索引并不会让依赖了数组内容的渲染函数重新执行。

如果数据来自 Vuex,那就老老实实走 commit:

// store.js
mutations: {
  doubleList(state) {
    state.list = state.list.map(item => item * 2)
  }
}

// 组件里
this.$store.commit('doubleList')
this.modified = true

顺带提一个排查技巧:遇到“数据变了但页面没动”的情况,先打开 Vue Devtools,切到组件树,看目标组件有没有被标记为“已更新”。如果操作前后组件始终是灰色的,说明响应链路根本没通,别再盯着业务代码看了,直接检查赋值方式是否被 Vue 追踪。

最后把这段案例总结成一条口诀:改数组用整体替换或 splice,别用索引;改对象用新对象展开,别直接加属性;所有跨组件的数据,一律走 Vuex mutation。 记住这三条,能省掉大半“为什么没更新”的排查时间。

评论0
评论 · 0
还没有评论