Vue3响应式系统原理剖析:Proxy代理与依赖收集到视图更新的完整链路

Vue3的响应式系统相比Vue2发生了根本性重构,从Object.defineProperty迁移到Proxy,解决了数组监听限制和新增属性检测等问题。理解Vue3响应式的底层实现链路,对排查组件更新异常、优化渲染性能和编写自定义ref/composable至关重要。本文从源码层面拆解reactive、ref、effect三者的协作机制。

Proxy代理与reactive的实现机制

Vue3使用ES6 Proxy替代Object.defineProperty实现对象代理。Proxy可以拦截13种操作,而defineProperty只能拦截属性读取和设置。对于数组的索引修改、length变更以及Map/Set操作,Proxy都能原生拦截。

reactive函数的核心实现:

const reactiveMap = new WeakMap()

function reactive(target) {
  if (typeof target !== 'object' || target === null) {
    return target
  }

  if (target[ReactiveFlags.IS_REACTIVE]) {
    return target
  }

  const existingProxy = reactiveMap.get(target)
  if (existingProxy) {
    return existingProxy
  }

  const proxy = new Proxy(target, {
    get(target, key, receiver) {
      if (key === ReactiveFlags.IS_REACTIVE) {
        return true
      }
      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 (!Object.is(oldValue, value)) {
        trigger(target, key, value, oldValue)
      }
      return result
    },
    deleteProperty(target, key) {
      const hadKey = key in target
      const result = Reflect.deleteProperty(target, key)
      if (hadKey && result) {
        trigger(target, key, undefined, undefined, 'delete')
      }
      return result
    },
    has(target, key) {
      const result = Reflect.has(target, key)
      track(target, key)
      return result
    },
    ownKeys(target) {
      track(target, ITERATE_KEY)
      return Reflect.ownKeys(target)
    }
  })

  reactiveMap.set(target, proxy)
  return proxy
}

关键设计点:reactiveMap使用WeakMap缓存原始对象到代理对象的映射,防止同一对象被多次代理造成内存泄漏。嵌套对象采用懒代理策略——只有访问到该属性时才递归调用reactive,减少初始化开销。

依赖收集系统:track与effect的依赖图

响应式系统的核心是建立属性到effect的依赖映射。当effect内读取了响应式对象的属性时,这个effect被记录为该属性的依赖;当属性变化时,遍历该属性的依赖列表重新执行对应effect。

const targetMap = new WeakMap()
let activeEffect = null
const effectStack = []

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, newValue, oldValue, type) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return

  const effects = new Set()

  if (type === 'add' || type === 'delete') {
    const iterateEffects = depsMap.get(ITERATE_KEY)
    if (iterateEffects) {
      iterateEffects.forEach(e => effects.add(e))
    }
  }

  const dep = depsMap.get(key)
  if (dep) {
    dep.forEach(e => effects.add(e))
  }

  effects.forEach(effect => {
    if (effect !== activeEffect) {
      effect.scheduler ? effect.scheduler() : effect.run()
    }
  })
}

依赖映射使用WeakMap<target, Map<key, Set<effect>>>三层结构。WeakMap的弱引用特性确保target对象被回收时,对应的依赖记录自动清除。

effect函数与调度器实现

effect是响应式系统的执行单元,组件的render函数和watch回调都被包装为effect。Vue3利用scheduler参数将更新推入微任务队列实现异步批处理。

class ReactiveEffect {
  constructor(fn, scheduler) {
    this.fn = fn
    this.scheduler = scheduler
    this.deps = []
    this.active = true
  }

  run() {
    if (!this.active) {
      return this.fn()
    }

    try {
      effectStack.push(this)
      activeEffect = this
      return this.fn()
    } finally {
      effectStack.pop()
      activeEffect = effectStack[effectStack.length - 1]
    }
  }

  stop() {
    if (this.active) {
      cleanup(this)
      this.active = false
    }
  }
}

function cleanup(effect) {
  effect.deps.forEach(dep => {
    dep.delete(effect)
  })
  effect.deps.length = 0
}

