1.Vue 响应式原理 - 标准答案

考察点:底层原理理解、技术对比、边界场景、实战经验

问题: Vue2 使用 Object.defineProperty 而 Vue3 改用 Proxy 来实现响应式系统。请从以下几个方面深入对比:

  1. 技术实现层面:两者在拦截数据变化时的核心差异是什么?
  2. 边界场景问题:Vue2 响应式系统有哪些已知的局限性?Vue3 的 Proxy 如何解决这些问题?
  3. 性能考量:在大型应用中,这两种方案的性能表现有什么差异?
  4. 实战经验:您在项目中是否遇到过 Vue2 响应式的坑?如何解决的?

Vue2 - Object.defineProperty

// Vue2 响应式实现原理
function defineReactive(obj, key, val) {
  const dep = new Dep() // 依赖收集器
  
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      // 依赖收集
      if (Dep.target) {
        dep.depend()
      }
      return val
    },
    set(newVal) {
      if (newVal === val) return
      val = newVal
      // 通知更新
      dep.notify()
    }
  })
}

// 递归遍历对象的所有属性
function observe(obj) {
  if (typeof obj !== 'object' || obj === null) return
  
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key])
    // 递归处理嵌套对象
    if (typeof obj[key] === 'object') {
      observe(obj[key])
    }
  })
}

特点

  • ✅ 可以精确拦截对象属性的读写
  • ❌ 只能拦截已存在的属性
  • ❌ 必须遍历对象的每个属性
  • ❌ 无法监听属性的新增/删除
  • ❌ 无法监听数组索引和 length

Vue3 - Proxy

// Vue3 响应式实现原理
function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      const result = Reflect.get(target, key, receiver)
      
      // 依赖收集
      track(target, key)
      
      // 惰性代理:只在访问时才代理嵌套对象
      if (typeof result === 'object' && result !== null) {
        return reactive(result)
      }
      
      return result
    },
    
    set(target, key, value, receiver) {
      const oldValue = target[key]
      const result = Reflect.set(target, key, value, receiver)
      
      // 触发更新
      if (oldValue !== value) {
        trigger(target, key)
      }
      
      return result
    },
    
    deleteProperty(target, key) {
      const hadKey = Object.prototype.hasOwnProperty.call(target, key)
      const result = Reflect.deleteProperty(target, key)
      
      if (hadKey && result) {
        trigger(target, key)
      }
      
      return result
    }
  })
}

特点

  • ✅ 可以拦截对象本身的 13 种操作(get、set、deleteProperty、has、ownKeys等)
  • ✅ 可以监听属性的新增/删除
  • ✅ 可以监听数组索引和 length
  • 惰性代理:只在访问嵌套对象时才代理
  • ✅ 使用 Reflect 确保正确的 this 绑定

核心差异总结

特性Object.definePropertyProxy
拦截层级属性级别对象级别
新增属性❌ 无法监听✅ 自动监听
删除属性❌ 无法监听✅ 自动监听
数组索引❌ 无法监听✅ 自动监听
初始化必须递归遍历惰性代理
性能初始化慢访问时略慢

Vue2 的局限性

问题1:无法监听属性新增

// Vue2 - 无法响应
const vm = new Vue({
  data: {
    user: {
      name: 'Alice'
    }
  }
})

// ❌ 直接添加属性,视图不会更新
vm.user.age = 18

// ✅ 必须使用 $set
vm.$set(vm.user, 'age', 18)
// 或
Vue.set(vm.user, 'age', 18)

问题2:无法监听属性删除

// Vue2 - 无法响应
delete vm.user.name  // ❌ 视图不会更新

// ✅ 必须使用 $delete
vm.$delete(vm.user, 'name')

问题3:无法监听数组索引变化

// Vue2 - 无法响应
const vm = new Vue({
  data: {
    items: ['a', 'b', 'c']
  }
})

vm.items[0] = 'x'  // ❌ 视图不会更新
vm.items.length = 0  // ❌ 视图不会更新

// ✅ 必须使用数组方法或 $set
vm.$set(vm.items, 0, 'x')
vm.items.splice(0, vm.items.length)

Vue2 的变通方案

  • 重写了 7 个数组方法(push、pop、shift、unshift、splice、sort、reverse)
  • 提供 $set$delete API

Vue3 的解决方案

// Vue3 - 全部自动响应
import { reactive } from 'vue'

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

// ✅ 属性新增 - 自动响应
state.user.age = 18

// ✅ 属性删除 - 自动响应
delete state.user.name

// ✅ 数组索引 - 自动响应
state.items[0] = 'x'

// ✅ 数组长度 - 自动响应
state.items.length = 0

// ✅ Map/Set - 也支持响应式
const map = reactive(new Map())
map.set('key', 'value')  // 自动响应

初始化性能

Vue2:必须递归遍历所有属性

