Skip to content

10|Vue 2 响应式:维护补丁与迁移边界 ​

需要从原实例开始排错时,先读Vue 2 响应式限制与异步观察,完成新增字段、数组和过期请求的实验。本篇进一步比较迁移边界;2.7 回移能力见版本与工具链。

第三章:响应式系统纵览 / 前置:Proxy 与 Reflect / 下一篇:调试响应式

本篇只服务于 Vue 2 存量代码和迁移。Vue 3 新代码不应继续引入 Vue.set、this.$set 或 Vue.delete;这些 API 是旧观察模型的补丁,不是跨版本最佳实践。

先确认运行版本 ​

环境响应式基础本篇补丁是否需要
Vue 2.6Object.defineProperty;Composition API 需插件需要
Vue 2.7仍是 getter/setter;内置部分 Composition API仍有同类检测边界
Vue 3Proxy 对象 + ref getter/setter不需要,也不应照搬

Vue 2 已结束维护。若无法立即迁移,至少把版本、浏览器范围和补丁调用集中记录;不要让旧兼容写法继续扩散。

为什么新增字段没有 setter ​

Vue 2 在实例初始化时遍历 data 的现有属性,用 Object.defineProperty 安装 getter/setter。之后直接增加的新 key 没有经过这一步:

js
new Vue({
  data: {
    profile: { name: 'Ada' },
    rootMessage: ''
  }
})

// 以下放在实例方法内,选择一条运行;每次恢复初始profile再做另一条。
this.profile.email = 'ada@example.com' // 新增嵌套key不可检测
// 或在email尚不存在的初始对象上:
this.$set(this.profile, 'email', 'ada@example.com') // 创建可检测字段

不能先直接添加email,再以为 $set 同名字段能自动补装setter:已存在的普通字段可能只被赋值。对已错误新增的字段应重建整个对象,或在确定可删除的前提下移除后再 $set。

新增根实例响应式属性不受支持,应在 data 中提前声明。批量加入嵌套字段时,Vue 2 官方建议替换对象:

js
this.profile = Object.assign({}, this.profile, {
  email: 'ada@example.com',
  active: true
})

直接 Object.assign(this.profile, fields) 仍是在旧对象上添加未观察的 key。

数组的两个检测缺口 ​

Vue 2 无法检测直接索引赋值与直接改 length:

js
this.items[index] = next          // 不可检测
this.items.length = newLength     // 不可检测

this.$set(this.items, index, next) // 可检测
this.items.splice(index, 1, next)  // 可检测
this.items.splice(newLength)       // 可检测地截断

push、pop、shift、unshift、splice、sort、reverse 等变更方法会被 Vue 2 包装并发出通知。Map/Set 也没有 Vue 3 那套集合 instrumentation;不要期望在 Vue 2 模板里直接把它们当响应式 source。

source → derived → effect 在 Vue 2 中哪里断 ​

text
初始化已有 data.key
  └─ getter 被渲染读取 → watcher 订阅
       └─ setter 写入 → watcher 入队 → DOM 更新

初始化后直接新增 data.newKey
  └─ 没有 getter,也没有 setter
       ├─ 渲染无法为该 setter 建边
       └─ 后续写入没有通知入口

Vue.set(target, key, value) 的职责是创建可观察属性并通知相关依赖。Vue.delete 则在删除后通知。它们解决的是拦截入口缺失,不是 flush 或 deep watch 问题。

迁移时怎样删掉补丁 ​

Vue 2 代码Vue 3 迁移注意
this.$set(obj, key, value)obj[key] = valueobj 必须仍是 reactive proxy
this.$delete(obj, key)delete obj[key]写入不能绕到 raw 对象
this.$set(items, i, value)items[i] = value直接索引在 Vue 3 可拦截
items.splice(n) 仅为响应式截断items.length = n 或保留 splice根据可读性与返回值需求选择

迁移前先给 Vue 2 行为写回归测试,特别是新增、删除、索引替换和异步 DOM 更新。迁移后删除 adapter,并证明 Vue 3 代理路径触发相同用户可见结果。不要同时保留两套写法“以防万一”,否则测试无法证明究竟哪条路径生效。

受控实验:defineProperty 的可见边界 ​

下面不是 Vue 2 的完整实现,只用于证明“初始化时存在的 key 才有 setter”:

js
import assert from 'node:assert/strict'

const events = []
const state = { known: 1 }
let known = state.known

Object.defineProperty(state, 'known', {
  enumerable: true,
  configurable: true,
  get: () => known,
  set: (value) => {
    known = value
    events.push('known')
  }
})

state.known = 2
state.added = 3
assert.deepEqual(events, ['known'])

它只验证语言机制,不替代在真实 Vue 2.6/2.7 应用中的组件回归测试。数组变更方法包装、依赖通知和异步队列都属于 Vue 2 运行时的额外工作。

排错顺序 ​

  1. 锁定实际 Vue 版本,不根据 Options API 写法猜版本。
  2. 检查字段是否在实例初始化时存在;根字段必须预声明。
  3. 检查是否直接设置数组索引或 length。
  4. 检查是否误把 Map/Set 当作 Vue 3 响应式集合。
  5. 补丁后 await this.$nextTick() 再验证 DOM;检测缺口和异步队列是两件事。
  6. 记录补丁的删除条件:升级到 Vue 3、回归测试通过、调用方都经 proxy 写入。

练习:把 Vue 2 补丁包进可删除边界 ​

为存量代码实现两个纯函数 adapter:

  • setField(setter, target, key, value) 只能通过注入的 setter(target, key, value) 写入,便于 Vue 2 传入 Vue.set。
  • replaceAt(items, index, value) 使用 splice,越界时抛出 RangeError,且不得改变失败输入。
  • 用严格断言覆盖已有字段、新增字段、合法索引和越界失败。
  • 再写出 Vue 3 迁移后的直接赋值版本,并列出可以删除的 adapter 与测试仍需保留的行为合同。

查看第 10 题参考答案。

版本依据可对照 Vue 2 的“Reactivity in Depth”与 Vue 3 Migration Guide;迁移时以项目锁定的小版本文档为准。

用心记录每一步成长