2.1 响应式系统的本质
响应式系统的核心目标是:当数据变化时,自动更新所有依赖这个数据的地方(比如模板、计算属性、侦听器)。Vue 3 的响应式系统基于 ES6 Proxy,与 Vue 2 的 Object.defineProperty 相比有根本性的改进。
Vue 2 的缺陷(defineProperty)
- 无法检测属性新增和删除(需要
this.$set) - 无法检测数组下标赋值(
arr[0] = val无效) - 初始化时需要递归遍历整个对象树(性能开销)
- 必须预先声明所有响应式属性
Vue 3 的 Proxy 方案
- 拦截对象的所有操作:get/set/deleteProperty/has 等
- 天然支持属性新增和删除
- 惰性递归:只在访问嵌套属性时才创建响应式
- 原生支持数组所有变更操作
// Vue 3 响应式核心原理(简化版)
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key) // 收集依赖:谁在读取这个属性?
const value = Reflect.get(target, key)
// 惰性递归:如果是对象才继续包装
return typeof value === 'object' ? reactive(value) : value
},
set(target, key, value) {
const result = Reflect.set(target, key, value)
trigger(target, key) // 触发更新:通知所有依赖重新执行
return result
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)
trigger(target, key) // 删除属性也能触发更新
return result
}
})
}
2.2 ref:基本值的响应式包装
ref() 接受一个值并返回一个响应式、可变的 ref 对象,通过 .value 属性访问内部值。
JavaScript 的基本类型(string、number、boolean)是值传递,无法被 Proxy 拦截。ref 将基本值包装在一个对象的 .value 属性里,从而使其可被 Proxy 追踪。在模板中 Vue 会自动解包,不需要写 .value。
<script setup lang="ts">
import { ref } from 'vue'
// 基本类型 ref
const count = ref(0) // Ref<number>
const name = ref('') // Ref<string>
const isOpen = ref(false) // Ref<boolean>
// 对象类型 ref(内部会调用 reactive)
const user = ref({ name: 'Alice', age: 25 })
// 在 script 中必须写 .value
count.value++
user.value.name = 'Bob'
// 模板 DOM 引用
const inputRef = ref<HTMLInputElement | null>(null)
function focusInput() {
inputRef.value?.focus() // 可选链,挂载后才有值
}
</script>
<template>
<!-- 模板中自动解包,无需 .value -->
<p>{{ count }} — {{ user.name }}</p>
<input ref="inputRef" @click="focusInput" />
</template>
2.3 reactive:对象的响应式代理
reactive() 直接代理一个对象,访问属性时无需 .value。但它只能代理对象(包括数组、Map、Set),不能代理基本类型。
<script setup lang="ts">
import { reactive } from 'vue'
const state = reactive({
count: 0,
todos: [] as string[],
user: { name: 'Alice' }
})
// 直接访问属性,无需 .value
state.count++
state.todos.push('新任务')
state.user.name = 'Bob'
// ❌ 危险:解构会失去响应性!
const { count } = state // count 变成了普通数字,不再响应式
// ✅ 正确:使用 toRefs 解构
import { toRefs } from 'vue'
const { count: countRef } = toRefs(state) // 保持响应性
</script>
2.4 ref vs reactive:如何选择?
| 对比项 | ref | reactive |
|---|---|---|
| 适用类型 | 基本类型、对象、数组(通用) | 只能用于对象/数组/Map/Set |
| 访问方式 | 脚本中需 .value,模板自动解包 | 直接访问属性,无 .value |
| 解构 | 天然可以解构后传递(保持 ref 身份) | 解构会丢失响应性 |
| 替换整体 | 直接赋值 xxx.value = newVal 即可 | 不能直接 state = newObj,需 Object.assign |
| 官方推荐 | ✅ 官方推荐默认使用 ref | 需要时使用,可读性更自然 |
Vue 3.x 官方文档推荐始终优先使用 ref。ref 的 API 在任何情况下都有效,而 reactive 有诸多限制(不能包装基本类型、解构丢失响应性等)。仅在你明确希望使用对象风格、且不需要解构时,才使用 reactive。
2.5 computed:派生状态
computed() 接受一个 getter 函数,返回一个只读的、惰性求值的响应式引用。仅当依赖变化时才重新计算,并会缓存结果。
<script setup lang="ts">
import { ref, computed } from 'vue'
const items = ref([
{ id: 1, name: '苹果', done: false },
{ id: 2, name: '香蕉', done: true },
])
// 只读 computed(getter)
const doneItems = computed(() =>
items.value.filter(i => i.done)
)
// 可写 computed(getter + setter)
const firstName = ref('三')
const lastName = ref('张')
const fullName = computed({
get: () => `${lastName.value}${firstName.value}`,
set(val: string) {
// 写入时拆分
lastName.value = val[0]
firstName.value = val.slice(1)
}
})
fullName.value = '李四' // 触发 setter
</script>
computed 的 getter 应是纯函数,不要在里面发起网络请求、修改 DOM 或改变其他响应式状态。这些操作应放在 watch/watchEffect 中。
2.6 watch 与 watchEffect
侦听器用于在数据变化时执行副作用(发请求、操作 DOM、写日志等)。Vue 3 提供了两种侦听器:
<script setup lang="ts">
import { ref, watch, watchEffect } from 'vue'
const userId = ref(1)
const userData = ref(null)
// watchEffect:立即执行,自动追踪依赖
watchEffect(async () => {
// 访问了 userId.value → 自动追踪
const data = await fetchUser(userId.value)
userData.value = data
})
// watch:精确侦听,提供新旧值
watch(userId, async (newId, oldId) => {
console.log(`userId 从 ${oldId} 变为 ${newId}`)
userData.value = await fetchUser(newId)
})
// watch 多个数据源
const page = ref(1)
const filter = ref('')
watch([page, filter], ([newPage, newFilter]) => {
fetchList({ page: newPage, filter: newFilter })
})
// watch 深层对象(需要 deep 选项)
const form = ref({ name: '', email: '' })
watch(form, (newForm) => {
saveDraft(newForm)
}, { deep: true })
// 清理副作用(组件卸载或重新执行前调用)
watchEffect((onCleanup) => {
const timer = setInterval(() => console.log('tick'), 1000)
onCleanup(() => clearInterval(timer)) // 自动清理
})
</script>
2.7 toRef 与 toRefs
当需要将 reactive 对象的某个属性传递给其他地方,同时保持响应性时,使用 toRef / toRefs:
<script setup lang="ts">
import { reactive, toRef, toRefs } from 'vue'
const state = reactive({ count: 0, name: 'Vue' })
// toRef:提取单个属性为 ref(仍然关联原始对象)
const countRef = toRef(state, 'count')
countRef.value++ // state.count 也变为 1
state.count++ // countRef.value 也变为 2
// toRefs:将 reactive 对象的所有属性转为 refs
// 常用于从 composable 中返回响应式数据
const { count, name } = toRefs(state)
// 现在 count 和 name 都是 ref,可安全解构
</script>
2.8 响应式数组操作
Vue 3 对所有变更数组的方法(push、pop、splice 等)都能正确追踪。但有些模式需要注意:
<script setup>
import { ref } from 'vue'
const list = ref([1, 2, 3])
// ✅ 这些方法都会触发更新
list.value.push(4)
list.value.pop()
list.value.splice(0, 1, 99)
list.value.sort()
list.value.reverse()
// ✅ 下标赋值也可以(Vue 3 修复了 Vue 2 的缺陷)
list.value[0] = 100
// ✅ 替换整个数组
list.value = list.value.filter(n => n > 1)
// ❌ 注意:filter/map/reduce 返回新数组,不会修改原数组
// 必须重新赋值给 list.value 才能触发更新
</script>
2.9 shallowRef 与 shallowReactive
对于大型对象或不需要深度响应的场景,可以使用浅层响应式 API 提升性能:
import { shallowRef, shallowReactive, triggerRef } from 'vue'
// shallowRef:只有 .value 本身是响应式的
// 内层属性变化不会触发更新
const bigData = shallowRef({ items: [/* 10000 条数据 */] })
// 修改内层后需手动触发
bigData.value.items.push(newItem)
triggerRef(bigData) // 手动强制更新
// 或直接替换整个对象(推荐)
bigData.value = { items: [...bigData.value.items, newItem] }
Vue 3 的响应式系统在底层基于 effect(副作用)、track(依赖收集)、trigger(触发更新) 三个核心操作。computed 是一个特殊的 effect,watch/watchEffect 也是基于 effect 实现的。理解这个模型有助于排查响应性丢失的问题。