Vue3 Composition API响应式系统:Proxy代理机制与依赖收集原理

Vue3的响应式系统从Object.defineProperty全面迁移到ES6 Proxy,解决了Vue2中无法检测属性新增/删除和数组索引修改的局限性。Composition API配合reactive和ref构建响应式状态,通过track和effect机制实现细粒度依赖追踪。本文从Proxy代理机制、依赖收集与触发、ref与reactive实现原理三个层面解析Vue3响应式系统的运行机制,并提供自定义响应式实现的代码方案。

Proxy代理机制与响应式对象创建

Vue3使用Proxy代替Object.defineProperty实现响应式。Proxy拦截对象的全部13种操作(包括get、set、has、deleteProperty等),无需预先定义所有属性。reactive函数接收原始对象,返回一个Proxy代理对象。

// Vue3 reactive 核心:代理对象创建
const reactiveMap = new WeakMap()

function reactive(target) {
  // 非对象类型直接返回
  if (typeof target !== 'object' || target === null) {
    return target
  }

  // 已代理过的对象直接返回缓存
  if (reactiveMap.has(target)) {
    return reactiveMap.get(target)
  }

  const proxy = new Proxy(target, {
    get(target, key, receiver) {
      if (key === '__isReactive') {
        return true
      }
      track(target, key)  // 依赖收集
      const result = Reflect.get(target, key, receiver)

      // 深层响应式:对象类型递归代理
      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 (!Object.is(oldValue, value)) {
        trigger(target, key)
      }
      return result
    },

    deleteProperty(target, key) {
      const hadKey = key in target
      const result = Reflect.deleteProperty(target, key)
      if (hadKey && result) {
        trigger(target, key)
      }
      return result
    },

    has(target, key) {
      track(target, key)
      return Reflect.has(target, key)
    },

    ownKeys(target) {
      track(target, Symbol.iterator)
      return Reflect.ownKeys(target)
    }
  })

  reactiveMap.set(target, proxy)
  return proxy
}

Reflect.get/set的使用保证代理行为与原始对象一致,receiver参数确保原型链上的getter/setter上下文正确。深层响应式通过get拦截器中对返回值递归调用reactive实现,这也是惰性的——只有被访问的属性才会被代理,避免初始化时的性能开销。

依赖收集与触发更新机制

Vue3的依赖收集基于effect函数和track/trigger两个核心操作。effect包装副作用函数,执行时自动收集其内部访问的响应式属性作为依赖;当属性变化时,trigger找到所有依赖该属性的effect并重新执行。

// 依赖映射表:target -> key -> Set
const targetMap = new WeakMap()
let activeEffect = null

// effect包装器
function effect(fn) {
  const _effect = function() {
    activeEffect = _effect
    try {
      fn()
    } finally {
      activeEffect = null
    }
  }
  _effect.deps = []  // 记录该effect依赖的所有属性
  _effect()
  return _effect
}

// 依赖收集
function track(target, key) {
  if (!activeEffect) return

  let depsMap = targetMap.get(target)
  if (!depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }

  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 dep = depsMap.get(key)
  if (!dep) return

  // 复制一份遍历,避免effect执行过程中修改Set导致无限循环
  const effects = new Set(dep)
  effects.forEach(effect => {
    // 避免effect内部触发自身更新导致递归
    if (effect !== activeEffect) {
      effect()
    }
  })
}

targetMap使用WeakMap存储依赖关系,key为目标对象,value为Map(属性名到依赖集合的映射)。WeakMap的弱引用特性使得原始对象被回收时,对应的依赖关系自动清理,避免内存泄漏。

使用示例:

const state = reactive({ count: 0, name: 'Vue' })

// effect自动收集count依赖
effect(() => {
  console.log('count changed:', state.count)
})

state.count = 1  // 输出: count changed: 1
state.name = 'Vue3'  // 无输出(effect未依赖name)

ref实现原理与ref与reactive对比

ref用于包装基本类型值(也可以包装对象),通过.value访问器实现响应式。ref内部使用一个只有value属性的虚拟对象,对其调用reactive或自定义get/set拦截。

function ref(value) {
  // 已是ref直接返回
  if (isRef(value)) {
    return value
  }

  // 对象类型直接使用reactive
  const wrapper = {
    get value() {
      track(wrapper, 'value')
      return value
    },
    set value(newValue) {
      if (!Object.is(value, newValue)) {
        value = newValue
        trigger(wrapper, 'value')
      }
    }
  }

  wrapper.__isRef = true
  return wrapper
}

function isRef(r) {
  return r && r.__isRef === true
}

// 使用示例
const count = ref(0)
const user = ref({ name: 'Alice', age: 25 })

effect(() => {
  console.log('count:', count.value)
})

count.value++  // 输出: count: 1
user.value.name = 'Bob'  // ref包裹的对象内部修改也会触发响应

ref与reactive的核心区别:ref通过.value访问,reactive直接访问属性;ref可包装基本类型,reactive只能包装对象;ref包装对象时内部委托给reactive,实现深层响应。在Composition API中,ref更适合独立值状态管理,reactive更适合表单、配置等对象状态。

computed计算属性实现

