【Vue】响应式中数组的特殊处理

文章详细解释了Vue中为何需要对数组进行特殊处理,介绍了如何通过重写JavaScript的7个数组方法来实现响应式,并讲解了依赖收集和派发更新的机制。作者还提供了一个完整的Vue响应式原理demo。

Vue 响应式中对数组的处理

前两节的内容:

Vue 数据劫持

Vue 响应式初步

0. 为什么需要对数组特殊处理?

在响应式初步那一篇文章的最后,我们提到过,需要对数组进行特殊的处理,为什么?

如果仍然用我们之前写的 demo 来简单模拟响应式的话,那么对于一个数组 arr,当我们访问这个数组时,同样会触发它身上的 getter 和 setter,但需要注意的是,我们在使用数组时,并不是仅仅有一般的读写操作,更多时候,我们会通过一些常用的数组方法去操作数组,例如:

arr.push(...)
arr.unshift(...)
arr.splice(...)
...

此时我们应该如何做到响应式呢?

Vue 中给出的方法是:对 js 中7个会改变数组的方法进行重写。这七个方法分别是:push, pop, unshift, shift, splice, reverse, sort

接下来,在我们的 demo 中简单地实现一下。

首先,需要对之前的 Observer 类进行一些修改,加入对数组类型的处理:

