Vue2 与 Vue3 响应式原理

一、核心概念与差异概述

特性 Vue2 Vue3
实现方式 Object.defineProperty Proxy
数组响应式 重写数组方法 原生支持
新增/删除属性 需要 Vue.set/delete 直接响应
性能优化 递归初始化所有属性 惰性代理
嵌套对象处理 递归遍历 按需代理
代码复杂度 较高 较低

二、Vue2 响应式原理详解

1. 核心实现:Object.defineProperty

Vue2 使用 Object.defineProperty 实现响应式:

javascript 复制代码
function defineReactive(obj, key) {
  let value = obj[key]
  const dep = new Dep() // 依赖收集器
  
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      // 依赖收集
      if (Dep.target) {
        dep.depend()
      }
      return value
    },
    set(newVal) {
      if (newVal === value) return
      value = newVal
      // 通知更新
      dep.notify()
    }
  })
}

2. 依赖收集系统

javascript 复制代码
class Dep {
  constructor() {
    this.subs = new Set()
  }
  
  depend() {
    if (Dep.target) {
      this.subs.add(Dep.target)
    }
  }
  
  notify() {
    this.subs.forEach(sub => sub.update())
  }
}

Dep.target = null

// 观察者类
class Watcher {
  constructor(getter) {
    this.getter = getter
    this.value = this.get()
  }
  
  get() {
    Dep.target = this
    const value = this.getter()
    Dep.target = null
    return value
  }
  
  update() {
    this.value = this.getter()
    console.log('视图更新!')
  }
}

3. 数组响应式处理

Vue2 需要特殊处理数组:

javascript 复制代码
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'
]

methodsToPatch.forEach(method => {
  const original = arrayProto[method]
  
  arrayMethods[method] = function(...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    
    if (inserted) ob.observeArray(inserted)
    
    // 通知更新
    ob.dep.notify()
    return result
  }
})

4. 完整响应式流程

javascript 复制代码
class Observer {
  constructor(value) {
    this.value = value
    this.dep = new Dep()
    
    // 标记已被观察
    Object.defineProperty(value, '__ob__', {
      value: this,
      enumerable: false,
      writable: true,
      configurable: true
    })
    
    if (Array.isArray(value)) {
      // 重写数组方法
      value.__proto__ = arrayMethods
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
  
  walk(obj) {
    Object.keys(obj).forEach(key => {
      defineReactive(obj, key)
    })
  }
  
  observeArray(items) {
    for (let i = 0; i < items.length; i++) {
      observe(items[i])
    }
  }
}

function observe(value) {
  if (typeof value !== 'object' || value === null) return
  
  // 已观察过则直接返回
  if (value.hasOwnProperty('__ob__') && value.__ob__ instanceof Observer) {
    return value.__ob__
  }
  
  return new Observer(value)
}

5. Vue2 响应式使用示例

javascript 复制代码
const data = {
  count: 0,
  user: {
    name: 'John',
    age: 30
  },
  items: [1, 2, 3]
}

observe(data)

// 创建观察者
new Watcher(() => {
  console.log(`Count: ${data.count}`)
})

new Watcher(() => {
  console.log(`Items: ${data.items.join(', ')}`)
})

// 测试响应式
data.count++ // 输出: Count: 1
data.items.push(4) // 输出: Items: 1, 2, 3, 4
data.user.name = 'Alice' // 不会触发更新,因为未创建对应Watcher

6. Vue2 响应式局限性

  1. 新增属性不响应

    javascript 复制代码
    data.newProp = 'value' // 非响应式

    需要使用 Vue.set

    javascript 复制代码
    Vue.set(data, 'newProp', 'value')
  2. 数组索引修改不响应

    javascript 复制代码
    data.items[0] = 10 // 非响应式

    需要使用 splice

    javascript 复制代码
    data.items.splice(0, 1, 10)
  3. 性能问题:初始化时递归遍历所有属性

三、Vue3 响应式原理详解

1. 核心实现:Proxy 代理

Vue3 使用 Proxy 实现响应式:

javascript 复制代码
function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      track(target, key) // 依赖收集
      const res = Reflect.get(target, key, receiver)
      // 处理嵌套对象
      if (typeof res === 'object' && res !== null) {
        return reactive(res) // 惰性代理
      }
      return res
    },
    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
    }
  })
}

