Vue 3 响应式原理剖析:从 Proxy 到副作用调度

Vue 3 响应式原理剖析:从 Proxy 到副作用调度

admin
2026-06-26 / 0 评论 / 5 阅读
Vue 3 用 Proxy 重写了响应式系统,性能与能力都大幅提升。本文从源码设计角度剖析依赖收集、触发更新、副作用调度和计算属性的实现原理,并手写一个迷你响应式系统。

一、Vue 2 vs Vue 3 响应式对比

特性Vue 2 (defineProperty)Vue 3 (Proxy)
属性新增/删除需要 Vue.set/Vue.delete直接支持
数组索引赋值不支持支持
Map/Set不支持支持
初始化递归遍历所有属性惰性代理,访问时才转换
性能大对象初始化慢按需代理,更快
// Vue 2 的局限
this.obj.newProp = 'value'   // ❌ 不触发更新
this.arr[0] = newItem        // ❌ 不触发更新
this.arr.length = 0          // ❌ 不触发更新

// Vue 3 全部支持
state.newProp = 'value'      // ✅
state.arr[0] = newItem       // ✅
state.arr.length = 0         // ✅
state.map.set('key', val)    // ✅

二、Proxy 基础

const raw = { price: 100, quantity: 2 }

const proxy = new Proxy(raw, {
  get(target, key, receiver) {
    console.log(`读取 ${key}`)
    track(target, key)  // 依赖收集
    return Reflect.get(target, key, receiver)
  },
  set(target, key, value, receiver) {
    console.log(`设置 ${key} = ${value}`)
    const result = Reflect.set(target, key, value, receiver)
    trigger(target, key)  // 触发更新
    return result
  },
  deleteProperty(target, key) {
    const result = Reflect.deleteProperty(target, key)
    trigger(target, key)
    return result
  },
  has(target, key) {
    track(target, key)
    return Reflect.has(target, key)
  }
})

proxy.price          // 读取 price
proxy.quantity = 3   // 设置 quantity = 3

为什么用 Reflect? 保证 this 指向代理对象,让内嵌对象也能被正确追踪:

const obj = reactive({
  nested: { count: 0 }  // nested 会被惰性转换为响应式
})
// obj.nested 访问时返回嵌套对象的代理
// 这是 Vue 3 实现深层响应式的关键

三、手写迷你响应式系统

1. 副作用函数与依赖收集

let activeEffect = null
const targetMap = new WeakMap()

// 追踪依赖
function track(target, key) {
  if (!activeEffect) return

  // targetMap: target → key → Set<effect>
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }

  let deps = depsMap.get(key)
  if (!deps) {
    deps = new Set()
    depsMap.set(key, deps)
  }

  deps.add(activeEffect)
}

// 触发更新
function trigger(target, key) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return

  const deps = depsMap.get(key)
  if (!deps) return

  // 复制一份,避免无限循环
  const effectsToRun = new Set(deps)
  effectsToRun.forEach(effect => {
    // 避免递归调用自身
    if (effect !== activeEffect) {
      effect()
    }
  })
}

2. effect 函数

function effect(fn) {
  const effectFn = () => {
    // 清除旧依赖(解决分支切换问题)
    cleanup(effectFn)
    activeEffect = effectFn
    fn()
    activeEffect = null
  }

  effectFn.deps = []
  effectFn()
  return effectFn
}

function cleanup(effectFn) {
  effectFn.deps.forEach(depSet => {
    depSet.delete(effectFn)
  })
  effectFn.deps.length = 0
}

3. reactive 实现

const reactiveMap = new Map()

function reactive(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 type = Object.prototype.hasOwnProperty.call(target, key)
        ? 'SET' : 'ADD'
      const result = Reflect.set(target, key, value, receiver)

      // 值变化才触发
      if (oldValue !== value || (typeof oldValue === 'NaN' && typeof value === 'NaN')) {
        trigger(target, key, type)
      }
      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, 'DELETE')
      }
      return result
    }
  })

  reactiveMap.set(target, proxy)
  return proxy
}

4. ref 实现

function ref(value) {
  return {
    get value() {
      track(this, 'value')
      return value
    },
    set value(newValue) {
      if (newValue !== value) {
        value = newValue
        trigger(this, 'value')
      }
    }
  }
}

5. computed 实现

function computed(getter) {
  let value
  let dirty = true  // 脏标记:是否需要重新计算

  // 作为副作用运行,依赖变化时标记为脏
  const effectFn = effect(() => {
    // 每次依赖变化重新执行,但不会立即触发渲染
    if (dirty) {
      value = getter()
    }
  })

  // 依赖变化时置脏
  // (简化实现,真实实现用 scheduler)
  
  return {
    get value() {
      if (dirty) {
        value = getter()
        dirty = false
      }
      return value
    }
  }
}