class Observer {
  constructor(value) {
    this.value = value
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      // 如果是数组类型数据的话就特殊处理
      // 代理原型
      ...
      // 监听数组内容
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
  
  observeArray(arr) {
    // 对数组内部的对象类型数据进行监听
    arr.forEach((i) => observe(i))
  }
  ...
}

接下来,就是对数组方法进行监听。

1. 代理原型

基于对原型链的理解,我们知道,当调用 arr 身上的某一方法如 push 时,实际上是顺着原型链找到了 Array.prototype 然后调用了它身上的 push 方法。

那么,如果我们想要在调用 push 时,对其进行拦截,让其执行我们自己定义的方法,一般我们想到的都是重写该方法,但实际上还有另一种方法 — 代理原型。

所谓代理原型,实际上就是在数组对象和其原型 Array.prototype 之间做一层代理,当通过数组对象调用某些特定的方法时,就会触发我们的代理,在不影响原方法执行的情况下,实现响应式。

如下图:

在这里插入图片描述

接下来就是实现了:

首先是 Observer 类中

// 定义两个全局变量
const arrayPrototype = Array.prototype	// 保存数组的原型
// 增加代理原型 proxyPrototype 且 proxyPrototype.__proto__ === arrayProrotype
const proxyPrototype = Object.create(arrayPrototype)

// Observer 类
class Observer {
  constructor(value) {
    this.value = value
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      // 如果是数组类型数据的话就特殊处理
      // 代理原型
      Object.setPrototypeOf(value, proxyPrototype)
      // 监听数组内容
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
  
  observeArray(arr) {
    // 对数组内部的对象类型数据进行监听
    arr.forEach((i) => observe(i))
  }
  ...
}

接下来对上面的7个方法进行代理:

// 在 array.js 中编写
const reactiveMethods = [
    'push',
    'pop',
    'unshift',
    'shift',
    'splice',
    'reverse',
    'sort',
]

reactiveMethods.forEach((method) => {
    // 取出原方法
    const originalMethod = arrayPrototype[method]
    // 在我们的代理原型上定义该方法的响应式版本
    Object.defineProperty(proxyPrototype, method, {
        value: function reactiveMethod(...args) {
            // 首先确保调用不受影响
            const result = originalMethod.apply(this, args)
            // 派发更新
            ...
            return result
        },
        enumerable: false,
    	writable: true,
    	configurable: true
    })
})

现在遇到了一个问题:如何派发更新?

2. 派发更新的实现

在对象类型数据的处理中,我们是首先在 defineReactive 方法中形成一个 dep 实例的闭包,然后在 setter 中通过 dep.notify() 依次通知相关的 watcher 实例来实现派发更新。这样保证了每一个响应式数据都有其自己的 dep 实例。

而这里,数组中的各项数据的确是拥有其自己的 dep 实例的,但是我们想要的是为数组对象本身准备一个 dep 实例,那么我们应该在哪里定义这一 dep 实例呢?

在前面数据劫持的学习实现中,为了防止对某一数据进行重复劫持,我们在每一个被劫持过的数据身上,都添加了一个属性 __ob__ ,并将该数据对应的 Observer 类实例存入了该属性中。

那么,同理,数组对象身上应该也存在这一属性:

在这里插入图片描述

由于该属性指向当前数据对应的 Observer 类实例,且两者是一一对应的,所以,此时我们只需要在 Observer 实例身上定义一个 dep 实例,就能够维持我们之前的特性:每一个响应式数据都有其自己的 dep 实例

那么就需要对 Observer 类进行一些修改:

// Observer 类
class Observer {
  constructor(value) {
    this.value = value
    // 声明该数据对应的 dep 实例
    this.dep = new Dep()
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      // 如果是数组类型数据的话就特殊处理
      // 代理原型
      Object.setPrototypeOf(value, proxyPrototype)
      // 监听数组内容
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
  
  observeArray(arr) {
    // 对数组内部的对象类型数据进行监听
    arr.forEach((i) => observe(i))
  }
  ...
}

接下来,就可以对我们前面的代码进行补全。同时还有一个小细节需要注意:当使用 push、unshift、splice 这三个方法操作数组时,可能会向数组中增加元素,那么这些增加的元素也需要被劫持一下。

// 在 array.js 中编写
const reactiveMethods = [
    'push',
    'pop',
    'unshift',
    'shift',
    'splice',
    'reverse',
    'sort',
]

reactiveMethods.forEach((method) => {
    // 取出原方法
    const originalMethod = arrayPrototype[method]
    // 在我们的代理原型上定义该方法的响应式版本
    Object.defineProperty(proxyPrototype, method, {
        value: function reactiveMethod(...args) {
            // 首先确保调用不受影响
            const result = originalMethod.apply(this, args)
            // 获取数组对象的 Observer 类实例
            const ob = this.__ob__
            // 对三种方法特殊处理
            let appended = null
            switch (method) {
                case 'push':
                case 'unshift':
                    appended = args
                    break;
                case 'splice':
                    // splice 方法中,第三个以及以后的参数是新增的数据
                    appended = args.slice(2)
            }
            // 如果有新增的数据,则对这些新增的数据进行劫持
            if (appended) ob.observerArray(appended)
            // 通过dep实例派发更新
            ob.dep.notify()
            return result
        },
        enumerable: false,
    	writable: true,
    	configurable: true
    })
})

完成了派发更新的逻辑,接下来还需要解决依赖收集的问题。

3. 依赖收集的实现

对于下面的数据:

const obj = {
	a: 1,
	arr: [
        {
            b: 2,
            c: 3,
        },
        {
            d: 4
        }
    ]
}

由于我们前面为被劫持的数据都添加了 __ob__ 属性,所以,被劫持后的数据实际上会变成下面这种形式:

const obj = {
	a: 1,
	arr: [
        {
            b: 2,
            c: 3,
            __ob__: {...}	// 数组中对象数据的Observer实例
        },
        {
            d: 4,
            __ob__: {...}	// 数组中对象数据的Observer实例
        },
        __ob__: {...}	// 数组对象arr的Observer实例(实际上的数组对象结构并不是这样的,这里只是简化的写法)
    ],
    __ob__: {...}	// obj对象的Observer实例
}

在前面的数据劫持时,我们在 observer 方法的最后,将新创建或已有的 Observer 类实例返回了出来,并在 defineReactive 方法中,用变量 childOb 接收到了该实例:

// 数据劫持
function defineReactive(data, key, value = data[key]) {
  const dep = new Dep()
  // 对当前属性的下一层属性进行劫持,并拿到当前数据对应的Observer实例
  let childOb = observe(val)
  // 对当前属性进行拦截
  Object.defineProperty(data, key, {
    get: function reactiveGetter() {
      // 收集依赖
      dep.depend()
      return value
    },
    set: function reactiveSetter(newValue) {
      if (newValue === value) return
      value = newValue
      // 触发依赖,并更新Observer实例
      childOb = observe(newValue)
      dep.notify()
    }
  })
}

即,当前的闭包中,我们不仅可以通过变量 dep 拿到该数据对应的 dep 实例,还可以通过 childOb.dep 拿到 dep 实例。

在针对对象类型数据的处理中,我们是通过变量 dep 指向的 dep 实例来进行依赖收集以及派发更新,但是这里对于数组类型的数据,我们是通过 __ob__.dep 或者说 childOb.dep 来进行派发更新。即对于对象类型和数组类型的数据,其会存在两个 dep 实例,一个是在 defineReactive 方法的闭包中,一个则在其对应的 Observer 实例对象身上!

因此,只要能够保证 __ob__.dep 与当前闭包中的变量 dep 这两个 dep 实例中保存的 watcher 相同,就能保证依赖收集以及派发更新不会出现问题。

所以,我们需要对原本的 getter 进行修改:

get: function reactiveGetter() {
    // 同时向两个dep实例中收集依赖
    // 由于此时的 Dep.target 变量指向某一watcher,所以只需要每次收集依赖时,都同时向两个dep实例中收集依赖,就能保证两个dep实例中保存的watcher相同
    dep.depend()
    childOb.dep.depend()
    return value
},

但是,我们还需要考虑一种特殊情况:在 observer 方法中,对于普通类型的数据,我们不会进行处理,即,普通类型的数据身上并不会有 __ob__ 属性!也就是说,普通类型数据的 childOb 可能为空,但是,在 defineReactive 方法的闭包中,变量 dep 仍然存在,且能够收集到该数据的依赖,所以此时我们仅需要向变量 dep 指向的 dep 实例中收集依赖就行了:

get: function reactiveGetter() {
    // 收集依赖
    dep.depend()
    if (childOb) {
      childOb.dep.depend() 
    }
    return value
},

这样就完成了依赖的收集。

4. 注意

考虑下面的情况:

当我们仅仅改变数组中一个对象的某一属性的值时,是否会触发更新?

const arr = [
    {
        a: 1
    }
]
// 劫持该数据
observer(arr)
// 劫持后的数据变为如下形式
const arr = [
    {
        a: 1,
        __ob__: {...}
    },
    __ob__: {...}
]

当某一watcher依赖于该数组时,会执行以下流程:

在 watcher 的构造函数中,会访问该数组,触发其 getter,然后在 getter 中触发依赖收集,从而使得 watcher 被收集到数组 arr__ob__.dep 的依赖数组中,但此时需要注意,arr[0] 这一数据的 __ob__.dep 中并没有收集到这个 watcher。显然,我们的实现对于这种情况是不会触发派发更新的。

但是,在 Vue 的源码中认为,只要依赖了该数组,就等价于依赖了数组中的所有元素,即只要数组中的任意元素更新了,依赖该数组的地方也需要更新,这实际上是合理的。

所以,我们需要在收集依赖时做出一些修改:

function defineReactive(data, key, value = data[key]) {
  const dep = new Dep()
  // 对当前属性的下一层属性进行劫持,并拿到当前数据对应的Observer实例
  let childOb = observe(val)
  // 对当前属性进行拦截
  Object.defineProperty(data, key, {
    get: function reactiveGetter() {
        // 收集依赖
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          // 新增
          if (Array.isArray(val)) {
              dependArray(val)
          }
        }
        return value
    },
    set: function reactiveSetter(newValue) {
      if (newValue === value) return
      value = newValue
      // 触发依赖,并更新Observer实例
      childOb = observe(newValue)
      dep.notify()
    }
  })
}

function dependArray(array) {
  for (let e of array) {
    e && e.__ob__ && e.__ob__.dep.depend()
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}

5. 完整demo

这已经是第三版的demo了:
首先是两个全局变量

// public.js
// 定义两个全局变量
const arrayPrototype = Array.prototype	// 保存数组的原型
// 增加代理原型 proxyPrototype 且 proxyPrototype.__proto__ === arrayProrotype
const proxyPrototype = Object.create(arrayPrototype)
// demo.js
// observer 方法
function observer (value) {
    if (!isObject(value)) {
        return
    }
    var ob;
    if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
        ob = value.__ob__;
    } else {
        ob = new Observer(value);
    }
    return ob
}

// Observer 类
export class Observer {
  constructor(value) {
    this.value = value
    // 声明该数据对应的 dep 实例
    this.dep = new Dep()
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      // 如果是数组类型数据的话就特殊处理
      // 代理原型
      Object.setPrototypeOf(value, proxyPrototype)
      // 监听数组内容
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
  // 遍历下一层属性,执行defineReactive
  walk (obj) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
  observeArray(arr) {
    // 对数组内部的对象类型数据进行监听
    arr.forEach((i) => observe(i))
  }
}

// def 方法,用于为当前正在拦截的数据添加 __ob__ 属性
function def (obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  });
}

// 数据劫持
function defineReactive(data, key, value = data[key]) {
  const dep = new Dep()
  // 对当前属性的下一层属性进行劫持,并拿到当前数据对应的Observer实例
  let childOb = observe(value)
  // 对当前属性进行拦截
  Object.defineProperty(data, key, {
    get: function reactiveGetter() {
        // 收集依赖
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          // 新增
          if (Array.isArray(value)) {
              dependArray(value)
          }
        }
        return value
    },
    set: function reactiveSetter(newValue) {
      if (newValue === value) return
      value = newValue
      // 触发依赖,并更新Observer实例
      childOb = observe(newValue)
      dep.notify()
    }
  })
}