2. 依赖收集系统

javascript 复制代码
const targetMap = new WeakMap() // 存储所有响应式对象及其依赖

// 当前活动的副作用函数
let activeEffect = null

function track(target, key) {
  if (!activeEffect) return
  
  // 获取target对应的depsMap
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }
  
  // 获取key对应的依赖集合
  let dep = depsMap.get(key)
  if (!dep) {
    dep = new Set()
    depsMap.set(key, dep)
  }
  
  // 添加当前副作用函数
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect)
    // 同时让副作用函数记录依赖集合
    activeEffect.deps.push(dep)
  }
}

function trigger(target, key) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  
  const effects = depsMap.get(key)
  if (effects) {
    // 创建副本避免无限循环
    new Set(effects).forEach(effect => effect())
  }
}

3. 副作用函数(Effect)

javascript 复制代码
function effect(fn) {
  const effectFn = () => {
    try {
      // 保存当前活动的副作用函数
      const prevActiveEffect = activeEffect
      activeEffect = effectFn
      // 执行前清除旧依赖
      cleanup(effectFn)
      return fn()
    } finally {
      activeEffect = prevActiveEffect
    }
  }
  
  // 存储该副作用函数依赖的集合
  effectFn.deps = []
  // 立即执行一次
  effectFn()
  return effectFn
}

function cleanup(effectFn) {
  // 从所有依赖集合中移除该副作用函数
  for (const dep of effectFn.deps) {
    dep.delete(effectFn)
  }
  effectFn.deps.length = 0
}

4. 完整响应式流程

javascript 复制代码
// 创建响应式对象
const state = reactive({
  count: 0,
  user: {
    name: 'John',
    age: 30
  },
  items: [1, 2, 3]
})

// 创建副作用函数
effect(() => {
  console.log(`Count: ${state.count}`)
})

effect(() => {
  console.log(`Items: ${state.items.join(', ')}`)
})

// 测试响应式
state.count++ // 输出: Count: 1
state.items.push(4) // 输出: Items: 1, 2, 3, 4
state.user.name = 'Alice' // 不会触发更新,因为没有访问user.name

5. Vue3 响应式优势

  1. 全面响应

    javascript 复制代码
    state.newProp = 'value' // 响应式
    delete state.newProp    // 响应式
  2. 数组索引修改响应

    javascript 复制代码
    state.items[0] = 10 // 响应式
  3. 性能优化

    • 惰性代理:只有访问到的属性才会被代理
    • 更细粒度的依赖收集
  4. 支持 Map、Set 等集合类型

6. ref 实现原理

javascript 复制代码
function ref(value) {
  return {
    get value() {
      track(this, 'value') // 依赖收集
      return value
    },
    set value(newVal) {
      if (newVal !== value) {
        value = newVal
        trigger(this, 'value') // 触发更新
      }
    }
  }
}

// 使用示例
const count = ref(0)

effect(() => {
  console.log(`Ref count: ${count.value}`)
})

count.value++ // 输出: Ref count: 1

四、响应式系统对比分析

1. 性能对比

操作 Vue2 响应式 Vue3 响应式
初始化 1000 个对象 100ms 50ms
添加新属性 慢(需特殊处理) 快(原生支持)
数组操作 中等(需重写方法) 快(原生支持)
嵌套对象访问 递归初始化 惰性代理

2. 功能对比

功能 Vue2 Vue3
属性添加/删除响应
数组索引修改响应
Map/Set 支持
性能优化 有限 显著
代码复杂度

3. 内存占用对比

Vue3 响应式系统使用 WeakMap 存储依赖关系,当对象不再被引用时,可以自动被垃圾回收,内存管理更高效。

五、实际应用场景

1. 表单绑定(Vue3)

javascript 复制代码
import { reactive } from 'vue'

export default {
  setup() {
    const form = reactive({
      username: '',
      password: '',
      remember: false
    })
    
    return { form }
  }
}

2. 复杂数据操作(Vue3)

javascript 复制代码
const state = reactive({
  users: new Map(),
  selectedIds: new Set()
})

// 添加用户
function addUser(id, user) {
  state.users.set(id, user)
}

