vue3响应式系统

110 阅读8分钟

Vue3响应式系统核心原理与实现详解

引言

Vue3的响应式系统是其性能提升和开发体验优化的核心所在。相比Vue2基于Object.defineProperty的实现,Vue3采用了更现代的Proxy API,带来了更全面的拦截能力和更好的性能表现。本文将深入解析Vue3响应式系统的实现原理,从核心概念到具体实现细节,帮助开发者深入理解这一重要机制。

核心原理概述

Vue3响应式系统的核心思想可以概括为四个步骤:

  1. 使用Proxy拦截对象的读取和设置操作
  2. 使用WeakMap存储副作用函数(依赖收集)
  3. 当读取属性时,收集依赖(track)
  4. 当设置属性时,触发更新(trigger)

为什么选择Proxy?

javascript

// Vue2使用Object.defineProperty
Object.defineProperty(obj, key, {
  get() {
    // 收集依赖
    return value
  },
  set(newVal) {
    // 触发更新
    value = newVal
  }
})

// Vue3使用Proxy
new Proxy(obj, {
  get(target, key, receiver) {
    // 收集依赖
    return Reflect.get(target, key, receiver)
  },
  set(target, key, value, receiver) {
    // 触发更新
    return Reflect.set(target, key, value, receiver)
  }
})

Proxy相比Object.defineProperty的优势:

  • 可以拦截更多操作(delete、in、for...in等)
  • 直接代理整个对象,无需递归遍历
  • 性能更好,内存占用更少

核心数据结构

响应式系统的核心是依赖收集机制,Vue3使用多层嵌套的数据结构来管理依赖关系:

javascript

/**
 * 存储副作用函数的"桶"
 * 使用 WeakMap 结构:
 * - key: 原始对象 (target)
 * - value: Map 结构,存储该对象所有属性的副作用函数
 */
const bucket = new WeakMap()

/**
 * 用于追踪 for...in 循环的 Symbol 键
 * 当使用 for...in 遍历对象时,无法确定具体访问了哪个属性
 * 因此使用一个特殊的 Symbol 键来存储遍历操作的副作用函数
 */
const ITERATE_KEY = Symbol()

/**
 * 当前激活的副作用函数
 * 用于在 track 函数中收集依赖
 */
let activeEffect

/**
 * 副作用函数栈
 * 用于处理嵌套的 effect 场景
 */
const effectStack = []

数据结构层次关系

text

bucket (WeakMap)
  └─ target (原始对象) → depsMap (Map)
       └─ key (属性名) → deps (Set)
            └─ effectFn (副作用函数)

为什么使用WeakMap?

  • WeakMap的键必须是对象,且是弱引用
  • 当对象被垃圾回收时,WeakMap中的对应项也会自动清除
  • 避免内存泄漏,不需要手动清理

创建响应式对象

Vue3提供了多种创建响应式对象的API,满足不同场景的需求:

reactive - 深度响应式

javascript

/**
 * 创建深度响应式对象
 * @param {Object|Array|Map|Set} obj - 要转换为响应式的原始对象
 * @returns {Proxy} - 返回响应式代理对象
 */
function reactive(obj) {
  // 检查缓存:如果已经创建过代理对象,直接返回
  const existionProxy = reactiveMap.get(obj)
  if (existionProxy) return existionProxy
  
  // 创建新的代理对象
  const proxy = createReactive(obj)
  
  // 存储到缓存中
  reactiveMap.set(obj, proxy)
  return proxy
}

createReactive - 核心创建函数

javascript