function dependArray(array) {
  for (let e of array) {
    e && e.__ob__ && e.__ob__.dep.depend()
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}

// Dep 类
class Dep {
  constructor() {
    this.subs = []
  }
  // 依赖收集
  depend() {
    if (Dep.target) {
      this.addSub(Dep.target)
    }
  }
  // 通知更新
  notify() {
    const subs = [...this.subs]
    subs.forEach((s) => s.update())
  }
  // 添加订阅
  addSub(sub) {
    this.subs.push(sub)
  }
}

// 全局变量 Dep.target
Dep.target = null

// 用于暂存 Dep.target 指向的栈
const targetStack = []

// 入栈
function pushTarget (_target) {
    targetStack.push(Dep.target)	// 保存当前 Dep.target
    Dep.target = _target
}
        
// 出栈
function popTarget () {
    Dep.target = targetStack.pop()
}

// Watcher 类
class Watcher {
  constructor(data, expression, cb) {
      this.data = data;	    // 要实现响应式的对象
      this.expression = expression;	// 依赖属性的访问路径
      this.cb = cb;	    // 依赖的回调
      this.value = this.get() // 访问目标属性以触发getter从而发起依赖收集流程
  }
  // 访问当前实例依赖的属性,并将全局变量指向自身
  get() {
    pushTarget(this)
    const value = parsePath(this.data, this.expression)
    popTarget()
    return value
  }
  // 收到更新通知后,进行更新,并触发依赖回调
  update() {
    const oldValue = this.value
    this.value = parsePath(this.data, this.expression)
    this.cb.call(this.data, this.value, oldValue)
  }
}

// 工具函数,用于根据指定访问路径,取出某一对象下的指定属性
function parsePath(obj, expression) {
  const segments = expression.split('.')
  for (let key of segments) {
    if (!obj) return
    obj = obj[key]
  }
  return obj
}
// demo.js
// observer 方法
function observer (value) {
    if (!isObject(value)) {
        return
    }
    var ob;
    if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
        ob = value.__ob__;
    } else {
        ob = new Observer(value);
    }
    return ob
}

