探讨 Vue 2 源码中响应式属性添加/删除的限制,以及 `Vue.set` 和 `Vue.delete` (或 “/“) 的源码实现。

各位靓仔靓女,晚上好!我是老王,今天咱们聊聊 Vue 2 响应式系统里那些“禁区”和“秘籍”。别怕,咱不搞高深理论,就用大白话和实在的代码,把这块儿啃下来。

开场白:响应式系统的“围墙”

Vue 2 的响应式系统是基于 Object.defineProperty 来实现的。这玩意儿很强大,但也有它的局限性。简单来说,它只能劫持对象已有的属性,对于新增或删除的属性,默认情况下它是“视而不见”的。

这就好比,你家装了摄像头监控,但只能监控已有的房间,你突然又盖了个地下室,摄像头就监控不到了。

第一幕:新增属性的“困境”

假设我们有这样一个 Vue 实例:

new Vue({
  data: {
    user: {
      name: '老王',
      age: 30
    }
  },
  template: '<div>{{ user.name }} - {{ user.age }} - {{ user.address }}</div>',
  mounted() {
    // 尝试添加新的属性
    this.user.address = '北京'; // 页面不会更新!
    console.log(this.user.address) // 结果是北京
  }
})

这段代码里,我们在 mounted 钩子函数里尝试给 user 对象添加了一个新的属性 address。看起来好像没什么问题,控制台也能打印出 北京,但是页面上却死活不显示。

这是因为 address 属性是在 Vue 实例初始化之后才添加的,响应式系统没有“劫持”到它,所以它的变化不会触发视图更新。

第二幕:删除属性的“尴尬”

类似地,删除属性也会遇到问题。

new Vue({
  data: {
    user: {
      name: '老王',
      age: 30
    }
  },
  template: '<div>{{ user.name }} - {{ user.age }}</div>',
  mounted() {
    // 尝试删除属性
    delete this.user.age; // 页面不会更新!
    console.log(this.user) // 结果是 {name: '老王'}
  }
})

同样,我们尝试删除了 age 属性,控制台显示 age 确实被删除了,但是页面上的 {{ user.age }} 仍然显示 30(或者显示为空,取决于具体浏览器和 Vue 版本)。

这是因为删除属性也没有触发响应式系统的更新。

第三幕:Vue.setVue.delete 的“救场”

为了解决这些问题,Vue 提供了两个全局 API:Vue.setVue.delete

  • Vue.set(object, key, value) 向响应式对象中添加一个属性,并确保这个新属性也是响应式的,且触发视图更新。
  • Vue.delete(object, key) 从响应式对象中删除一个属性,并确保触发视图更新。

现在,我们用这两个 API 来改造一下上面的代码:

new Vue({
  data: {
    user: {
      name: '老王',
      age: 30
    }
  },
  template: '<div>{{ user.name }} - {{ user.age }} - {{ user.address }}</div>',
  mounted() {
    // 使用 Vue.set 添加属性
    Vue.set(this.user, 'address', '北京'); // 页面会更新!

    // 使用 Vue.delete 删除属性
    Vue.delete(this.user, 'age'); // 页面会更新!
  }
})

这次,一切都如我们所愿了!页面正确地显示了 address 属性,并且 age 属性也成功被删除。

第四幕:$set$delete 的“亲民版”

除了全局 API,Vue 还提供了实例方法 $set$delete,它们的作用和 Vue.setVue.delete 完全一样,只是使用方式略有不同。

new Vue({
  data: {
    user: {
      name: '老王',
      age: 30
    }
  },
  template: '<div>{{ user.name }} - {{ user.age }} - {{ user.address }}</div>',
  mounted() {
    // 使用 $set 添加属性
    this.$set(this.user, 'address', '北京'); // 页面会更新!

    // 使用 $delete 删除属性
    this.$delete(this.user, 'age'); // 页面会更新!
  }
})

源码剖析:Vue.setVue.delete 的“秘密”

现在,让我们深入 Vue 2 的源码,看看 Vue.setVue.delete 到底做了些什么。

Vue.set 的源码(简化版)

/**
 * Set a property on an object. Adds the new property
 * if does not already exist.
 */
export function set (target: Array<any> | Object, key: string | number, val: any): any {
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }
  if (!ob) {
    target[key] = val
    return val
  }
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