function createReactive(obj, isShallow = false, isReadonly = false) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      // 特殊属性处理
      if (key === 'raw') return target
      
      // 收集依赖
      if (!isReadonly && typeof key !== 'symbol') {
        track(target, key)
      }
      
      const res = Reflect.get(target, key, receiver)
      
      // 浅响应直接返回
      if (isShallow) return res
      
      // 深度响应式处理
      if (typeof res === 'object' && res !== null) {
        return isReadonly ? readonly(res) : reactive(res)
      }
      
      return res
    },
    
    set(target, key, newVal, receiver) {
      // 只读检查
      if (isReadonly) {
        console.warn(`Set operation on key "${String(key)}" failed: target is readonly.`)
        return true
      }
      
      const oldVal = target[key]
      const type = Array.isArray(target)
        ? Number(key) < target.length ? 'SET' : 'ADD'
        : Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD'
      
      const res = Reflect.set(target, key, newVal, receiver)
      
      // 触发更新条件判断
      if (receiver.raw === target && oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
        trigger(target, key, type, newVal)
      }
      
      return res
    },
    
    // 其他拦截器...
    has(target, key) {
      if (!isReadonly) track(target, key)
      return Reflect.has(target, key)
    },
    
    ownKeys(target) {
      if (!isReadonly) {
        track(target, Array.isArray(target) ? 'length' : ITERATE_KEY)
      }
      return Reflect.ownKeys(target)
    },
    
    deleteProperty(target, key) {
      if (isReadonly) {
        console.warn(`Delete operation on key "${String(key)}" failed: target is readonly.`)
        return true
      }
      
      const hadKey = Object.prototype.hasOwnProperty.call(target, key)
      const res = Reflect.deleteProperty(target, key)
      
      if (res && hadKey) {
        trigger(target, key, 'DELETE')
      }
      return res
    }
  })
}

其他响应式API

javascript

// 浅响应式
function shallowReactive(obj) {
  return createReactive(obj, true)
}

// 深度只读
function readonly(obj) {
  return createReactive(obj, false, true)
}

// 浅只读
function shallowReadonly(obj) {
  return createReactive(obj, true, true)
}

副作用系统

副作用系统是响应式机制的核心,负责建立数据与副作用函数之间的关联。

effect函数

javascript

/**
 * 副作用函数
 * 用于注册副作用函数,当依赖的响应式数据改变时,自动重新执行
 */
function effect(fn, options = {}) {
  const effectFn = () => {
    // 清理旧依赖
    cleanup(effectFn)
    
    // 设置当前激活的副作用函数
    activeEffect = effectFn
    effectStack.push(effectFn)
    
    // 执行副作用函数
    const res = fn()
    
    // 恢复之前的状态
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]
    
    return res
  }
  
  effectFn.options = options
  effectFn.deps = []
  
  if (!options.lazy) {
    effectFn()
  }
  
  return effectFn
}

依赖收集与触发

javascript

/**
 * 收集依赖(追踪)
 * 当访问响应式数据的属性时,建立属性与副作用函数的联系
 */
function track(target, key) {
  if (!activeEffect || !shouldTrack) return
  
  let depsMap = bucket.get(target)
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()))
  }
  
  let deps = depsMap.get(key)
  if (!deps) {
    depsMap.set(key, (deps = new Set()))
  }
  
  // 建立双向联系
  deps.add(activeEffect)
  activeEffect.deps.push(deps)
}

/**
 * 触发更新
 * 当修改响应式数据的属性时,执行所有依赖于这个属性的副作用函数
 */
