Skip to content

Vue 3 响应式原理

Vue 3 的响应式系统基于 ES6 Proxy,相比 Vue 2 的 Object.defineProperty,能拦截更多操作,且无需预先声明属性。

核心:Proxy + 依赖追踪

js
// Vue 3 响应式的最小实现
let activeEffect = null  // 当前正在执行的副作用

// 依赖收集容器:WeakMap<target, Map<key, Set<effect>>>
const targetMap = new WeakMap()

function track(target, key) {
  if (!activeEffect) return
  let depsMap = targetMap.get(target)
  if (!depsMap) targetMap.set(target, (depsMap = new Map()))
  let deps = depsMap.get(key)
  if (!deps) depsMap.set(key, (deps = new Set()))
  deps.add(activeEffect)
}

function trigger(target, key) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  const deps = depsMap.get(key)
  deps?.forEach(effect => effect())
}

function reactive(raw) {
  return new Proxy(raw, {
    get(target, key, receiver) {
      const result = Reflect.get(target, key, receiver)
      track(target, key)  // 收集依赖
      return result
    },
    set(target, key, value, receiver) {
      const result = Reflect.set(target, key, value, receiver)
      trigger(target, key)  // 触发更新
      return result
    }
  })
}

function effect(fn) {
  const effectFn = () => {
    activeEffect = effectFn
    fn()  // 执行时会触发 getter,自动收集依赖
    activeEffect = null
  }
  effectFn()
  return effectFn
}

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

effect(() => {
  console.log(`count is ${state.count}`)  // 自动追踪 count
})

state.count++  // 触发上面的 effect → 'count is 1'

ref vs reactive

ts
import { ref, reactive, computed, watch, watchEffect } from 'vue'

// ref:包装任意值(基本类型必须用 ref)
const count = ref(0)
count.value++  // 通过 .value 访问
// 模板中自动解包,不需要 .value

// reactive:包装对象(深度响应式)
const state = reactive({
  user: { name: 'Alice', age: 30 },
  posts: []
})
state.user.name = 'Bob'  // 深度响应,直接修改

// ⚠️ reactive 的限制
const { user } = state  // ❌ 解构后失去响应性
const { user } = toRefs(state)  // ✅ toRefs 保持响应性

// ref 的底层:对象类型也用 Proxy
const obj = ref({ count: 0 })
// obj.value 是一个 reactive 对象

computed

ts
const firstName = ref('John')
const lastName = ref('Doe')

// 只读 computed
const fullName = computed(() => `${firstName.value} ${lastName.value}`)

// 可写 computed
const fullNameWritable = computed({
  get: () => `${firstName.value} ${lastName.value}`,
  set: (val) => {
    const [first, last] = val.split(' ')
    firstName.value = first
    lastName.value = last
  }
})

fullNameWritable.value = 'Jane Smith'
// firstName.value → 'Jane', lastName.value → 'Smith'

// computed 是惰性的:只有被访问时才计算,且会缓存结果
// 只有依赖变化时才重新计算

watch & watchEffect

ts
const count = ref(0)
const user = reactive({ name: 'Alice', age: 30 })

// watch:明确指定监听源,懒执行(默认不立即执行)
watch(count, (newVal, oldVal) => {
  console.log(`count: ${oldVal}${newVal}`)
})

// 监听多个源
watch([count, () => user.name], ([newCount, newName], [oldCount, oldName]) => {
  console.log('变化了')
})

// 深度监听对象
watch(user, (newUser) => {
  console.log('user 变化:', newUser)
}, { deep: true })

// 立即执行
watch(count, (val) => { /* ... */ }, { immediate: true })

// watchEffect:自动追踪依赖,立即执行
const stop = watchEffect(() => {
  // 自动追踪 count.value 和 user.name
  console.log(`${user.name}: ${count.value}`)
})

// 停止监听
stop()

// watchEffect 的清理
watchEffect((onCleanup) => {
  const timer = setInterval(() => { /* ... */ }, 1000)
  onCleanup(() => clearInterval(timer))  // 下次执行前或停止时清理
})

Vue 2 vs Vue 3 响应式对比

js
// Vue 2:Object.defineProperty 的局限
const vm = new Vue({
  data: { user: { name: 'Alice' } }
})

// ❌ 无法检测属性添加
vm.user.age = 30  // 不是响应式的!
Vue.set(vm.user, 'age', 30)  // 必须用 Vue.set

// ❌ 无法检测数组索引赋值
vm.list[0] = 'new'  // 不是响应式的!
vm.$set(vm.list, 0, 'new')  // 必须用 $set

// Vue 3:Proxy 完全解决这些问题
const state = reactive({ user: { name: 'Alice' } })

state.user.age = 30  // ✅ 自动响应式
state.list = []
state.list[0] = 'new'  // ✅ 自动响应式
delete state.user.name  // ✅ 删除也能检测

响应式工具函数

ts
import { isRef, isReactive, isProxy, toRaw, markRaw, shallowRef, shallowReactive } from 'vue'

// 检测
isRef(count)        // true
isReactive(state)   // true
isProxy(state)      // true

// toRaw:获取原始对象(跳过响应式,用于性能敏感操作)
const raw = toRaw(state)

// markRaw:标记对象永远不转为响应式
const chart = markRaw(new ECharts())  // 第三方库实例不需要响应式

// shallowRef / shallowReactive:浅层响应式(性能优化)
const shallowState = shallowReactive({
  nested: { count: 0 }  // nested 内部不是响应式的
})

实战:自定义响应式 Hook

ts
// useMouse:追踪鼠标位置
function useMouse() {
  const x = ref(0)
  const y = ref(0)

  function update(e: MouseEvent) {
    x.value = e.clientX
    y.value = e.clientY
  }

  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  return { x, y }
}

// useAsync:异步数据获取
function useAsync<T>(fn: () => Promise<T>) {
  const data = ref<T | null>(null)
  const loading = ref(true)
  const error = ref<Error | null>(null)

  fn()
    .then(result => { data.value = result })
    .catch(err => { error.value = err })
    .finally(() => { loading.value = false })

  return { data, loading, error }
}

总结

  • Vue 3 响应式基于 Proxy,能拦截属性增删和数组索引操作
  • ref 用于基本类型,reactive 用于对象(解构用 toRefs
  • computed 是惰性缓存的,watchEffect 自动追踪依赖
  • markRawshallowReactive 用于性能优化
  • 响应式系统与组件解耦,可以在任何地方使用

系统学习 Web 前端生态,深入底层架构