更接近真实的 computed:

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

  const runner = effect(getter, {
    lazy: true,
    // scheduler:依赖变化时不立即执行,只标记脏
    scheduler() {
      if (!dirty) {
        dirty = true
        // 通知依赖该 computed 的渲染副作用更新
        triggerRefValue(this)
      }
    }
  })

  return {
    get value() {
      if (dirty) {
        value = runner()
        dirty = false
      }
      // 让当前 activeEffect 追踪此 computed
      trackRefValue(this)
      return value
    }
  }
}

四、副作用调度与批更新

Vue 3 的组件更新是异步批处理的:

const queue = new Set()
let isFlushing = false

function queueJob(job) {
  queue.add(job)  // Set 自动去重,同一组件多次修改只更新一次

  if (!isFlushing) {
    isFlushing = true
    Promise.resolve().then(flushJobs)
  }
}

function flushJobs() {
  // 按组件 id 排序,父组件先更新
  const jobs = [...queue].sort((a, b) => a.id - b.id)
  queue.clear()
  isFlushing = false

  jobs.forEach(job => job())
}

这就是为什么修改多次数据只触发一次渲染:

import { ref, nextTick } from 'vue'

const count = ref(0)

// 触发 3 次修改,但只渲染 1 次
count.value++
count.value++
count.value++

// DOM 更新是异步的
console.log(document.querySelector('#count').textContent)  // 还是旧值

nextTick(() => {
  console.log(document.querySelector('#count').textContent)  // "3"
})

五、Vue 3 编译优化

静态提升(hoistStatic)

// 模板中的静态节点会被提升到 render 函数外部
// 只创建一次,重复渲染时直接复用

// 编译前
// <div>
//   <span class="static">静态内容</span>
//   <span>{{ dynamic }}</span>
// </div>

// 编译后
const _hoisted_1 = createVNode("span", { class: "static" }, "静态内容")

function render() {
  return createVNode("div", null, [
    _hoisted_1,  // 直接复用
    createVNode("span", null, toDisplayString(dynamic))
  ])
}

Patch Flags(补丁标记)

// 动态节点带上标记,diff 时只对比标记的部分
createVNode("span", null, text, 1 /* TEXT */)
createVNode("span", { class: dynamicClass }, null, 2 /* CLASS */)
createVNode("span", { id: dynamicId }, null, 4 /* PROPS */, ["id"])

// PatchFlag 枚举:
// TEXT = 1          只有文本变化
// CLASS = 2         只有 class 变化
// STYLE = 4         只有 style 变化
// PROPS = 8         只有 props 变化
// FULL_PROPS = 16   props 有动态 key
// NEED_HYDRATION = 32
// STABLE_FRAGMENT = 64
// KEYED_FRAGMENT = 128  带 key 的 fragment
// ...

Block Tree(块树)

// 动态节点被收集到 block 中,diff 时跳过静态内容
function render() {
  return (openBlock(), createElementBlock("div", null, [
    // 大量静态节点...
    createVNode("span", null, dynamic, 1 /* TEXT */)
    // 这个动态节点会被"block"追踪
    // 更新时直接定位,不需要遍历整棵树
  ]))
}

六、调试响应式

onTrack 与 onTrigger

// 仅开发模式可用
const state = reactive({ count: 0 }, {
  onTrack(event) {
    // 依赖被追踪时
    console.log('track:', event.key, event.target)
  },
  onTrigger(event) {
    // 依赖变化触发时
    console.log('trigger:', event.key, event.type, event.newValue)
  }
})

watch(source, callback, {
  onTrack(e) { debugger },
  onTrigger(e) { debugger }
})

常见响应式陷阱

// 1. 解构 reactive 丢失响应式
const state = reactive({ count: 0 })
let { count } = state         // ❌ count 是普通值
const { count } = toRefs(state) // ✅ 保持响应式

// 2. ref 在模板中自动解包,在 JS 中需要 .value
const count = ref(0)
console.log(count)      // Ref 对象
console.log(count.value) // 0

// 3. reactive 被整体替换
let state = reactive({ list: [] })
state = reactive({ list: [1] })  // ❌ 旧引用的依赖全部失效
// ✅ 用 ref 包裹,或只修改属性

// 4. markRaw 跳过转换
const state = reactive({
  chart: markRaw(new ECharts())  // ✅ 第三方实例不代理
})

七、小结

Vue 3 响应式系统的核心是 Proxy 拦截 + WeakMap 存储依赖 + 副作用调度。理解 track/trigger 机制、惰性深层代理和异步批更新,不仅能帮你避开响应式陷阱,也是进阶 Vue 源码的第一步。手写迷你版响应式系统是最好的学习方式。


本文由 inspirecl.asia 原创,转载请注明出处。
0

评论 (0)

取消
0:00