// Observer 类
export class Observer {
  constructor(value) {
    this.value = value
    // 声明该数据对应的 dep 实例
    this.dep = new Dep()
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      // 如果是数组类型数据的话就特殊处理
      // 代理原型
      Object.setPrototypeOf(value, proxyPrototype)
      // 监听数组内容
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
  // 遍历下一层属性,执行defineReactive
  walk (obj) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
  observeArray(arr) {
    // 对数组内部的对象类型数据进行监听
    arr.forEach((i) => observe(i))
  }
}

// def 方法,用于为当前正在拦截的数据添加 __ob__ 属性
function def (obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  });
}

// 数据劫持
function defineReactive(data, key, value = data[key]) {
  const dep = new Dep()
  // 对当前属性的下一层属性进行劫持,并拿到当前数据对应的Observer实例
  let childOb = observe(value)
  // 对当前属性进行拦截
  Object.defineProperty(data, key, {
    get: function reactiveGetter() {
        // 收集依赖
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          // 新增
          if (Array.isArray(value)) {
              dependArray(value)
          }
        }
        return value
    },
    set: function reactiveSetter(newValue) {
      if (newValue === value) return
      value = newValue
      // 触发依赖,并更新Observer实例
      childOb = observe(newValue)
      dep.notify()
    }
  })
}

