响应式原理
Vue 3 的响应式系统是其核心特性之一,它基于 ES6 的 Proxy 实现,提供了更强大和高效的响应式能力。
响应式基础
什么是响应式?
响应式是指当数据发生变化时,依赖这些数据的视图能够自动更新。Vue 的响应式系统让我们可以声明式地描述数据和视图之间的关系。
javascript
import { ref, reactive, computed, watchEffect } from 'vue'
// 基本响应式数据
const count = ref(0)
const state = reactive({ name: 'Vue', version: 3 })
// 计算属性
const doubleCount = computed(() => count.value * 2)
// 副作用函数
watchEffect(() => {
console.log(`Count: ${count.value}, Double: ${doubleCount.value}`)
})
// 修改数据会自动触发更新
count.value++ // 输出: Count: 1, Double: 2
Proxy vs Object.defineProperty
Vue 2 的限制 (Object.defineProperty)
javascript
// Vue 2 的响应式实现存在以下限制:
// 1. 无法检测对象属性的添加或删除
const obj = { a: 1 }
// obj.b = 2 // 无法检测到
// delete obj.a // 无法检测到
// 2. 无法检测数组索引和长度的变化
const arr = [1, 2, 3]
// arr[0] = 4 // 无法检测到
// arr.length = 0 // 无法检测到
// 3. 需要深度遍历对象的所有属性
// 性能开销较大
Vue 3 的改进 (Proxy)
javascript
// Vue 3 使用 Proxy 解决了这些问题:
// 1. 可以检测所有属性操作
const state = reactive({
a: 1,
nested: { b: 2 }
})
// 添加属性
state.c = 3 // ✅ 可以检测到
// 删除属性
delete state.a // ✅ 可以检测到
// 嵌套对象
state.nested.d = 4 // ✅ 可以检测到
// 2. 数组操作完全支持
const list = reactive([1, 2, 3])
list[0] = 4 // ✅ 可以检测到
list.push(5) // ✅ 可以检测到
list.length = 0 // ✅ 可以检测到
响应式 API 详解
ref()
ref()
接受一个内部值,返回一个响应式的、可更改的 ref 对象。
javascript
import { ref, isRef, unref, toRef } from 'vue'
// 基本用法
const count = ref(0)
console.log(count.value) // 0
count.value = 1
console.log(count.value) // 1
// 对象 ref
const objectRef = ref({ count: 0 })
objectRef.value.count = 1
// 检查是否为 ref
console.log(isRef(count)) // true
console.log(isRef(1)) // false
// 获取 ref 的值
console.log(unref(count)) // 1
console.log(unref(1)) // 1
// 从响应式对象创建 ref
const state = reactive({ count: 0 })
const countRef = toRef(state, 'count')
countRef.value = 1
console.log(state.count) // 1
reactive()
reactive()
返回一个对象的响应式代理。
javascript
import { reactive, isReactive, toRaw, markRaw } from 'vue'
// 基本用法
const state = reactive({
count: 0,
nested: {
value: 'hello'
}
})
// 深度响应式
state.nested.value = 'world' // 会触发更新
// 检查是否为响应式
console.log(isReactive(state)) // true
console.log(isReactive(state.nested)) // true
// 获取原始对象
const raw = toRaw(state)
console.log(raw === state) // false
// 标记对象为非响应式
const nonReactive = markRaw({ count: 0 })
const state2 = reactive({ obj: nonReactive })
console.log(isReactive(state2.obj)) // false
computed()
计算属性基于其响应式依赖进行缓存。
javascript
import { ref, computed } from 'vue'
const count = ref(1)
// 只读计算属性
const doubleCount = computed(() => count.value * 2)
console.log(doubleCount.value) // 2
// 可写计算属性
const fullName = computed({
get() {
return `${firstName.value} ${lastName.value}`
},
set(newValue) {
[firstName.value, lastName.value] = newValue.split(' ')
}
})
fullName.value = 'John Doe'
console.log(firstName.value) // John
console.log(lastName.value) // Doe
watch() 和 watchEffect()
javascript
import { ref, reactive, watch, watchEffect } from 'vue'
const count = ref(0)
const state = reactive({ name: 'Vue' })
// watch 单个源
watch(count, (newValue, oldValue) => {
console.log(`count: ${oldValue} -> ${newValue}`)
})
// watch 多个源
watch([count, () => state.name], ([newCount, newName], [oldCount, oldName]) => {
console.log(`count: ${oldCount} -> ${newCount}`)
console.log(`name: ${oldName} -> ${newName}`)
})
// watch 响应式对象
watch(state, (newValue, oldValue) => {
console.log('state changed:', newValue)
}, { deep: true })
// watchEffect 自动收集依赖
watchEffect(() => {
console.log(`count is ${count.value}`)
console.log(`name is ${state.name}`)
})
// 停止侦听器
const stop = watchEffect(() => {
console.log(`count: ${count.value}`)
})
// 稍后停止
stop()
响应式原理深入
依赖收集
javascript
// 简化的响应式实现原理
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)
if (dep) {
dep.forEach(effect => effect())
}
}
function reactive(target) {
return new Proxy(target, {
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) {
activeEffect = fn
fn()
activeEffect = null
}
响应式转换
javascript
import {
ref,
reactive,
readonly,
shallowRef,
shallowReactive,
shallowReadonly,
toRef,
toRefs,
toRaw,
markRaw
} from 'vue'
// 深度响应式
const deepState = reactive({
nested: { count: 0 }
})
// 浅层响应式
const shallowState = shallowReactive({
nested: { count: 0 }
})
// shallowState.nested = { count: 1 } // 会触发更新
// shallowState.nested.count = 1 // 不会触发更新
// 只读
const readonlyState = readonly(deepState)
// readonlyState.nested.count = 1 // 警告:无法修改只读属性
// 浅层只读
const shallowReadonlyState = shallowReadonly({
nested: { count: 0 }
})
// shallowReadonlyState.nested = {} // 警告
// shallowReadonlyState.nested.count = 1 // 允许
// 转换为 refs
const state = reactive({ count: 0, name: 'Vue' })
const { count, name } = toRefs(state)
// count 和 name 现在是 refs,与 state.count 和 state.name 保持同步
// 单个属性转 ref
const countRef = toRef(state, 'count')
性能优化
避免不必要的响应式转换
javascript
import { markRaw, shallowRef } from 'vue'
// 对于不需要响应式的大型对象
const largeObject = markRaw({
// 大量数据...
})
const state = reactive({
data: largeObject // 不会被转换为响应式
})
// 对于只需要替换整个对象的场景
const shallowData = shallowRef({
items: [/* 大量数据 */]
})
// 替换整个对象会触发更新
shallowData.value = { items: [/* 新数据 */] }
// 但修改内部属性不会
shallowData.value.items.push(newItem) // 不会触发更新
计算属性缓存
javascript
import { ref, computed } from 'vue'
const count = ref(0)
// 计算属性会缓存结果
const expensiveComputed = computed(() => {
console.log('计算中...') // 只在依赖变化时执行
return count.value * 2
})
// 多次访问不会重复计算
console.log(expensiveComputed.value) // 计算中... 0
console.log(expensiveComputed.value) // 0 (使用缓存)
count.value = 1
console.log(expensiveComputed.value) // 计算中... 2
调试响应式
调试钩子
javascript
import { computed, watchEffect } from 'vue'
const count = ref(0)
// 计算属性调试
const doubleCount = computed(() => count.value * 2, {
onTrack(e) {
console.log('追踪:', e)
},
onTrigger(e) {
console.log('触发:', e)
}
})
// watchEffect 调试
watchEffect(() => {
console.log(count.value)
}, {
onTrack(e) {
console.log('追踪:', e)
},
onTrigger(e) {
console.log('触发:', e)
}
})
Vue DevTools
Vue DevTools 提供了强大的响应式调试功能:
- 查看组件的响应式状态
- 追踪依赖关系
- 监控状态变化
- 时间旅行调试
最佳实践
1. 选择合适的响应式 API
javascript
// ✅ 基本类型使用 ref
const count = ref(0)
const message = ref('hello')
// ✅ 对象使用 reactive
const state = reactive({
user: { name: 'John', age: 30 },
settings: { theme: 'dark' }
})
// ❌ 避免对基本类型使用 reactive
// const count = reactive(0) // 错误!
2. 解构响应式对象
javascript
// ❌ 直接解构会失去响应性
const state = reactive({ count: 0, name: 'Vue' })
const { count, name } = state // 失去响应性
// ✅ 使用 toRefs
const { count, name } = toRefs(state)
// ✅ 或者使用 toRef
const count = toRef(state, 'count')
3. 避免创建不必要的响应式数据
javascript
// ❌ 常量不需要响应式
const API_URL = ref('https://api.example.com') // 不必要
// ✅ 直接使用常量
const API_URL = 'https://api.example.com'
// ❌ 大型静态数据不需要响应式
const largeStaticData = reactive({
// 大量静态配置数据
})
// ✅ 使用 markRaw 或直接使用普通对象
const largeStaticData = markRaw({
// 大量静态配置数据
})
4. 合理使用计算属性
javascript
// ✅ 用于派生状态
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
// ✅ 用于复杂计算
const filteredItems = computed(() => {
return items.value.filter(item => item.active)
})
// ❌ 避免在计算属性中产生副作用
const badComputed = computed(() => {
// 不要在这里修改其他状态
otherState.value = 'changed' // 错误!
return someValue.value * 2
})
常见问题
1. ref 的自动解包
javascript
const count = ref(0)
const state = reactive({
count // 在 reactive 中会自动解包
})
console.log(state.count) // 0,不需要 .value
state.count = 1 // 直接赋值
// 但在数组中不会自动解包
const list = reactive([count])
console.log(list[0].value) // 需要 .value
2. 响应式丢失
javascript
// ❌ 解构会丢失响应性
const state = reactive({ count: 0 })
let { count } = state
count++ // 不会触发更新
// ✅ 使用 toRefs
const { count } = toRefs(state)
count.value++ // 会触发更新
// ❌ 传递给函数会丢失响应性
function updateCount(count) {
count++ // 不会触发更新
}
updateCount(state.count)
// ✅ 传递整个对象或使用 ref
function updateCount(state) {
state.count++ // 会触发更新
}
updateCount(state)
3. 异步更新
javascript
import { nextTick } from 'vue'
const count = ref(0)
function increment() {
count.value++
// DOM 还没有更新
console.log(document.getElementById('count').textContent) // 旧值
// 等待 DOM 更新
nextTick(() => {
console.log(document.getElementById('count').textContent) // 新值
})
}
下一步
响应式系统是 Vue 的核心,理解其原理将帮助你写出更高效的 Vue 应用!