这段代码看起来有点长,但其实逻辑很简单:

  1. 处理数组: 如果 target 是一个数组,并且 key 是一个合法的数组索引,那么就使用 splice 方法来添加元素,并触发视图更新。
  2. 属性已存在: 如果 key 已经存在于 target 对象中,并且不是原型链上的属性,那么直接设置属性值即可,因为这个属性已经被响应式系统劫持了。
  3. Vue 实例或其根数据: 如果 target 是一个 Vue 实例或者 Vue 实例的根数据对象,那么会发出警告,建议在 data 选项中提前声明属性。这是为了避免一些潜在的问题。
  4. 非响应式对象: 如果 target 不是一个响应式对象(没有 __ob__ 属性),那么直接设置属性值即可。
  5. 响应式对象: 如果 target 是一个响应式对象,那么:

    • 使用 defineReactive 方法来将新属性转换为响应式属性。
    • 调用 ob.dep.notify() 方法来通知依赖更新,从而触发视图更新。

Vue.delete 的源码(简化版)

/**
 * Delete a property and trigger change if necessary.
 */
export function del (target: Array<any> | Object, key: string | number) {
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1)
    return
  }
  const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid deleting properties on a Vue instance or its root $data ' +
      '- just set it to null.'
    )
    return
  }
  if (!hasOwn(target, key)) {
    return
  }
  delete target[key]
  if (!ob) {
    return
  }
  ob.dep.notify()
}

这段代码的逻辑也类似:

  1. 处理数组: 如果 target 是一个数组,并且 key 是一个合法的数组索引,那么就使用 splice 方法来删除元素,并触发视图更新。
  2. Vue 实例或其根数据: 如果 target 是一个 Vue 实例或者 Vue 实例的根数据对象,那么会发出警告,建议将属性设置为 null 而不是直接删除。
  3. 属性不存在: 如果 key 属性在 target 对象上不存在,那么直接返回。
  4. 删除属性: 使用 delete 操作符删除 target 对象的 key 属性。
  5. 响应式对象: 如果 target 是一个响应式对象,那么:

    • 调用 ob.dep.notify() 方法来通知依赖更新,从而触发视图更新。

总结:Vue.setVue.delete 的“核心”

Vue.setVue.delete 的核心在于:

  • 对于响应式对象,它们会确保新添加或删除的属性也被响应式系统劫持,并且触发视图更新。
  • 对于数组,它们会使用 splice 方法来添加或删除元素,并触发视图更新。
  • 它们会避免直接操作 Vue 实例或其根数据对象,而是建议使用其他方式来处理这些情况。
功能 作用
Vue.set 1. 处理数组: 使用 splice 添加元素并触发更新。
2. 属性已存在: 直接设置属性值(已是响应式)。
3. Vue 实例/根数据: 发出警告,建议提前声明。
4. 非响应式对象: 直接设置属性值。
5. 响应式对象: 使用 defineReactive 将新属性转为响应式,并 notify 依赖。
Vue.delete 1. 处理数组: 使用 splice 删除元素并触发更新。
2. Vue 实例/根数据: 发出警告,建议设置为 null
3. 属性不存在: 直接返回。
4. 删除属性: 使用 delete 操作符。
5. 响应式对象: notify 依赖。
$set/$delete| 实例方法,作用与Vue.setVue.delete` 相同,只是使用方式不同。

扩展思考:为什么 Vue 3 不需要 Vue.setVue.delete 了?

Vue 3 使用了 Proxy 来实现响应式系统,Proxy 可以劫持对象的所有操作,包括属性的添加和删除。因此,在 Vue 3 中,你直接添加或删除属性,响应式系统也能够自动检测到,并触发视图更新,不再需要 Vue.setVue.delete 了。

总结:

今天我们一起学习了 Vue 2 响应式系统里新增和删除属性的限制,以及 Vue.setVue.delete 的用法和源码实现。希望通过这次学习,大家对 Vue 的响应式系统有了更深入的了解。

记住,理解这些“禁区”和“秘籍”,才能更好地驾驭 Vue,写出更健壮、更高效的代码。

好了,今天的分享就到这里,大家晚安!如果还有什么问题,欢迎随时提问。下次有机会,咱们再聊聊 Vue 源码的其他有趣的地方。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注