项目文件夹

文件
2026-07-13 21:37:07 +08:00

6.0 KiB

name, description
name description
Vue Reactivity System 核心响应式原语 - ref、reactive、computed 与侦听器

Vue 响应式系统

Vue 的响应式系统能够自动追踪状态变更并更新 DOM。

ref()

使用 ref() 创建响应式原始值。在 JavaScript 中通过 .value 访问/修改,在模板中自动解包。

import { ref } from 'vue'

const count = ref(0)
console.log(count.value) // 0
count.value++
<script setup lang="ts">
import { ref } from 'vue'

const count = ref(0)

function increment() {
  count.value++
}
</script>

<template>
  <button @click="increment">{{ count }}</button>
</template>

为 ref 添加类型

import { ref } from 'vue'
import type { Ref } from 'vue'

// 类型推断
const year = ref(2020) // Ref<number>

// 显式泛型
const name = ref<string | null>(null)

// Ref 类型注解
const id: Ref<string | number> = ref('abc')

reactive()

创建响应式对象。无需使用 .value,但无法重新赋值整个对象。

import { reactive } from 'vue'

interface State {
  count: number
  name: string
}

const state: State = reactive({
  count: 0,
  name: 'Vue'
})

state.count++ // 响应式

reactive() 的局限性

  1. 仅适用于对象——不适用于原始值
  2. 无法替换整个对象——会丢失响应性
  3. 解构会丢失响应性——应使用 toRefs()
const state = reactive({ count: 0 })

// ❌ 丢失响应性
let { count } = state

// ✅ 使用 toRefs
import { toRefs } from 'vue'
const { count } = toRefs(state)

建议

使用 ref() 作为声明响应式状态的首选 API——它适用于任何值类型且行为一致。

深层响应性

ref()reactive() 默认都是深层响应式的:

const obj = ref({
  nested: { count: 0 },
  arr: ['foo', 'bar']
})

// 以下操作会触发更新
obj.value.nested.count++
obj.value.arr.push('baz')

使用 shallowRef()shallowReactive() 可退出深层响应式以提升性能。

DOM 更新时机

DOM 更新是批量且异步进行的。使用 nextTick() 等待更新完成:

import { ref, nextTick } from 'vue'

const count = ref(0)

async function increment() {
  count.value++
  await nextTick()
  // 此时 DOM 已更新
}

Ref 解包规则

  • 在模板中:顶层 ref 自动解包
  • 在响应式对象中:作为属性访问时 ref 自动解包
  • 在数组/集合中ref 不会自动解包
const count = ref(0)
const state = reactive({ count })

console.log(state.count) // 0(已解包)

const books = reactive([ref('Vue Guide')])
console.log(books[0].value) // 需要 .value

computed()

从响应式状态派生值,并自动缓存。仅当依赖项发生变化时才重新求值。

import { ref, computed } from 'vue'

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

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

// 可写计算属性
const fullNameWritable = computed({
  get() {
    return `${firstName.value} ${lastName.value}`
  },
  set(newValue: string) {
    [firstName.value, lastName.value] = newValue.split(' ')
  }
})

计算属性最佳实践

  • Getter 应保持纯净——无副作用,不修改其他状态
  • 不要直接修改计算属性——应修改其源数据
  • 对于派生数据,优先使用计算属性而非方法(利用缓存优势)
// ✅ 已缓存——仅当 items 变化时重新计算
const activeItems = computed(() => items.value.filter(x => x.active))

// ❌ 未缓存——每次渲染都执行
function getActiveItems() {
  return items.value.filter(x => x.active)
}

watch()

显式侦听响应式源,在其发生变化时执行副作用。默认惰性求值。

import { ref, watch } from 'vue'

const id = ref(1)

watch(id, async (newId, oldId) => {
  const data = await fetchData(newId)
  // 处理数据...
})

侦听源类型

const x = ref(0)
const obj = reactive({ count: 0 })

// 单个 ref
watch(x, (newX) => console.log(newX))

// Getter 函数
watch(() => obj.count, (count) => console.log(count))

// 多个源
watch([x, () => obj.count], ([newX, newCount]) => {
  console.log(newX, newCount)
})

侦听选项

watch(source, callback, {
  immediate: true,  // 创建时立即执行
  deep: true,       // 侦听嵌套属性
  once: true,       // 仅触发一次(3.4+
  flush: 'post'     // DOM 更新后执行
})

watchEffect()

自动追踪依赖并立即执行。当任一被追踪的依赖发生变化时重新运行。

import { ref, watchEffect } from 'vue'

const todoId = ref(1)
const data = ref(null)

watchEffect(async () => {
  const response = await fetch(`/api/todos/${todoId.value}`)
  data.value = await response.json()
})

watch 与 watchEffect 对比

特性 watch watchEffect
依赖追踪 显式指定 自动追踪
惰性求值 否(立即执行)
访问旧值
适用场景 特定源 多个依赖

侦听器清理(3.5+

取消过期的异步操作:

import { watch, onWatcherCleanup } from 'vue'

watch(id, async (newId) => {
  const controller = new AbortController()

  fetch(`/api/${newId}`, { signal: controller.signal })

  onWatcherCleanup(() => controller.abort())
})

停止侦听器

const stop = watch(source, callback)
const stop2 = watchEffect(() => { /* ... */ })

// 手动停止
stop()
stop2()

// 暂停/恢复(3.5+
const { stop, pause, resume } = watchEffect(() => { /* ... */ })