function dependArray(array) {
  for (let e of array) {
    e && e.__ob__ && e.__ob__.dep.depend()
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}

// Dep 类
class Dep {
  constructor() {
    this.subs = []
  }
  // 依赖收集
  depend() {
    if (Dep.target) {
      this.addSub(Dep.target)
    }
  }
  // 通知更新
  notify() {
    const subs = [...this.subs]
    subs.forEach((s) => s.update())
  }
  // 添加订阅
  addSub(sub) {
    this.subs.push(sub)
  }
}

// 全局变量 Dep.target
Dep.target = null

// 用于暂存 Dep.target 指向的栈
const targetStack = []

// 入栈
function pushTarget (_target) {
    targetStack.push(Dep.target)	// 保存当前 Dep.target
    Dep.target = _target
}
        
// 出栈
function popTarget () {
    Dep.target = targetStack.pop()
}

// Watcher 类
class Watcher {
  constructor(data, expression, cb) {
      this.data = data;	    // 要实现响应式的对象
      this.expression = expression;	// 依赖属性的访问路径
      this.cb = cb;	    // 依赖的回调
      this.value = this.get() // 访问目标属性以触发getter从而发起依赖收集流程
  }
  // 访问当前实例依赖的属性,并将全局变量指向自身
  get() {
    pushTarget(this)
    const value = parsePath(this.data, this.expression)
    popTarget()
    return value
  }
  // 收到更新通知后,进行更新,并触发依赖回调
  update() {
    const oldValue = this.value
    this.value = parsePath(this.data, this.expression)
    this.cb.call(this.data, this.value, oldValue)
  }
}

// 工具函数,用于根据指定访问路径,取出某一对象下的指定属性
function parsePath(obj, expression) {
  const segments = expression.split('.')
  for (let key of segments) {
    if (!obj) return
    obj = obj[key]
  }
  return obj
}
// array.js
const reactiveMethods = [
    'push',
    'pop',
    'unshift',
    'shift',
    'splice',
    'reverse',
    'sort',
]

// 代理原型
reactiveMethods.forEach((method) => {
    // 取出原方法
    const originalMethod = arrayPrototype[method]
    // 在我们的代理原型上定义该方法的响应式版本
    Object.defineProperty(proxyPrototype, method, {
        value: function reactiveMethod(...args) {
            // 首先确保调用不受影响
            const result = originalMethod.apply(this, args)
            // 获取数组对象的 Observer 类实例
            const ob = this.__ob__
            // 对三种方法特殊处理
            let appended = null
            switch (method) {
                case 'push':
                case 'unshift':
                    appended = args
                    break;
                case 'splice':
                    // splice 方法中,第三个以及以后的参数是新增的数据
                    appended = args.slice(2)
            }
            // 如果有新增的数据,则对这些新增的数据进行劫持
            if (appended) ob.observerArray(appended)
            // 通过dep实例派发更新
            ob.dep.notify()
            return result
        },
        enumerable: false,
    	writable: true,
    	configurable: true
    })
})

github 仓库地址:Vue 响应式原理demo

<think> 我们被要求回答:在Vue2中如何响应式地添加数组对象。 根据提供的引用,我们可以总结如下: 1. Vue2使用Object.defineProperty来实现响应式,但它无法检测到对象属性的添加或删除,以及数组索引的直接设置和长度的修改(引用[1]、[4])。 2. 对于数组Vue2包装了数组的变异方法(如push, pop, shift, unshift, splice, sort, reverse),这些方法可以触发视图更新(引用[3])。 3. 但是,当我们直接通过索引设置数组项(如arr[index]=newValue)或者修改数组长度(如arr.length=newLength)时,不会触发响应式更新。 4. 为了响应式地添加数组对象(即在数组中添加一个新对象),我们可以使用以下方法: a. 使用Vue包装过的数组变异方法,比如push(向末尾添加)、unshift(向开头添加)、splice(在指定位置添加)等(引用[3])。 b. 如果我们需要在数组的中间某个位置添加一个对象,可以使用splice方法。 c. 如果我们想要给数组中的某个对象添加一个新的属性,并且希望这个属性是响应式的,则需要使用Vue.set或this.$set(引用[2]、[4])。 但注意问题:我们是要添加一个数组元素(即一个新的对象)到数组中,而不是给已有的数组元素添加属性。因此,这里主要使用数组的变异方法。 具体方法: 1. 使用push方法:在数组末尾添加一个或多个对象。 this.arr.push(newObject); 2. 使用unshift方法:在数组开头添加一个或多个对象。 this.arr.unshift(newObject); 3. 使用splice方法:在指定位置添加一个或多个对象。 // 在索引为index的位置,删除0个元素,然后插入newObject this.arr.splice(index, 0, newObject); 4. 如果我们直接通过索引设置(比如在某个位置替换一个对象)并希望触发响应式,则不能直接使用arr[index]=newObject,而应该使用splice方法或者Vue.set(即$set)方法(引用[1]、[2]、[4])。 使用$set设置数组的某个索引: this.$set(this.arr, index, newObject); // 这会替换index位置的对象 注意:$set也可以用于数组,第一个参数是数组,第二个参数是索引,第三个参数是新值。 但是,对于添加一个新的对象到数组中(即在数组的末尾添加一个对象),我们通常使用push,因为这样最简单。 总结:添加新的对象到数组中,推荐使用数组的变异方法(push, unshift, splice)或使用$set(当需要设置特定索引时,但注意$set通常用于替换,添加新索引的位置如果超出数组长度,则不会自动填充中间的空位,所以一般用于替换已有索引或添加索引在已有长度范围内,超出范围通常不用,而是用splice或push)。 因此,对于在数组末尾添加一个对象,使用push;在开头使用unshift;在中间使用splice。如果我们要替换数组中某个索引的对象,则可以使用$set或者splice。 示例代码: 假设我们有一个数组items,现在要在末尾添加一个对象{name: 'new item'}: ```javascript this.items.push({name: 'new item'}); ``` 或者,在索引为1的位置插入一个对象: ```javascript this.items.splice(1, 0, {name: 'new item'}); ``` 或者,如果我们想要设置索引为1的位置为一个新对象(替换): ```javascript this.$set(this.items, 1, {name: 'new item'}); ``` 注意:直接通过索引设置(如this.items[1] = {name: 'new item'})不会触发响应式更新。 另外,引用[5]提到,当从后端获取数据后,有时需要动态添加响应式数据,可以使用this.$set()。但这里针对的是数组中的对象属性?实际上,引用[5]的情况是在已有数组中的对象上添加新属性,而不是添加整个对象到数组。所以,添加整个对象到数组,使用上述的数组变异方法即可。 因此,回答重点:添加数组对象(即添加一个元素到数组)使用push、unshift、splice或$set(用于替换或设置特定索引)。 根据问题,用户想知道如何响应式地添加数组对象,我们可以这样回答: 在Vue2中,要响应式地向数组添加一个对象(即数组元素),可以使用Vue重写的数组变异方法,如push、unshift、splice。这些方法能够触发视图更新。此外,也可以使用Vue.set(在组件内使用this.$set)来设置数组的某个索引,但注意$set主要用于设置已有索引的值或添加一个新索引(在数组长度范围内,如果索引大于当前长度,数组会被填充到该索引,中间用undefined填充,但这样不常用)。通常,我们使用数组变异方法来添加元素。 具体方法: 1.数组末尾添加:使用push() 2.数组开头添加:使用unshift() 3.数组中间指定位置添加:使用splice(index, 0, ...items) 4. 替换数组中的某个元素:可以使用splice(index, 1, newItem)或this.$set(array, index, newItem) 注意:避免直接通过索引设置数组元素(如array[index]=newValue),因为这样不会触发响应式更新。 示例: ```javascript // 在Vue组件中 methods: { addItemToEnd() { // 在数组末尾添加一个对象 this.items.push({ id: 4, name: 'New Item' }); }, addItemToStart() { this.items.unshift({ id: 0, name: 'First Item' }); }, insertItemAtIndex() { // 在索引1的位置插入一个对象 this.items.splice(1, 0, { id: 2.5, name: 'Middle Item' }); }, replaceItemAtIndex() { // 替换索引1的对象 this.$set(this.items, 1, { id: 2, name: 'Replaced Item' }); } } ``` 另外,如果需要在数组中的某个对象上添加新的响应式属性,则必须使用this.$set(),例如: ```javascript this.$set(this.items[0], 'newProperty', 'value'); ``` 但这个问题是关于添加数组对象(即整个对象作为元素)的,所以我们主要关注数组变异方法。 因此,回答的核心在于使用数组的变异方法(push, unshift, splice)来添加对象到数组。 生成相关问题: 1. Vue2中为什么直接通过索引设置数组项不能触发响应式更新? 2. 除了push和unshift,还有哪些数组方法在Vue2中是响应式的? 3.Vue2中,如何响应式地删除数组中的对象? 4.Vue2中,如何给数组中的一个对象添加新的响应式属性? </think> 在 Vue2 中响应式添加数组对象的方法主要有以下几种,核心在于绕过 `Object.defineProperty` 的限制: ### ✅ 推荐方法 1. **使用变异方法 (直接修改原数组)** Vue 重写了以下原生数组方法,调用时会自动触发视图更新: ```javascript // 添加单个对象到末尾 this.arr.push({ id: 4, name: "新对象" }); // 添加多个对象到开头 this.arr.unshift({ id: 0 }, { id: 1 }); // 在索引 2 处插入对象 this.arr.splice(2, 0, { id: 2.5 }); ``` 2. **`Vue.set()` / `this.$set()` (动态添加响应式属性)** 当需要向数组中的**已有对象**添加新属性时使用: ```javascript // 向数组索引 1 的对象添加响应式属性 this.$set(this.arr, 1, { ...this.arr[1], newProp: "动态添加的值" // 新增响应式属性 }); ``` --- ### ⚠ 注意事项 - **直接赋值无效** ```javascript this.arr[3] = { id: 4 } // ❌ 不会触发更新 this.arr.length = 10 // ❌ 不会触发更新 ``` - **对象属性需特殊处理** 若需给数组中的对象添加**新属性**,必须用 `$set`: ```javascript // 错误方式(非响应式) this.arr[0].newKey = "value"; // 正确方式 this.$set(this.arr[0], "newKey", "value"); ``` --- ### 💡 解决原理 Vue2 通过 `Object.defineProperty` 实现响应式,但该 API 有两大限制: 1. 无法检测**数组索引的直接修改**(如 `arr[index]=x`)[^1] 2. 无法检测**对象属性的新增/删除**(需用 `Vue.set`/`Vue.delete`)[^4] 因此 Vue 重写了 7 个数组方法(`push`, `pop`, `shift`, `unshift`, `splice`, `sort`, `reverse`)来实现响应式更新[^3]。 --- ### 📝 完整示例 ```javascript export default { data() { return { items: [ { id: 1, name: "A" }, { id: 2, name: "B" } ] }; }, methods: { addItem() { // 方法1:使用变异方法 this.items.push({ id: 3, name: "C" }); // 方法2:splice 添加 this.items.splice(1, 0, { id: 1.5, name: "B+" }); // 方法3:动态扩展对象属性 this.$set(this.items[0], "status", "active"); } } }; ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值