function effect(fn, options = {}) {
  const _effect = new ReactiveEffect(fn, options.scheduler)
  if (!options.lazy) {
    _effect.run()
  }
  return _effect
}

// 组件更新批处理
const queue = new Set()
let isFlushing = false

function queueJob(job) {
  queue.add(job)
  if (!isFlushing) {
    isFlushing = true
    Promise.resolve().then(() => {
      queue.forEach(job => job())
      queue.clear()
      isFlushing = false
    })
  }
}

cleanup函数在每次effect重新执行前做依赖清理——清除上一次执行时收集的依赖关系,重新收集。这解决了条件分支导致的依赖变化。queueJob使用Promise.resolve().then()将多次同步状态变更合并为一次DOM更新——同一tick内多次修改响应式数据,组件只重新渲染一次。

ref的工作原理与基本类型响应式

Proxy只能代理对象,无法拦截基本类型的属性访问。ref通过对象包装解决这一限制——用.value属性作为代理点。

class RefImpl {
  constructor(value) {
    this._isRef = true
    this._rawValue = value
    this._value = typeof value === 'object' && value !== null
      ? reactive(value)
      : value
    this.dep = new Set()
  }

  get value() {
    if (activeEffect) {
      if (!this.dep.has(activeEffect)) {
        this.dep.add(activeEffect)
        activeEffect.deps.push(this.dep)
      }
    }
    return this._value
  }

  set value(newVal) {
    if (!Object.is(newVal, this._rawValue)) {
      this._rawValue = newVal
      this._value = typeof newVal === 'object' && newVal !== null
        ? reactive(newVal)
        : newVal
      this.dep.forEach(effect => {
        effect.scheduler ? effect.scheduler() : effect.run()
      })
    }
  }
}

ref与reactive的依赖模型不同:reactive的依赖存在全局targetMap中,ref的依赖存在实例自身的dep属性中。proxyRefs实现了模板中ref的自动解包——在template中写{{ count }}而非{{ count.value }}的原因。

computed实现与缓存机制

computed基于effect的lazy选项实现缓存。声明时创建effect但不立即执行,首次访问时才执行并缓存结果。只有依赖的响应式数据变化后,下一次访问才重新计算。

class ComputedRefImpl {
  constructor(getter) {
    this._isRef = true
    this._dirty = true
    this._value = undefined
    this.dep = new Set()

    this.effect = new ReactiveEffect(getter, () => {
      if (!this._dirty) {
        this._dirty = true
        this.dep.forEach(e => {
          e.scheduler ? e.scheduler() : e.run()
        })
      }
    })
  }

  get value() {
    if (activeEffect) {
      if (!this.dep.has(activeEffect)) {
        this.dep.add(activeEffect)
        activeEffect.deps.push(this.dep)
      }
    }

    if (this._dirty) {
      this._dirty = false
      this._value = this.effect.run()
    }

    return this._value
  }
}

核心设计:_dirty作为缓存标记,首次get时为true触发计算,之后为false直接返回缓存值。当computed依赖的reactive数据变化时,触发effect的scheduler,scheduler不执行计算,只将_dirty标记为true并通知依赖该computed的render effect。render effect重新执行时访问computed.value,发现dirty为true才触发重算。这种惰性求值策略避免了不需要的计算。

响应式调试与性能优化实践

排查响应式更新未触发时,使用Vue DevTools的Component Inspector检查依赖列表。如果某个属性未出现在依赖集合中,说明组件render过程中未实际读取该属性。常见原因:在事件处理函数中读取属性不构成依赖(因为事件处理函数不是render effect),或条件分支导致属性未被访问。

对于大型列表渲染,使用shallowRef包装大数组,只在需要时手动触发更新:

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

const bigList = shallowRef([])

function loadData(data) {
  bigList.value = data
  triggerRef(bigList)
}

// 第三方类实例标记为不需要响应式
const staticConfig = markRaw({
  columns: [/* 大量静态数据 */],
  options: { /* ... */ }
})

markRaw在对象上添加__v_skip标记,reactive函数检测到该标记时跳过代理。适用于第三方类实例(如Map、Chart实例)和无需响应式的大数据集合。

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

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

相关推荐