function trigger(target, key, type, newVal) {
  const depsMap = bucket.get(target)
  if (!depsMap) return
  
  const effects = depsMap.get(key)
  const effectsToRun = new Set()
  
  effects && effects.forEach(effectFn => {
    if (effectFn !== activeEffect) {
      effectsToRun.add(effectFn)
    }
  })
  
  // 特殊处理:数组操作
  if (type === 'ADD' && Array.isArray(target)) {
    const lengthEffects = depsMap.get('length')
    lengthEffects && lengthEffects.forEach(effectFn => {
      if (effectFn !== activeEffect) {
        effectsToRun.add(effectFn)
      }
    })
  }
  
  // 特殊处理:数组length修改
  if (Array.isArray(target) && key === 'length') {
    depsMap.forEach((effects, key) => {
      const numericKey = Number(key)
      if (!isNaN(numericKey) && numericKey >= newVal) {
        effects.forEach(effectFn => {
          if (effectFn !== activeEffect) {
            effectsToRun.add(effectFn)
          }
        })
      }
    })
  }
  
  // 特殊处理:遍历操作
  if (type === 'ADD' || type === 'DELETE' || 
      (type === 'SET' && (target instanceof Map || target instanceof Set))) {
    const iterateEffects = depsMap.get(ITERATE_KEY)
    iterateEffects && iterateEffects.forEach(effectFn => {
      if (effectFn !== activeEffect) {
        effectsToRun.add(effectFn)
      }
    })
  }
  
  // 执行副作用函数
  effectsToRun.forEach(effectFn => {
    if (effectFn.options.scheduler) {
      effectFn.options.scheduler(effectFn)
    } else {
      effectFn()
    }
  })
}

依赖清理

javascript

/**
 * 清理副作用函数的依赖
 * 处理分支切换的情况(如 if 条件)
 */
function cleanup(effectFn) {
  for (let i = 0; i < effectFn.deps.length; i++) {
    const deps = effectFn.deps[i]
    deps.delete(effectFn)
  }
  effectFn.deps.length = 0
}

计算属性

计算属性是基于响应式数据的衍生值,具有缓存机制。

javascript

/**
 * 计算属性
 * 基于响应式数据计算出一个值,具有缓存机制
 */
function computed(getter) {
  let value
  let dirty = true
  let effectFn
  
  const obj = {
    get value() {
      if (dirty) {
        value = effectFn()
        dirty = false
      }
      // 收集依赖
      track(obj, 'value')
      return value
    }
  }
  
  effectFn = effect(getter, {
    lazy: true,
    scheduler() {
      if (!dirty) {
        dirty = true
        // 触发更新
        trigger(obj, 'value')
      }
    }
  })
  
  return obj
}

计算属性的特点:

  • 懒计算:只有在访问value时才计算
  • 缓存机制:依赖数据未改变时直接返回缓存值
  • 响应式:依赖改变时标记为dirty,下次访问重新计算

监听器

监听器用于观察响应式数据的变化,执行相应的回调函数。

javascript

/**
 * 监听器
 * 监听响应式数据的变化,当数据改变时执行回调函数
 */
function watch(source, cb, options = {}) {
  let getter
  
  if (typeof source === "function") {
    getter = source
  } else {
    getter = () => traverse(source)
  }
  
  let oldValue, newValue
  let cleanup
  
  function onInvalidate(fn) {
    cleanup = fn
  }
  
  const job = () => {
    newValue = effectFn()
    if (cleanup) {
      cleanup()
    }
    cb(oldValue, newValue, onInvalidate)
    oldValue = newValue
  }
  
  const effectFn = effect(() => getter(), {
    lazy: true,
    scheduler: () => {
      if (options.flush === 'post') {
        const p = Promise.resolve()
        p.then(job)
      } else {
        job()
      }
    }
  })
  
  if (options.immediate) {
    job()
  } else {
    oldValue = effectFn()
  }
}

/**
 * 遍历对象
 * 递归遍历对象的所有属性,用于建立响应式联系
 */
function traverse(value, seen = new Set()) {
  if (typeof value !== 'object' || value === null || seen.has(value)) return
  seen.add(value)
  for (const k in value) {
    traverse(value[k], seen)
  }
  return value
}

Ref API

Ref用于创建原始值的响应式引用。

javascript

/**
 * 创建原始值的响应式引用
 */
function ref(val) {
  const wrapper = {
    value: val
  }
  Object.defineProperty(wrapper, '__v_isRef', {
    value: true
  })
  return reactive(wrapper)
}

/**
 * 将响应式对象的某个属性转为 ref
 */
function toRef(obj, key) {
  const wrapper = {
    get value() {
      return obj[key]
    },
    set value(val) {
      obj[key] = val
    }
  }
  Object.defineProperty(wrapper, '__v_isRef', {
    value: true
  })
  return wrapper
}

