Vue3 响应式原理(Proxy 与 ref/reactive)
响应式几乎是 Vue 面的必答题。Vue3 换成 Proxy 之后,和 Vue2 的
defineProperty对比、以及ref和reactive什么时候用,建议一块准备。下面按「原理 → 运行时行为 → 手写迷你版 → 项目注意点」记。
题目
Vue3 的响应式原理?
核心思路:拦截 + 依赖收集 + 触发更新
访问响应式数据时 收集 当前正在执行的副作用(组件 render、watch、computed);数据变更时 通知 这些副作用重新跑。
Vue3 用 Proxy 代理目标对象,在 get / set / deleteProperty 等 trap 里完成收集和派发。底层实现主要在 @vue/reactivity 包,对外暴露 reactive、ref、computed、effect 等。
reactive:对象用代理
import { reactive, effect } from 'vue'
const state = reactive({ count: 0, nested: { ok: true } })
effect(() => {
console.log('count =', state.count)
})
state.count++ // 触发 effect
state.nested.ok = false // 深层也是响应式(递归代理)特点:
- 只能用于 对象类型(对象、数组、Map、Set 等)
- 返回的是 Proxy,不要解构赋值丢响应式(
const { count } = state后 count 是普通值)- 用
toRefs可以把 reactive 里的字段拆成 ref 再解构
import { reactive, toRefs } from 'vue'
const state = reactive({ a: 1, b: 2 })
const { a, b } = toRefs(state) // a、b 是 ref,.value 读写ref:任何类型都能包一层
基本类型没有「属性」可代理,所以用 ref 包在 .value 里,内部一样走依赖收集。
import { ref, effect } from 'vue'
const n = ref(0)
const list = ref([1, 2, 3])
effect(() => {
console.log(n.value)
})
n.value++
list.value.push(4)模板里 ref 自动解包,不用写 .value:
<script setup>
import { ref } from 'vue'
const msg = ref('hello')
</script>
<template>{{ msg }}</template>和 Vue2 defineProperty 对比(口述)
| Vue2 | Vue3 | |
|---|---|---|
| 手段 | 遍历 key,get/set | Proxy 拦截整对象 |
| 新增属性 | 需 $set | 直接加,可响应 |
| 数组下标 | 需特殊处理 | 原生支持 |
| 性能 | 初始化递归遍历成本高 | 惰性访问时才深层代理 |
| 兼容 | 支持 IE9+(配合 polyfill) | 不支持 IE |
依赖收集简化理解:effect 与 track/trigger
面试若追问源码,可以说清三句话:
effect(fn)执行fn时把当前activeEffect指向自己;- 读数据 →
track(目标, key)把 effect 记到依赖桶里; - 写数据 →
trigger(目标, key)把桶里的 effect 拿出来再执行。
下面是一个极简示意(不是 Vue 源码,帮助记忆):
let activeEffect = null
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 dep = depsMap.get(key)
if (!dep) depsMap.set(key, (dep = new Set()))
dep.add(activeEffect)
}
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
dep?.forEach((effect) => effect())
}
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
track(target, key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
const ok = Reflect.set(target, key, value, receiver)
trigger(target, key)
return ok
}
})
}
function effect(fn) {
activeEffect = fn
fn()
activeEffect = null
}computed、watch 和响应式的关系
- computed:内部也是 effect,有缓存,依赖不变不会重算。
- watch:监听某个 ref/reactive 源,异步调度(
flush: 'post'可在 DOM 更新后执行)。
<script setup>
import { ref, computed, watch } from 'vue'
const price = ref(10)
const qty = ref(2)
const total = computed(() => price.value * qty.value)
watch(total, (v) => {
console.log('合计变了', v)
})
</script>项目里的实用注意点
- 大对象、表单模型若整包
reactive,调试时不如拆开ref直观reactive赋值整个对象会丢代理:state = reactive({})要改属性或Object.assign- 用
markRaw/shallowReactive可以关掉深层响应(第三方实例、地图 SDK 等)- 异步改数据后组件仍更新,是因为调度器把 component effect 排队进微任务
import { shallowReactive, markRaw } from 'vue'
const state = shallowReactive({
chart: markRaw(echartsInstance) // 图表实例别代理
})面试怎么收尾
先答 Proxy + track/trigger + effect,再区分 ref 与 reactive 使用场景,最后对比 Vue2 defineProperty 的局限。若能提到 编译时优化(PatchFlag) 和运行时响应式是两条线,加分。
写在最后
手写迷你 Proxy 建议在白板练一遍,比背概念稳。真写业务时,console.log(toRaw(state)) 查原始对象、isReactive 排查有时比瞎猜快。
创作不易,转载请注明出处和作者。
