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. 更细粒度的依赖跟踪
相关推荐
卤蛋fg622 分钟前
vxe-table 列拖拽排序与行拖拽排序:让表格布局任意排序
vue.js
粉末的沉淀1 小时前
vue:Vite项目中高效管理纯色SVG图标的方案
前端·javascript·vue.js
卤蛋fg61 小时前
vxe-table 列宽与行高拖拽调整:让表格布局极其灵活,拖拽功能非常强大
vue.js
向日的葵0061 小时前
Vue 路由传参的三种方式(三)
vue.js·路由
如果超人不会飞2 小时前
TinyVue Checkbox复选框组件使用指南
前端·vue.js
如果超人不会飞2 小时前
TinyVue Radio单选框组件使用指南
vue.js
鲁班小子2 小时前
Vite resolve.dedupe 使用教程
vue.js·vite
如果超人不会飞2 小时前
TinyVue Input输入框组件使用指南
vue.js
如果超人不会飞2 小时前
TinyVue Pager分页组件使用指南
前端·vue.js
大刚测试开发实战3 小时前
TestHub重磅更新!AI用例生成增加流式输出、Markdown文档上传、模型配置检测、AI评审开关控制...
vue.js·后端·github