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

显示模式

登录
ARCHIVE DOCUMENTVUE

Vue 为什么要用 vm.$set() 解决对象新增属性不能响应的问题?

所属馆藏
Vue
文件格式
Markdown
原始路径
Vue/16-Vue 为什么要用 vm $set() 解决对象新增属性不能响应的问题 ?你能说说如下代码的实现原理
本文目录6 个章节
  1. 问题背景
  2. Vue 2 的 Vue.set / vm.$set
  3. Vue 2.7 set 的核心源码
  4. Vue 3 为什么不需要 $set
  5. 常见边界
  6. 参考

Vue 为什么要用 vm.$set() 解决对象新增属性不能响应的问题?

本地原文只保留了 GitHub 面试题链接。本文保留原问题和 Vue 2 的实现思路,并补充 Vue 3 的对应写法。

问题背景

Vue.set(object, propertyName, value)
vm.$set(object, propertyName, value)

在 Vue 2 中,响应式对象的已有属性会在初始化时被转换成 getter/setter。初始化之后直接给普通对象新增属性,Vue 没有机会为这个新属性安装 getter/setter,因此依赖它的视图不会自动更新:

下面两段 mounted 是对照示例,不能在同一次执行中连续运行;否则直接赋值后属性已经存在,后面的 $set 只会命中“已有属性”分支:

// 反例:Vue 2 不会为这个新属性自动安装响应式 getter/setter
mounted() {
  this.user.age = 18
}
// 正确的 Vue 2 历史写法
mounted() {
  this.$set(this.user, 'age', 18)
}

这里需要区分两个概念:Object.defineProperty() 负责 Vue 2 的响应式依赖收集与变更通知;v-model 等功能才通常被称为“双向绑定”。不能简单说 Object.defineProperty() 本身实现了完整的双向数据绑定。

Vue 2 的 Vue.set / vm.$set

  • Vue.set(target, key, value) 是全局方法。
  • vm.$set(target, key, value) 是实例方法别名。
  • 两者主要用于已经被 Vue 2 Observer 观察的嵌套对象,或者用于通过索引更新数组。
  • 根实例已经创建后,不能依靠 $set 随意增加根级响应式属性;根级属性应在 data 中预先声明。

对象新增属性

this.$set(this.user, 'age', 18)
// 等价于 Vue.set(this.user, 'age', 18)

如果要一次加入多个属性,不要把新属性直接交给 Object.assign() 后期待它们自动响应;可以创建一个新对象替换旧引用:

this.user = Object.assign({}, this.user, {
  age: 18,
  city: 'London'
})

数组索引和 length

Vue 2 不能检测以下两种数组变化:

this.items[index] = value
this.items.length = newLength

可以使用 $setsplice

this.$set(this.items, index, value)
this.items.splice(index, 1, value)
this.items.splice(newLength)

Vue 2 会包装 pushpopshiftunshiftsplicesortreverse 七个变异方法,使这些方法可以通知依赖。

Vue 2.7 set 的核心源码

下面根据 Vue 2.7.16 src/core/observer/index.ts 整理核心分支。它是源码阅读用的简化版,不是完整文件:

export function set(target, key, val) {
  // 数组索引:通过 splice 触发 Vue 2 的数组响应式通知
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }

  // 已有属性:直接赋值即可走已有 setter
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }

  const ob = target.__ob__

  // 非响应式对象:只能普通赋值,Vue 不会为它建立依赖
  if (!ob) {
    target[key] = val
    return val
  }

  // 为新属性安装 getter/setter,并通知依赖该对象的订阅者
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

真实的 Vue 2.7 源码还会处理 Vue 实例、根 $data、开发环境警告、非法数组索引等情况。核心过程可以概括为:

  1. 数组索引使用 splice,绕过直接索引赋值无法被拦截的限制。
  2. 已有属性直接赋值,使用它原本已经存在的 setter。
  3. 对响应式对象的新属性调用 defineReactive,动态安装 getter/setter。
  4. 通过对象的依赖管理器通知更新。

defineReactive 会为属性创建 getter/setter。getter 在读取时收集依赖,setter 在赋值时通知依赖;这就是 $set 能让 Vue 2 视图更新的原因。

Vue 3 为什么不需要 $set

Vue 3 使用 Proxy 代理对象,可以拦截属性新增、修改、删除以及数组索引和 length 等操作:

import { reactive } from 'vue'

const state = reactive({
  user: { name: 'Ada' },
  items: ['a', 'b']
})

state.user.age = 18
state.items[1] = 'Vue'
state.items.length = 1
delete state.user.name

Vue 3 已移除 Vue.setvm.$setVue.deletevm.$delete。使用 reactive() 创建的对象直接赋值即可;如果使用普通对象而没有经过 Vue 的响应式 API,普通赋值当然也不会自动触发 Vue 更新。

常见边界

  • Vue 2 的根级 data 属性要在初始化时声明。
  • Vue.set 只能对可观察的 Vue 2 数据建立响应式;对一个普通未观察对象调用它,最终仍只是普通赋值。
  • Object.assign() 不会把新增键自动变成 Vue 2 响应式属性。
  • Vue 3 的 reactive() 只接收对象类型,原始值通常使用 ref()
  • Vue 2 项目升级到 2.7 并不等于拥有 Vue 3 的 Proxy 响应式;Vue 2.7 仍受 Vue 2 的响应式限制。

参考

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

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

按分类浏览

10 COLLECTIONS