技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTVUE

Vue2.0为什么不能检查数组的变化?又该如何解决?

所属馆藏
Vue
文件格式
Markdown
原始路径
Vue/23-Vue2 0为什么不能检查数组的变化?又该如何解决?
本文目录7 个章节
  1. 前言
  2. 分析
  3. 思考:索引赋值真的不能被原生 API 监听吗?
  4. 后续理解
  5. Vue 2 历史解决方案
  6. 当前 Vue 3 说法
  7. 官方参考

Vue2.0为什么不能检查数组的变化?又该如何解决?

前言

Vue 2 的响应式实现存在两个常见边界:

  • 对象初始化后直接新增或删除属性,无法被自动检测。
  • 直接通过索引修改数组、直接修改数组 length,无法触发响应式更新。

Vue 2 已停止维护,下面的写法主要用于维护旧项目;新项目应优先使用 Vue 3。

分析

Vue 2 在初始化时遍历已有属性,通过 Object.defineProperty() 把它们转换为 getter/setter。事后直接增加的对象键并没有对应 setter,因此不能自动通知更新。

数组并非“完全不能检测”。Vue 2 专门包装了七个变异方法:pushpopshiftunshiftsplicesortreverse。这些方法能够通知依赖;真正的边界是直接索引赋值和直接修改 length

思考:索引赋值真的不能被原生 API 监听吗?

下面保留原文的教学实验:遍历数组当前已有索引,用 Object.defineProperty() 安装访问器。

function defineReactive(data, key, value) {
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get() {
      console.log(`get key: ${key} value: ${value}`)
      return value
    },
    set(newValue) {
      console.log(`set key: ${key} value: ${newValue}`)
      value = newValue
    }
  })
}

function observe(data) {
  Object.keys(data).forEach((key) => {
    defineReactive(data, key, data[key])
  })
}

const arr = [1, 2, 3]
observe(arr)
arr[1] = 20

为数组已有索引安装 setter 的实验

实验中 arr[1] 会触发 setter,说明原生 API 能观察当时已经存在且已转换的索引。但它不能证明 Vue 2 的 Observer 就这样处理数组,也不能覆盖新增索引、稀疏数组与 length。Vue 2 实际选择了“观察已有对象属性 + 包装数组变异方法”的实现。

后续理解

原文后续讨论指出数组可能很大,并试图把这一行为单纯归因于每次变更都要遍历带来的性能损耗:

关于 Vue 2 数组响应式实现的后续讨论

更准确的结论是:这是 Vue 2 基于 Object.defineProperty() 的实现边界与设计取舍,不是“数组每次变化都必须重新遍历,因此无法检测”。初始化时已有的对象嵌套属性会被递归观察,数组的七个变异方法也会响应;直接索引、length、对象键新增和删除才需要专门 API。

Vue 2 历史解决方案

数组

  1. 使用 Vue.set / this.$set
this.$set(this.dataArr, 0, { data: '修改第一个元素' })

如果 dataArroriginArr 指向同一个数组,两边都会变化,这是共享引用造成的,不是 $set 的“深度修改”特性。

  1. 使用 splice 完成增、删、改或截断:
this.items.splice(index, 1, value)
this.items.splice(newLength)
  1. 用新数组替换旧数组:
const next = [...this.targetArr]
next[0] = { data: 'test' }
this.targetArr = next

展开运算符只浅拷贝数组容器,元素对象的嵌套引用仍然共享。

对象

使用 Vue.set / $set 新增属性,使用 Vue.delete / $delete 删除属性:

this.$set(this.blog, 'title', 'Vue 2')
this.$delete(this.blog, 'draft')

深度监听用于观察已有嵌套数据,以及通过响应式 API 完成的新增/删除:

this.$watch('blog', this.getCatalog, {
  deep: true,
  immediate: true
})

deep: true 需要遍历嵌套结构,不能把普通的 obj.newKey = value 自动变成响应式。就地修改对象或数组时,回调中的 newValueoldValue 还可能是同一个引用,它不是前后快照。

只监听某个键时可以写:

watch: {
  'obj.name'(currentValue, oldValue) {
    console.log(currentValue, oldValue)
  }
}

当前 Vue 3 说法

Vue 3 的 reactive() 使用 Proxy,能够拦截对象键新增/删除、数组索引和 lengthref() 可以持有原始值;持有对象时会进行深层响应式转换。

import { reactive, ref, watchEffect } from 'vue'

const items = reactive(['a', 'b'])
const selected = ref(0)

watchEffect(() => {
  console.log(items[selected.value], items.length)
})

items[1] = 'Vue 3'
items.length = 1
selected.value = 0

这是可运行的依赖追踪示例;与只打印赋值前后值不同,watchEffect 会在读取的响应式依赖变化后重新执行。

官方参考

作者:小只前端攻城狮

原文链接:https://juejin.cn/post/6970752937047883789

来源:稀土掘金。著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS