Code前端首页关于Code前端联系我们

Vue2 是怎么让数组有响应式能力的?

terry 4小时前 阅读数 9 #Vue
文章标签 Vue2;数组响应式

用过Vue2做项目的同学,十有八九碰到过“数组数据改了,页面却纹丝不动”的情况——明明自己亲手改了数组里的内容,列表渲染却像“聋子的耳朵”,急得人抓耳挠腮,这背后藏着Vue2对数组响应式的特殊处理逻辑,今天咱把数组响应式的原理、踩坑点、正确用法一次性掰碎了讲,解决你开发时的困惑~

聊数组前,先回忆下对象的响应式:Vue 靠 `Object.defineProperty` 遍历对象属性,给每个属性加 getter/setter,读取时收集谁用了这个数据(依赖收集),修改时通知这些“依赖”更新(触发更新),但数组要是照搬这套逻辑,**性能会崩掉**——假设数组有几百个元素,给每个索引都做 `defineProperty`,那遍历、拦截的开销谁扛得住?

Vue2 对数组搞了“特殊劫持方案”:它直接改写了数组原型上的 7 个“变异方法”(push、pop、shift、unshift、splice、sort、reverse),这些方法原本属于 Array.prototype,Vue 会给数组的 __proto__ 换一个“增强版原型”,让数组调用这些方法时,先执行 Vue 自己的逻辑(比如把新元素变成响应式、通知视图更新),再执行原生逻辑。

举个🌰:当你调用 arr.push(1),这个 push 已经不是原生的 push 了——Vue 重写的 push 会先把“1”变成响应式(1”是对象/数组,还会递归劫持),然后通知所有依赖这个数组的 Watcher(可以理解为“视图更新的传令兵”)“数据变了,赶紧更新视图”,最后才把“1”真正推进数组,这样就能保证数组变了,视图也跟着变。

为啥直接改数组索引或长度,页面没反应?

这是很多同学踩的大坑:arr[0] = 新值 或者 arr.length = 0 后,页面完全没变化,得从 Vue2 的响应式原理扒起——

对象的属性能被拦截,是因为每个属性都有 setter;但数组的索引和长度没有 setter,Vue2 没给数组的每个索引做 defineProperty(性能顶不住),也没给 length 加 setter,所以当你直接改索引(arr[1] = 5),Vue 根本“感知”不到这个修改,自然不会触发视图更新。

举个真实开发场景:做 TodoList 时,想把第 3 项改成“已完成”,要是直接写 todoList[2].done = true(todoList[2] 是对象,改对象内部属性会触发更新,因为对象自己有响应式);但如果是 todoList[2] = 新对象(改数组索引),视图就不更新了,再比如想清空数组,写 todoList.length = 0,页面也不会清空,因为 length 没被 Vue 劫持。

Vue2 里操作数组,哪些方法能触发响应式更新?

记住两类核心操作:数组原型上的 7 个变异方法,以及 Vue 提供的辅助工具

7 个“变异方法”(修改原数组的方法)