/**
 * 将响应式对象的所有属性转为 ref
 */
function toRefs(obj) {
  const ret = {}
  for (const key in obj) {
    ret[key] = toRef(obj, key)
  }
  return ret
}

/**
 * 自动脱 ref
 * 在模板中访问 ref 时自动获取 .value 的值
 */
function proxyRefs(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      const value = Reflect.get(target, key, receiver)
      return value.__v_isRef ? value.value : value
    },
    set(target, key, newValue, receiver) {
      const value = target[key]
      if (value.__v_isRef) {
        value.value = newValue
        return true
      }
      return Reflect.set(target, key, newValue, receiver)
    }
  })
}

特殊类型处理

数组方法重写

javascript

const arrayInstrumentations = {}

// 重写数组的查找方法
['includes', 'indexOf', 'lastIndexOf'].forEach(method => {
  const originMethod = Array.prototype[method]
  arrayInstrumentations[method] = function (...args) {
    let res = originMethod.apply(this, args)
    if (res === false || res === -1) {
      res = originMethod.apply(this.raw, args)
    }
    return res
  }
})

// 重写会修改数组长度的方法
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(method => {
  const originMethod = Array.prototype[method]
  arrayInstrumentations[method] = function (...args) {
    shouldTrack = false
    let res = originMethod.apply(this, args)
    shouldTrack = true
    return res
  }
})

Map/Set方法重写

javascript

const mutableInstrumentations = {
  add(key) {
    const target = this.raw
    const hadKey = target.has(key)
    const res = target.add(key)
    if (!hadKey) {
      trigger(target, key, 'ADD')
    }
    return res
  },
  
  delete(key) {
    const target = this.raw
    const hadKey = target.has(key)
    const res = target.delete(key)
    if (hadKey) {
      trigger(target, key, 'DELETE')
    }
    return res
  },
  
  get(key) {
    const target = this.raw
    const had = target.has(key)
    track(target, key)
    if (had) {
      const res = target.get(key)
      return typeof res === 'object' && res !== null ? reactive(res) : res
    }
  },
  
  set(key, value) {
    const target = this.raw
    const had = target.has(key)
    const oldValue = target.get(key)
    const rawValue = value && value.raw ? value.raw : value
    target.set(key, rawValue)
    
    if (!had) {
      trigger(target, key, 'ADD')
    } else if (oldValue !== value || (oldValue === oldValue && value === value)) {
      trigger(target, key, 'SET')
    }
    return this
  },
  
  forEach(callback, thisArg) {
    const wrap = (val) => typeof val === 'object' && val !== null ? reactive(val) : val
    const target = this.raw
    track(target, ITERATE_KEY)
    target.forEach((v, k) => {
      callback.call(thisArg, wrap(v), wrap(k), this)
    })
  }
}

性能优化策略

Vue3响应式系统包含多种性能优化策略:

  1. 懒执行:computed属性只有在被访问时才计算
  2. 依赖清理:副作用函数重新执行前清理旧依赖
  3. 调度器:控制副作用函数的执行时机
  4. 分支切换优化:处理条件分支导致的依赖变化
  5. 缓存机制:避免重复计算和重复代理

总结

Vue3的响应式系统通过Proxy实现了更全面、更高效的响应式机制。其核心在于:

  1. 依赖收集:通过track函数建立数据与副作用函数的关联
  2. 触发更新:通过trigger函数在数据变化时执行相关副作用
  3. 精细控制:通过调度器控制副作用执行时机
  4. 全面覆盖:支持对象、数组、Map、Set等多种数据类型
  5. 性能优化:通过多种策略确保系统高效运行

理解Vue3响应式系统的实现原理,不仅有助于更好地使用Vue3,也能为开发者设计和实现自己的响应式系统提供宝贵参考。这种基于Proxy的响应式实现方式,代表了现代前端框架的发展方向,值得深入学习和掌握。