响应式系统:ref 与 Reactive 之道

第 1 节 概述

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

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

在 Vue 3 中,我们使用组合式 API(Composition API) 来编写组件逻辑。

第 2 节 setup 函数

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

1
2
3
4
5
6
7
8
<script lang="ts">
export default {
  setup() {
    const count = 0
    return { count }
  },
}
</script>

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

使用 <script setup> 可以简写为:

1
2
3
4
5
6
7
<script setup lang="ts">
const count = 0
</script>

<template>
  {{ count }}
</template>
写法优点缺点
完整写法结构清晰,可读性强代码冗长
<script setup>简洁,少写样板代码

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

第 3 节 ref 函数

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

要让变量变成"响应式"的,需要用 ref 包裹:

1
2
3
4
5
6
7
8
9
<script setup lang="ts">
import { ref } from 'vue'

const count = ref(0)
</script>

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

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

<script> 中读取或修改时,需要 .value

1
2
3
const count = ref(0)
console.log(count.value) // 0
count.value = 1

<template> 中不需要 .value,Vue 会自动解包:

1
2
3
<template>
  <p>{{ count }}</p>  <!-- 不需要写 count.value -->
</template>

3.1 工作原理

ref 创建的对象有 gettersetter 方法:

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

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

说明

ref 可以包裹任意类型的数据,包括数字、字符串、对象、数组。

第 4 节 reactive 函数

对于对象类型的数据,还可以使用 reactive

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<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

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

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

4.1 ref vs reactive

对比项refreactive
支持的数据类型全部类型仅对象类型
script 中取值.value直接读取
template 中取值自动解包直接读取
深层响应式
技巧

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

第 5 节 toRefstoRef

解构响应式对象时,会丢失响应式连接:

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

toRefs 可以把每个属性转成独立的 ref,保持连接:

1
2
3
4
5
6
import { toRefs } from 'vue'

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

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

如果 personref 创建的,需要先取 .value

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

toRef 只解包某一个属性:

1
const name = toRef(person, 'name')

第 6 节 计算属性

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<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 有两个重要特性:

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

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

可写:可以设置 setter,但用得少:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const fullName = computed({
  get() {
    return firstName.value + ' ' + lastName.value
  },
  set(newValue: string) {
    const [first = '', last = ''] = newValue.split(' ')
    firstName.value = first
    lastName.value = last
  },
})

第 7 节 侦听器

7.1 watch

watch 监听一个数据的变化,变化时执行回调:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<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

1
2
3
4
const person = ref({ name: '张三', age: 18 })
watch(person, (value) => {
  console.log('person 变了', value)
}, { deep: true })
注意

reactive 创建的对象默认就是深层监听,不需要 deep

监听单个属性:用 getter 函数:

1
2
3
4
5
6
watch(
  () => person.value.name,
  (newValue) => {
    console.log('name 变了', newValue)
  }
)

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

7.2 watchEffect

watchEffectwatch 的简化版。它会自动追踪内部用到的响应式数据,数据变化时重新执行:

1
2
3
4
5
import { watchEffect } from 'vue'

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

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

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

第 8 节 生命周期钩子

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

8.1 完整流程

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

8.2 onMounted — 组件挂载后

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<script setup lang="ts">
import { ref, onMounted } from 'vue'

const count = ref(0)

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

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

8.3 onUnmounted — 组件卸载后

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<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>

8.4 onBeforeUpdateonUpdated

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<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>
注意

不要在 onUpdated 中修改响应式数据,会触发无限循环。

8.5 生命周期钩子汇总

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

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

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