这 7 个方法有个共同点:会修改原数组,而且被 Vue 重写后,调用时会自动触发视图更新,分别是:

  • push():往数组末尾加元素
  • pop():删除数组最后一个元素
  • shift():删除数组第一个元素
  • unshift():往数组开头加元素
  • splice(起始索引, 删除个数, 新增元素...):增/删/改都能用(比如改第 2 项,用 splice(1, 1, 新值)
  • sort(排序函数):给数组排序
  • reverse():反转数组顺序

举个改指定索引的例子:想把 todoList 第 2 项(索引 1)改成新任务,用 splice 就很方便:

// 把索引1的元素删掉,换成新对象
todoList.splice(1, 1, { text: '新任务', done: false })

Vue 提供的 Vue.set(或实例上的 this.$set

当你需要给数组指定索引赋值时,必须用 $set,它的原理是内部调用 splice 方法(因为 splice 是变异方法,能触发更新),用法长这样:

this.$set(数组, 索引, 新值)
// 例子:把第3项(索引2)改成“已完成”
this.$set(todoList, 2, { text: '旧任务', done: true })

替换数组(用新数组覆盖原数组)

filtermapslice 这些方法不会修改原数组,而是返回新数组,这时候把新数组赋值给原数组,Vue 能检测到数组被替换,进而触发更新,比如过滤已完成的任务:

// 新数组是响应式的,因为赋值时Vue会给新数组做劫持
todoList = todoList.filter(item => !item.done)

数组响应式和对象响应式,核心区别在哪?

虽然都是响应式,但数组和对象的实现逻辑、“踩坑点”类型完全不一样:

劫持方式不同

  • 对象:遍历所有属性,用 Object.defineProperty 给每个属性加 getter/setter,靠属性的 setter 拦截修改。
  • 数组:不劫持索引和长度,而是重写原型上的 7 个方法,靠方法调用时的逻辑触发更新。

”的响应式差异

  • 对象新增属性:obj.newKey = '值',新属性没有 setter,Vue 检测不到,必须用 this.$set(obj, 'newKey', '值')
  • 数组新增索引:arr[5] = '值',同样检测不到,得用 this.$set(arr, 5, '值') 或者 splice

嵌套数据的处理

如果数组里存的是对象(todoList 里每个元素是对象),改对象的属性(todoList[0].done = true)能触发更新——因为对象本身是响应式的,它的属性有 setter,但如果是替换整个对象(todoList[0] = 新对象),这属于改数组索引,必须用 $set 或者变异方法。

实际开发中,处理数组响应式容易踩哪些坑?

总结几个高频“翻车”场景,以及对应的解决方案,帮你避坑:

坑 1:直接修改数组索引

// 错误写法:直接改索引,Vue感知不到,视图不更新
todoList[0] = { text: '改了', done: true }
// 正确做法:用 $set 或 splice
this.$set(todoList, 0, { text: '改了', done: true })
// 或者用 splice 替换
todoList.splice(0, 1, { text: '改了', done: true })

坑 2:直接修改数组长度

// 错误写法:改长度清空数组,视图没变化
todoList.length = 0
// 正确做法:用 splice 清空,或用空数组替换
todoList.splice(0, todoList.length)
// 或者直接赋值空数组
todoList = []

坑 3:忘记“变异方法”的作用范围

比如想给数组开头加元素,用 unshift 是对的;但如果用 concat(返回新数组,不修改原数组),必须赋值才行:

// 错误:concat不修改原数组,直接调用视图不更新
todoList.concat({ text: '新任务' }) 
// 正确:把新数组合并后赋值给原数组
todoList = todoList.concat({ text: '新任务' })

坑 4:嵌套数组/对象的深层更新

如果数组里嵌套数组([ [1,2], [3,4] ]),改内层数组的元素时:

// 先拿到内层数组
innerArr = todoList[0]
// 错误:innerArr是数组,改索引不触发响应式
innerArr[0] = 10 
// 正确:给内层数组用 $set 或变异方法
this.$set(innerArr, 0, 10)
// 或者用内层数组的 splice
innerArr.splice(0, 1, 10)

从数组响应式设计,看Vue2的“权衡智慧”

Vue2 对数组的特殊处理,本质是性能与开发体验的平衡选择

如果像对象一样,给数组每个索引做 defineProperty,当数组很大(1000 个元素)时,遍历、拦截的性能消耗会爆炸,页面大概率卡顿,Vue2 选择“妥协”:放弃对索引和长度的劫持,只重写常用的 7 个变异方法——这样既保证了大部分场景下“调用方法就能更新视图”的开发体验,又避免了性能灾难。

这种设计也给开发者提了个醒:框架不是“万能魔法”,得了解它的规则,就像开车得懂交规,用 Vue2 也得明白数组响应式的“特殊逻辑”,才能少踩坑、效率高。

掌握这3点,数组响应式不踩坑

  1. 改数组优先用“变异方法”:push/pop/splice 等 7 个方法,或者用 $set 处理索引场景。
  2. 替换数组要赋值:filter/map 等返回新数组的方法,必须把新数组赋值给原数组,Vue 才能检测到变化。
  3. 嵌套数据分层处理:数组里的对象/数组,改它们的内部属性时,要确保自身是响应式的(初始化时被 Vue 劫持),必要时用 $set 或变异方法。

理解透这些,再遇到“数组改了视图没更”的问题,就能快速定位原因,再也不用对着代码干着急啦~

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门