# 响应式系统：ref 与 Reactive 之道


## 概述

Vue 3 最核心的能力是**响应式系统**。

简单说就是：你改了数据，页面自动跟着变。你不用手动操作 DOM。

在 Vue 3 中，我们使用**组合式 API（Composition API）** 来编写组件逻辑。

## `setup` 函数

`<script setup>` 是 Vue 3 提供的一种简写方式。完整的写法是这样的：

```vue
<script lang="ts">
export default {
  setup() {
    const count = 0
    return { count }
  },
}
</script>
```

`setup` 函数返回的对象，里面的属性可以在模板中直接使用。

使用 `<script setup>` 可以简写为：

```vue
<script setup lang="ts">
const count = 0
</script>

<template>
  {{ count }}
</template>
```

| 写法 | 优点 | 缺点 |
|------|------|------|
| 完整写法 | 结构清晰，可读性强 | 代码冗长 |
| `<script setup>` | 简洁，少写样板代码 | — |

日常开发都用 `<script setup>`，这也是官方推荐的方式。

## `ref` 函数

上面的 `count` 只是一个普通变量，改了它页面不会更新。

要让变量变成"响应式"的，需要用 `ref` 包裹：

```vue
<script setup lang="ts">
import { ref } from 'vue'

const count = ref(0)
</script>

<template>
  {{ count }}
</template>
```

`ref` 会创建一个 `RefImpl` 对象，它的 `value` 属性存的就是真正的值。

在 `<script>` 中读取或修改时，需要 `.value`：

```typescript
const count = ref(0)
console.log(count.value) // 0
count.value = 1
```

在 `<template>` 中不需要 `.value`，Vue 会自动解包：

```vue
<template>
  <p>{{ count }}</p>  <!-- 不需要写 count.value -->
</template>
```

### 工作原理

`ref` 创建的对象有 `getter` 和 `setter` 方法：

- 读取时，`getter` 会记录"谁在用这个数据"
- 修改时，`setter` 通知所有用到这个数据的地方重新渲染

这样数据一变，页面就自动更新了。

> [!info] 说明
> `ref` 可以包裹任意类型的数据，包括数字、字符串、对象、数组。

## `reactive` 函数

对于对象类型的数据，还可以使用 `reactive`：

```vue
<script setup lang="ts">
import { reactive } from 'vue'

const person = reactive({
  name: '张三',
  age: 18,
  skills: ['Vue', 'TypeScript'],
})
</script>

<template>
  <p>姓名：{{ person.name }}</p>
  <p>年龄：{{ person.age }}</p>
</template>
```

`reactive` 创建的是 `Proxy` 对象。在 `<script>` 中读取和修改都不需要 `.value`：

```typescript
const person = reactive({ name: '张三' })
console.log(person.name)   // 不需要 .value
person.name = '李四'
```

`reactive` 的响应式是深层的，修改嵌套对象也会触发更新。

### `ref` vs `reactive`

| 对比项          | `ref`    | `reactive` |
| ------------ | -------- | ---------- |
| 支持的数据类型      | 全部类型     | 仅对象类型      |
| script 中取值   | `.value` | 直接读取       |
| template 中取值 | 自动解包     | 直接读取       |
| 深层响应式        | 是        | 是          |

> [!tip] 技巧
> 一个常见的做法是全部用 `ref`。这样写法一致，不需要纠结用哪个。`ref` 包裹对象时，内部会自动转成 `Proxy`，响应式效果一样。

## `toRefs` 和 `toRef`

解构响应式对象时，会丢失响应式连接：

```typescript
const person = reactive({ name: '张三', age: 18 })
let { name, age } = person
name = '李四'   // person.name 不会变
```

`toRefs` 可以把每个属性转成独立的 ref，保持连接：

```typescript
import { toRefs } from 'vue'

const person = reactive({ name: '张三', age: 18 })
const { name, age } = toRefs(person)

name.value = '李四'   // person.name 也变了
```

如果 `person` 是 `ref` 创建的，需要先取 `.value`：

```typescript
const person = ref({ name: '张三', age: 18 })
const { name, age } = toRefs(person.value)
```

`toRef` 只解包某一个属性：

```typescript
const name = toRef(person, 'name')
```

## 计算属性

如果一个值需要从其他值计算得到，可以用 `computed`：

```vue
<script setup lang="ts">
import { ref, computed } from 'vue'

const firstName = ref('张')
const lastName = ref('三')

const fullName = computed(() => {
  return firstName.value + ' ' + lastName.value
})
</script>

<template>
  <p>{{ fullName }}</p>
</template>
```

`computed` 有两个重要特性：

**缓存**：只有当依赖的值变化时，才会重新计算。多次读取直接返回上次结果。

```typescript
const fullName = computed(() => {
  console.log('计算了') // 只输出一次
  return firstName.value + ' ' + lastName.value
})
```

**可写**：可以设置 `setter`，但用得少：