// Vue2 - 初始化时递归遍历
function observe(obj) {
  if (typeof obj !== 'object') return
  
  // 遍历所有属性
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key])
    // 递归处理嵌套对象
    observe(obj[key])
  })
}

const data = {
  level1: {
    level2: {
      level3: {
        level4: { /* 1000个属性 */ }
      }
    }
  }
}

// 初始化时会遍历所有嵌套对象的所有属性
observe(data)  // 慢!

Vue3:惰性代理

// Vue3 - 只在访问时才代理嵌套对象
function reactive(target) {
  return new Proxy(target, {
    get(target, key) {
      const result = Reflect.get(target, key)
      
      // 只在实际访问时才代理嵌套对象
      if (typeof result === 'object' && result !== null) {
        return reactive(result)
      }
      
      return result
    }
  })
}

const data = reactive({
  level1: {
    level2: {
      level3: {
        level4: { /* 1000个属性 */ }
      }
    }
  }
})

// 初始化很快,只代理了最外层对象
// 只有访问 data.level1 时才代理 level1

性能对比

  • 初始化性能:Vue3 > Vue2(惰性代理 vs 递归遍历)
  • 运行时性能:Vue2 ≈ Vue3(Proxy 单次操作略慢,但差异很小)
  • 内存占用:Vue3 < Vue2(惰性代理减少了不必要的代理对象)

大型应用优化建议

Vue2 优化

// 使用 Object.freeze 跳过响应式
export default {
  data() {
    return {
      // 大量静态数据,不需要响应式
      staticData: Object.freeze(largeArray),
      // 需要响应式的数据
      dynamicData: []
    }
  }
}

Vue3 优化

import { shallowReactive, shallowRef, markRaw } from 'vue'

// 浅层响应式:只代理第一层
const state = shallowReactive({
  nested: { /* 深层对象,不会被代理 */ }
})

// 浅层 ref:不深度监听 .value 的变化
const list = shallowRef([/* 大数组 */])

// 标记为非响应式
const data = markRaw({
  /* 永远不会变成响应式 */
})

坑1:Vue2 动态添加属性不响应

问题场景

export default {
  data() {
    return {
      form: {
        name: ''
      }
    }
  },
  mounted() {
    // ❌ 后端返回的额外字段,视图不更新
    this.$http.get('/api/form').then(res => {
      this.form.email = res.email  // 不响应!
    })
  }
}

解决方案

// 方案1:使用 $set
this.$set(this.form, 'email', res.email)

// 方案2:整体替换对象
this.form = { ...this.form, email: res.email }

// 方案3:初始化时声明所有字段
data() {
  return {
    form: {
      name: '',
      email: ''  // 提前声明
    }
  }
}

坑2:数组索引修改不响应

问题场景

export default {
  data() {
    return {
      list: [{ id: 1, name: 'A' }, { id: 2, name: 'B' }]
    }
  },
  methods: {
    updateItem() {
      // ❌ 不响应
      this.list[0] = { id: 1, name: 'Updated' }
    }
  }
}

解决方案

// 方案1:使用 $set
this.$set(this.list, 0, { id: 1, name: 'Updated' })

// 方案2:使用 splice
this.list.splice(0, 1, { id: 1, name: 'Updated' })

// 方案3:使用 Vue.set
Vue.set(this.list, 0, { id: 1, name: 'Updated' })

坑3:嵌套对象响应式丢失

问题场景

// Vue2
export default {
  data() {
    return {
      user: {
        profile: {
          name: 'Alice'
        }
      }
    }
  },
  methods: {
    resetProfile() {
      // ❌ 新对象没有响应式
      this.user.profile = { name: 'Bob', age: 20 }
      // age 字段不响应
    }
  }
}

解决方案

// 方案1:使用 $set 添加新属性
this.$set(this.user.profile, 'age', 20)

// 方案2:使用扩展运算符
this.user = {
  ...this.user,
  profile: { name: 'Bob', age: 20 }
}

// 方案3:Vue3 没有这个问题
const user = reactive({ profile: { name: 'Alice' } })
user.profile = { name: 'Bob', age: 20 }  // ✅ 自动响应

  1. 提前声明所有属性:避免动态添加属性
  2. 使用 $set/$delete:处理动态属性
  3. 数组操作用变异方法:push、splice 等
  4. 大数据用 Object.freeze:性能优化
  1. 选择合适的响应式 API
    • reactive():对象/数组
    • ref():基本类型
    • shallowReactive():浅层响应式
    • readonly():只读代理
  2. 性能优化
    • 使用 shallowRef/shallowReactive 处理大数据
    • 使用 markRaw 标记非响应式数据
    • 使用 toRaw 获取原始对象
  3. 注意事项
    • reactive 对象解构会丢失响应式,使用 toRefs
    • ref 在模板中自动解包,在 JS 中需要 .value