computed基于effect实现,增加缓存和脏标记(dirty flag)机制。依赖未变化时返回缓存值,依赖变化时标记为脏,下次访问时重新计算。

function computed(getter) {
  let value
  let dirty = true

  // 使用effect但设置lazy模式,不立即执行
  const _effect = effect(getter, {
    lazy: true,
    scheduler: () => {
      dirty = true  // 依赖变化时标记为脏
      // 通知依赖该computed的effect
      trigger(wrapper, 'value')
    }
  })

  const wrapper = {
    get value() {
      if (dirty) {
        value = _effect()  // 执行getter计算新值
        dirty = false
      }
      track(wrapper, 'value')  // 收集依赖此computed的effect
      return value
    },
    __isRef: true
  }

  return wrapper
}

// 修改effect支持lazy选项
function effect(fn, options = {}) {
  const _effect = function() {
    activeEffect = _effect
    try {
      return fn()
    } finally {
      activeEffect = null
    }
  }
  _effect.deps = []
  _effect.scheduler = options.scheduler

  if (!options.lazy) {
    _effect()  // 非lazy模式立即执行
  }
  return _effect
}

// 使用示例
const state = reactive({ firstName: '张', lastName: '三' })
const fullName = computed(() => `${state.firstName}${state.lastName}`)

effect(() => {
  console.log('姓名:', fullName.value)  // 姓名: 张三
})

state.lastName = '四'  // 输出: 姓名: 张四

watch侦听器与副作用清理

watch监听响应式数据变化,执行回调函数。内部基于effect和旧值缓存实现,支持 oldValue参数传入回调。通过onCleanup注册副作用清理函数,避免竞态条件。

function watch(source, cb, options = {}) {
  let oldValue
  let cleanup

  function onCleanup(fn) {
    cleanup = fn
  }

  const _effect = effect(() => {
    // 访问source收集依赖
    const newValue = typeof source === 'function' ? source() : source.value

    // 清理上一次的副作用
    if (cleanup) cleanup()

    if (!Object.is(oldValue, newValue)) {
      cb(newValue, oldValue, onCleanup)
      oldValue = newValue
    }
  }, {
    lazy: true,
    scheduler: () => {
      // 依赖变化时触发effect重新执行
      _effect()
    }
  })

  // 初始化获取旧值
  oldValue = _effect()

  // immediate选项
  if (options.immediate) {
    cb(oldValue, undefined, onCleanup)
  }
}

// 使用示例
const keyword = ref('')

watch(keyword, async (newVal, oldVal, onCleanup) => {
  const controller = new AbortController()
  onCleanup(() => controller.abort())

  const res = await fetch(`/api/search?q=${newVal}`, {
    signal: controller.signal
  })
  console.log(await res.json())
})

keyword.value = 'Vue3'  // 发起请求
keyword.value = 'Vue4'  // 中断上一个请求,发起新请求

watch的onCleanup机制解决了异步操作中的竞态问题:当快速连续修改keyword时,前一个请求被abort,只有最后一个请求的结果会被处理。这在搜索联想、自动保存等场景中非常实用。

响应式系统性能优化

Vue3响应式系统在设计层面做了多项性能优化:

1. 惰性响应式:reactive对象的属性在被访问时才递归代理,避免初始化时遍历整个对象树。

2. 依赖清理:effect执行前清理上一轮的依赖关系(通过deps数组),处理条件分支导致的依赖变化。例如effect内部有if/else分支,条件变化后不再访问的属性需要从依赖中移除。

// 依赖清理实现
function cleanupEffect(effect) {
  effect.deps.forEach(dep => {
    dep.delete(effect)
  })
  effect.deps.length = 0
}

function effect(fn, options = {}) {
  const _effect = function() {
    activeEffect = _effect
    cleanupEffect(_effect)  // 清理旧依赖
    try {
      return fn()
    } finally {
      activeEffect = null
    }
  }
  // ...
}

// track中更新反向追溯
function track(target, key) {
  if (!activeEffect) return
  // ...
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect)
    activeEffect.deps.push(dep)
  }
}

3. 批量更新:多次状态修改在同一事件循环中只触发一次DOM更新。Vue3通过jobQueue和flushJobs实现微任务级别的批量调度。

const queue = new Set()
let isFlushing = false

function queueJob(job) {
  queue.add(job)  // Set自动去重
  if (!isFlushing) {
    isFlushing = true
    Promise.resolve().then(flushJobs)
  }
}

function flushJobs() {
  try {
    queue.forEach(job => job())
  } finally {
    queue.clear()
    isFlushing = false
  }
}

// trigger中使用队列
function trigger(target, key) {
  // ...
  effects.forEach(effect => {
    if (effect.options.scheduler) {
      queueJob(effect.options.scheduler)
    } else {
      effect()
    }
  })
}

批量更新确保了连续修改多个响应式属性时不会重复执行渲染effect,将多次DOM操作合并为一次。这是Vue3渲染性能优于Vue2的关键因素之一。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3compositionapi-xiang-ying-shi-xi-tong-proxy-dai-li-ji/

(0)
小编小编
上一篇 1小时前
下一篇 1小时前

相关推荐