Skip to content

响应式原理

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 应用!

基于 Vue.js 官方文档构建的学习宝典