```typescript
const fullName = computed({
  get() {
    return firstName.value + ' ' + lastName.value
  },
  set(newValue: string) {
    const [first = '', last = ''] = newValue.split(' ')
    firstName.value = first
    lastName.value = last
  },
})
```

## 侦听器

### `watch`

`watch` 监听一个数据的变化，变化时执行回调：

```vue
<script setup lang="ts">
import { ref, watch } from 'vue'

const count = ref(0)

watch(count, (newValue, oldValue) => {
  console.log(`从 ${oldValue} 变为 ${newValue}`)
})
</script>

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

`watch` 的常见配置选项：

| 选项 | 作用 |
|------|------|
| `{ deep: true }` | 深层监听对象内部的变化 |
| `{ immediate: true }` | 创建时立即执行一次回调 |
| `{ once: true }` | 只触发一次 |

**深层监听**：当监听的是一个 ref 包裹的对象时，内部属性变化需要用 `deep`：

```typescript
const person = ref({ name: '张三', age: 18 })
watch(person, (value) => {
  console.log('person 变了', value)
}, { deep: true })
```

> [!warning] 注意
> reactive 创建的对象默认就是深层监听，不需要 `deep`。

**监听单个属性**：用 `getter` 函数：

```typescript
watch(
  () => person.value.name,
  (newValue) => {
    console.log('name 变了', newValue)
  }
)
```

这样可以只监听 `name` 的变化，而不关心 `age`。

### `watchEffect`

`watchEffect` 是 `watch` 的简化版。它会自动追踪内部用到的响应式数据，数据变化时重新执行：

```typescript
import { watchEffect } from 'vue'

watchEffect(() => {
  console.log(`姓名：${person.value.name}，年龄：${person.value.age}`)
})
```

不用声明要监听谁，它自己会分析依赖。

| 对比   | `watch`               | `watchEffect` |
| ---- | --------------------- | ------------- |
| 声明方式 | 明确指定监听的数据             | 自动追踪依赖        |
| 初始执行 | 默认不执行（可加 `immediate`） | 立即执行          |
| 适用场景 | 需要新旧值对比，或精确控制         | 简单副作用，不需要旧值   |

## 生命周期钩子

组件从创建到销毁，会经历一系列阶段。Vue 3 提供了对应的钩子函数。

### 完整流程

```
setup → onBeforeMount → onMounted → onBeforeUpdate → onUpdated → onBeforeUnmount → onUnmounted
```

### `onMounted` — 组件挂载后

组件已经渲染到 DOM 中，可以操作 DOM 元素、调用 API。

```vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'

const count = ref(0)

onMounted(() => {
  console.log('组件已经渲染完成')
  // 可以在这里调接口获取数据
  fetchData()
})

function fetchData() {
  // 调用后端 API
}
</script>
```

### `onUnmounted` — 组件卸载后

组件被移除，用来清理定时器、取消订阅。

```vue
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'

const count = ref(0)
let timer: number | undefined

onMounted(() => {
  timer = setInterval(() => {
    count.value++
  }, 1000)
})

onUnmounted(() => {
  clearInterval(timer) // 清理定时器
})
</script>
```

### `onBeforeUpdate` 与 `onUpdated`

数据变化导致 DOM 重新渲染时触发：

```vue
<script setup lang="ts">
import { ref, onBeforeUpdate, onUpdated } from 'vue'

const count = ref(0)

onBeforeUpdate(() => {
  console.log('DOM 即将更新', count.value)
})

onUpdated(() => {
  console.log('DOM 已更新')
})
</script>
```

> [!warning] 注意
> 不要在 `onUpdated` 中修改响应式数据，会触发无限循环。

### 生命周期钩子汇总

| 钩子 | 触发时机 | 常见用途 |
|------|----------|----------|
| `onBeforeMount` | DOM 渲染前 | 准备工作 |
| `onMounted` | DOM 渲染后 | 调接口、操作 DOM |
| `onBeforeUpdate` | 数据变化，DOM 更新前 | 获取更新前的 DOM 状态 |
| `onUpdated` | DOM 更新后 | 执行依赖新 DOM 的操作 |
| `onBeforeUnmount` | 组件卸载前 | 保存临时数据 |
| `onUnmounted` | 组件卸载后 | 清理定时器、取消监听 |

---

响应式系统是 Vue 3 的基石。你可以用 `ref` 和 `reactive` 定义数据，用 `computed` 派生数据，用 `watch` 监听变化。生命周期钩子则让你在合适的时机做合适的事。

数据准备好了，下一步是：**怎么把数据显示到页面上，以及怎么处理用户的操作？**


---

> 作者: Aphros  
> URL: https://blog.papergate.top/posts/%E5%93%8D%E5%BA%94%E5%BC%8F%E7%B3%BB%E7%BB%9Fref-%E4%B8%8E-reactive-%E4%B9%8B%E9%81%93/  