// 选择用户
function toggleUser(id) {
  if (state.selectedIds.has(id)) {
    state.selectedIds.delete(id)
  } else {
    state.selectedIds.add(id)
  }
}

3. 组件状态管理(Vue3 Composition API)

javascript 复制代码
import { reactive, computed } from 'vue'

export function useCounter() {
  const state = reactive({
    count: 0,
    double: computed(() => state.count * 2)
  })
  
  function increment() {
    state.count++
  }
  
  return {
    state,
    increment
  }
}

六、Vue3 响应式进阶特性

1. shallowReactive

javascript 复制代码
import { shallowReactive } from 'vue'

const state = shallowReactive({
  nested: {
    count: 0 // 不会自动转换为响应式
  }
})

// 需要手动转换
state.nested = reactive(state.nested)

2. readonly

javascript 复制代码
import { reactive, readonly } from 'vue'

const original = reactive({ count: 0 })
const copy = readonly(original)

// 修改原始对象会影响只读副本
original.count++ // 成功
copy.count++ // 失败,控制台警告

3. watchEffect 和 watch

javascript 复制代码
import { ref, watchEffect, watch } from 'vue'

const count = ref(0)

// 自动追踪依赖
watchEffect(() => {
  console.log(`Count: ${count.value}`)
})

// 明确指定侦听源
watch(count, (newVal, oldVal) => {
  console.log(`Count changed from ${oldVal} to ${newVal}`)
})

七、响应式系统最佳实践

  1. 合理选择响应式 API

    • 对象:使用 reactive
    • 基本类型:使用 ref
    • 只读数据:使用 readonly
  2. 避免解构响应式对象

    javascript 复制代码
    // 错误:解构会丢失响应性
    const { x, y } = reactive({ x: 0, y: 0 })
    
    // 正确:使用 toRefs
    const state = reactive({ x: 0, y: 0 })
    const { x, y } = toRefs(state)
  3. 优化大型列表

    javascript 复制代码
    // 使用 shallowRef 避免深度响应
    const largeList = shallowRef([])
  4. 注意循环引用

    javascript 复制代码
    const obj = reactive({})
    obj.self = obj // 可能导致内存泄漏
  5. 组合式函数封装

    javascript 复制代码
    // useFetch.js
    import { ref } from 'vue'
    
    export function useFetch(url) {
      const data = ref(null)
      const error = ref(null)
      
      fetch(url)
        .then(res => res.json())
        .then(json => data.value = json)
        .catch(err => error.value = err)
      
      return { data, error }
    }

总结

Vue2 和 Vue3 的响应式系统核心差异在于实现机制:

  • Vue2 使用 Object.defineProperty,需要递归遍历对象属性进行劫持,对数组需要特殊处理
  • Vue3 使用 Proxy,可以代理整个对象,支持更多操作类型,性能更好

Vue3 响应式系统的优势:

  1. 更全面的响应式覆盖(包括新增/删除属性)
  2. 更好的性能表现(惰性代理)
  3. 更简洁的实现代码
  4. 更好的集合类型支持(Map、Set等)
  5. 更细粒度的依赖跟踪
相关推荐
quan263112 分钟前
Vue实践篇-02,AI生成代码
前端·javascript·vue.js
qb1 小时前
vue3.5.18源码-编译-入口
前端·vue.js·架构
虫无涯1 小时前
【分享】基于百度脑图,并使用Vue二次开发的用例脑图编辑器组件
前端·vue.js·编辑器
NewChapter °1 小时前
如何通过 Gitee API 上传文件到指定仓库
前端·vue.js·gitee·uni-app
练习时长两年半的Java练习生(升级中)1 小时前
从0开始学习Java+AI知识点总结-30.前端web开发(JS+Vue+Ajax)
前端·javascript·vue.js·学习·web
小高0073 小时前
🧙‍♂️ 老司机私藏清单:从“记事本”到“旗舰 IDE”,我只装了这 12 个插件
前端·javascript·vue.js
EveryPossible4 小时前
终止异步操作
前端·javascript·vue.js
Stringzhua4 小时前
setup函数相关【3】
前端·javascript·vue.js
neon12044 小时前
解决Vue Canvas组件在高DPR屏幕上的绘制偏移和区域缩放问题
前端·javascript·vue.js·canva可画
李长鸿7 小时前
vue3中的插槽和其他
vue.js