跳到主要内容

Vue ref 和 reactive

在 Vue 3 的组合式 API 中,refreactive 是两个最常用的响应式 API。它们是实现 Vue 响应式系统的基础工具。本章将详细介绍它们的用法、区别和最佳实践。

ref()

ref() 用于创建响应式数据,可以包装任意类型的值。

为什么需要 ref?

JavaScript 中没有办法拦截基本类型(如数字、字符串)的直接访问和修改。ref() 通过将值包装在一个对象中,利用 .value 属性的 getter 和 setter 来实现响应式追踪:

// 问题:基本类型的修改无法被拦截
let count = 0
count = 1 // 没有机制可以知道 count 被修改了

// 解决:包装成对象
const count = ref(0)
count.value = 1 // 可以在 setter 中拦截

基本用法

import { ref } from 'vue'

// 基本类型
const count = ref(0)
const name = ref('张三')
const isActive = ref(true)

// 读取值(需要 .value)
console.log(count.value) // 0
console.log(name.value) // '张三'
console.log(isActive.value) // true

// 修改值
count.value++
name.value = '李四'
isActive.value = false

ref 对象的结构

ref() 返回一个包含 .value 属性的对象:

const count = ref(0)

console.log(count) // { value: 0 }
console.log(count.value) // 0

// ref 对象的内部结构(简化)
// {
// __v_isRef: true, // 标记为 ref
// _rawValue: 0, // 原始值
// _value: 0, // 响应式值
// get value() { ... },
// set value(newValue) { ... }
// }

在模板中使用

在模板中使用时,Vue 会自动解包 ref,不需要 .value

<script setup>
import { ref } from 'vue'

const count = ref(0)

function increment() {
count.value++ // 在 JavaScript 中需要 .value
}
</script>

<template>
<!-- 自动解包,显示 0 -->
<p>{{ count }}</p>

<!-- 在表达式中也可以直接使用 -->
<p>{{ count + 1 }}</p>

<button @click="increment">增加</button>

<!-- 在事件处理中也可以直接修改 -->
<button @click="count++">直接增加</button>
</template>

ref 接收对象

ref() 接收对象类型时,它会自动使用 reactive() 将对象转为响应式:

import { ref } from 'vue'

const user = ref({
name: '张三',
age: 25
})

// 修改值需要 .value
user.value.name = '李四'
user.value.age = 30

// 替换整个对象
user.value = { name: '王五', age: 35 }

深层响应式

ref() 创建的是深层响应式,嵌套对象的修改也会被追踪:

import { ref } from 'vue'

const state = ref({
nested: {
count: 0
},
arr: [1, 2, 3]
})

// 深层修改也是响应式的
state.value.nested.count = 1
state.value.arr.push(4)

reactive()

reactive() 用于创建响应式的对象,它使对象本身变为响应式代理。

基本用法

import { reactive } from 'vue'

const state = reactive({
count: 0,
name: '张三',
isActive: true
})

// 直接访问属性,不需要 .value
console.log(state.count) // 0
console.log(state.name) // '张三'

// 修改属性
state.count++
state.name = '李四'

在模板中使用

<script setup>
import { reactive } from 'vue'

const state = reactive({
count: 0
})

function increment() {
state.count++ // 直接修改
}
</script>

<template>
<p>{{ state.count }}</p>
<button @click="increment">增加</button>
</template>

深层响应式

reactive() 默认创建深层响应式,嵌套对象也会被转为响应式:

import { reactive } from 'vue'

const state = reactive({
user: {
name: '张三',
address: {
city: '北京',
district: '朝阳'
}
},
hobbies: ['读书', '运动']
})

// 所有深层修改都是响应式的
state.user.address.city = '上海'
state.hobbies.push('编程')

// 添加新属性也会是响应式的
state.user.email = '[email protected]'

reactive 的局限性

reactive() 有几个重要的限制需要注意:

1. 只能用于对象类型

// ❌ 错误:不能用于基本类型
const count = reactive(0) // 警告,返回无效值
const name = reactive('张三') // 警告,返回无效值

// ✅ 正确:用于对象类型
const state = reactive({ count: 0 })
const list = reactive([1, 2, 3])
const map = reactive(new Map())
const set = reactive(new Set())

2. 不能替换整个对象

import { reactive } from 'vue'

let state = reactive({ count: 0 })

// ❌ 错误:替换会丢失响应式连接
state = reactive({ count: 1 })
// 原来的响应式对象被丢弃,所有依赖它的地方都不会更新

// ✅ 正确:修改属性
state.count = 1

// ✅ 正确:使用 Object.assign
Object.assign(state, { count: 1, name: '张三' })

3. 解构会丢失响应式

import { reactive } from 'vue'

const state = reactive({
name: '张三',
age: 25
})

// ❌ 错误:解构会丢失响应式
let { name, age } = state
name = '李四' // 只是修改了局部变量,不会触发更新

// ✅ 正确:使用 toRefs
import { toRefs } from 'vue'
const { name, age } = toRefs(state)
name.value = '李四' // 会触发更新

4. 传递属性到函数会丢失响应式

import { reactive } from 'vue'

const state = reactive({ count: 0 })

// ❌ 问题:函数接收的是普通数字,无法追踪变化
function useCount(count) {
// count 是普通数字,不是响应式的
}

useCount(state.count) // 传递的是值,不是引用

// ✅ 正确:传递整个对象
function useCount(state) {
// state 是响应式对象
}
useCount(state)

// ✅ 或者使用 getter 函数
useCount(() => state.count)

Reactive Proxy vs 原始对象

reactive() 返回的是原始对象的 Proxy,两者不相等:

import { reactive } from 'vue'

const raw = { count: 0 }
const proxy = reactive(raw)

console.log(proxy === raw) // false

// 只修改原始对象不会触发更新
raw.count = 1 // ❌ 不会触发更新

// 修改 Proxy 才会触发更新
proxy.count = 1 // ✅ 会触发更新

最佳实践:只使用 reactive() 返回的 Proxy,不要保留对原始对象的引用。

Proxy 的一致性保证

import { reactive } from 'vue'

const raw = { count: 0 }

// 对同一对象多次调用返回同一个 Proxy
const proxy1 = reactive(raw)
const proxy2 = reactive(raw)
console.log(proxy1 === proxy2) // true

// 对 Proxy 调用 reactive 返回自身
console.log(reactive(proxy1) === proxy1) // true

ref vs reactive 对比

特性refreactive
支持类型任意类型仅对象类型
访问方式需要 .value直接访问属性
模板解包自动解包不需要解包
替换整个值支持不支持
解构不适用(已是单值)需要 toRefs
重新赋值支持不支持
使用场景单一值、可能被替换的对象一组相关的属性

选择建议

// 场景1:基本类型 → 使用 ref
const count = ref(0)
const name = ref('张三')
const isActive = ref(true)

// 场景2:单一对象,可能被整体替换 → 使用 ref
const user = ref(null)
user.value = await fetchUser() // 方便替换

// 场景3:一组相关的属性 → 使用 reactive
const form = reactive({
username: '',
password: '',
remember: false
})

// 场景4:组件内部状态 → 根据需求选择
// 简单状态用 ref
const loading = ref(false)
// 复杂状态用 reactive
const state = reactive({
loading: false,
error: null,
data: []
})

官方推荐

根据 Vue 官方文档的建议:

  1. 推荐使用 ref() 作为声明响应式状态的主要方式
  2. 对于组件内部的状态,一组相关的属性可以使用 reactive()
  3. 在可复用的组合式函数中,推荐始终返回 ref() 以保持一致性和解构友好

toRef() 和 toRefs()

toRef()

toRef() 从响应式对象的单个属性创建 ref,保持响应式连接:

import { reactive, toRef } from 'vue'

const state = reactive({
name: '张三',
age: 25
})

// 为单个属性创建 ref
const name = toRef(state, 'name')

console.log(name.value) // '张三'

// 修改 ref 或原对象,另一个也会同步
state.name = '李四'
console.log(name.value) // '李四'

name.value = '王五'
console.log(state.name) // '王五'

toRefs()

toRefs() 将响应式对象转换为普通对象,每个属性都是 ref:

import { reactive, toRefs } from 'vue'

const state = reactive({
name: '张三',
age: 25,
city: '北京'
})

// 转换为普通对象,属性都是 ref
const { name, age, city } = toRefs(state)

console.log(name.value) // '张三'
console.log(age.value) // 25

// 修改会同步到原对象
name.value = '李四'
console.log(state.name) // '李四'

使用场景:组合式函数返回值

import { reactive, toRefs } from 'vue'

function useUser() {
const state = reactive({
name: '张三',
age: 25,
email: '[email protected]'
})

function updateName(newName) {
state.name = newName
}

// 返回时使用 toRefs,方便调用者解构
return {
...toRefs(state),
updateName
}
}
<script setup>
// 可以直接解构,保持响应式
const { name, age, email, updateName } = useUser()
</script>

<template>
<p>{{ name }} - {{ age }}</p>
<button @click="updateName('李四')">改名</button>
</template>

ref 在 reactive 对象中的解包

当 ref 作为 reactive 对象的属性时,会自动解包:

import { ref, reactive } from 'vue'

const count = ref(0)
const state = reactive({ count })

// 访问时自动解包,不需要 .value
console.log(state.count) // 0

// 修改时也自动解包
state.count = 1
console.log(count.value) // 1

// 注意:赋值新的 ref 会替换连接
const otherCount = ref(2)
state.count = otherCount
console.log(state.count) // 2
console.log(count.value) // 1(原来的 ref 不再关联)

数组和集合类型中的 ref 不会解包

import { ref, reactive } from 'vue'

// 数组中的 ref
const books = reactive([ref('Vue 3 Guide')])
// ❌ 需要 .value
console.log(books[0].value) // 'Vue 3 Guide'

// Map 中的 ref
const map = reactive(new Map([['count', ref(0)]]))
// ❌ 需要 .value
console.log(map.get('count').value) // 0

// Set 中的 ref
const set = reactive(new Set([ref('a')]))
set.forEach(item => console.log(item.value)) // 'a'

模板中的 ref 解包规则

在模板中使用 ref 时,只有顶层的 ref 属性才会自动解包:

<script setup>
import { ref } from 'vue'

const count = ref(0)
const object = { id: ref(1) }
</script>

<template>
<!-- ✅ 正确:顶层 ref 自动解包 -->
<p>{{ count }}</p>
<p>{{ count + 1 }}</p>

<!-- ⚠️ 注意:嵌套 ref 在表达式中不解包 -->
<p>{{ object.id + 1 }}</p>
<!-- 结果: "[object Object]1" -->

<!-- ✅ 但在文本插值中会解包 -->
<p>{{ object.id }}</p>
<!-- 结果: 1 -->
</template>

解决方案:解构到顶层

<script setup>
import { ref } from 'vue'

const object = { id: ref(1) }
const { id } = object // 解构到顶层
</script>

<template>
<p>{{ id + 1 }}</p> <!-- 2 -->
</template>

shallowRef() 和 shallowReactive()

shallowRef()

只追踪 .value 的变化,不追踪内部属性:

import { shallowRef, triggerRef } from 'vue'

const state = shallowRef({
nested: { count: 0 }
})

// ✅ 替换整个 value 会触发更新
state.value = { nested: { count: 1 } }

// ❌ 修改深层属性不会触发更新
state.value.nested.count = 2

// 可以手动触发更新
state.value.nested.count = 3
triggerRef(state) // 手动触发

shallowReactive()

只追踪顶层属性的变化:

import { shallowReactive } from 'vue'

const state = shallowReactive({
count: 0,
nested: {
value: 1
}
})

// ✅ 顶层属性变化是响应式的
state.count = 1

// ❌ 嵌套属性变化不是响应式的
state.nested.value = 2 // 不会触发更新

使用场景

import { shallowRef, markRaw } from 'vue'

// 1. 大型数据结构,避免深层响应式开销
const bigList = shallowRef([...])

// 2. 第三方库实例(通常不需要响应式)
import { Chart } from 'chart.js'
const chart = shallowRef(null)
chart.value = new Chart(ctx, config)

// 3. 与外部状态管理集成
const externalState = shallowRef(null)
externalState.value = externalLib.getState()

readonly() 和 shallowReadonly()

readonly()

readonly() 接收一个对象(无论是响应式的还是普通的)或 ref,返回一个只读的代理。这个代理是深层的,任何嵌套属性也都是只读的:

import { reactive, readonly, ref } from 'vue'

// 对 reactive 对象使用
const original = reactive({ count: 0 })
const copy = readonly(original)

// 对 ref 使用
const count = ref(0)
const readonlyCount = readonly(count)

// 对普通对象使用
const plain = { name: '张三' }
const readonlyPlain = readonly(plain)

// ❌ 尝试修改会失败并触发警告
copy.count++ // 警告: Set operation on key "count" failed
readonlyCount.value++ // 警告

响应式连接

readonly() 创建的只读代理与原始对象保持响应式连接。当原始响应式对象变化时,只读代理也会同步更新:

import { reactive, readonly, watchEffect } from 'vue'

const original = reactive({ count: 0 })
const copy = readonly(original)

// 监听只读代理
watchEffect(() => {
console.log('copy.count:', copy.count)
})

// 修改原始对象,只读代理也会更新
original.count++ // 控制台输出: copy.count: 1

实际应用:防止意外修改

import { reactive, readonly } from 'vue'

const state = reactive({
user: { name: '张三' },
settings: { theme: 'dark' }
})

// 暴露给子组件时使用只读版本
export function useUserStore() {
// 内部可以修改
function updateUser(name) {
state.user.name = name
}

// 外部只能读取,不能修改
return {
user: readonly(state.user),
settings: readonly(state.settings),
updateUser
}
}

与 provide/inject 配合

import { provide, reactive, readonly } from 'vue'

const theme = reactive({ mode: 'dark' })

// 提供只读版本,防止子组件意外修改
provide('theme', readonly(theme))

// 同时提供修改方法
provide('setTheme', (newTheme) => {
Object.assign(theme, newTheme)
})

shallowReadonly()

shallowReadonly() 只创建顶层属性的只读代理,嵌套对象仍然可以修改:

import { shallowReadonly } from 'vue'

const state = shallowReadonly({
count: 0,
nested: {
value: 1
}
})

// ❌ 顶层属性不可修改
state.count++ // 警告

// ⚠️ 嵌套属性可以修改(但通常不应该这样做)
state.nested.value = 2 // 允许修改,无警告

使用场景

import { shallowReadonly } from 'vue'

// 当只需要保护顶层配置,但内部数据需要可变时
const config = shallowReadonly({
apiUrl: 'https://api.example.com',
timeout: 5000,
headers: {
// 这个对象仍然可以修改
Authorization: ''
}
})

// 可以更新授权头
config.headers.Authorization = 'Bearer token'

// 但不能修改顶层配置
config.apiUrl = 'https://other.com' // 警告

isReadonly()

检查对象是否是由 readonly()shallowReadonly() 创建的只读代理:

import { reactive, readonly, shallowReadonly, ref, isReadonly } from 'vue'

const state = reactive({ count: 0 })
const readonlyState = readonly(state)
const shallowReadonlyState = shallowReadonly({ nested: {} })
const count = ref(0)
const readonlyCount = readonly(count)

console.log(isReadonly(readonlyState)) // true
console.log(isReadonly(shallowReadonlyState)) // true
console.log(isReadonly(readonlyCount)) // true
console.log(isReadonly(state)) // false
console.log(isReadonly(count)) // false

// 注意:readonly 对象的嵌套属性也是 readonly
console.log(isReadonly(readonlyState.nested)) // true(如果有嵌套对象)
console.log(isReadonly(shallowReadonlyState.nested)) // false

readonly vs shallowReadonly 对比

特性readonlyshallowReadonly
顶层只读
嵌套只读
响应式连接
性能较低(深层转换)较高(浅层)
使用场景需要完全保护只需保护顶层

工具函数

isRef()

判断一个值是否是 ref:

import { ref, isRef, reactive } from 'vue'

const count = ref(0)
const state = reactive({ count: 0 })

console.log(isRef(count)) // true
console.log(isRef(state)) // false
console.log(isRef(0)) // false

unref()

如果参数是 ref 则返回其值,否则返回参数本身:

import { ref, unref } from 'vue'

const count = ref(0)

console.log(unref(count)) // 0
console.log(unref(1)) // 1

// 等价于
val = isRef(val) ? val.value : val

toValue()

Vue 3.3+ 新增,规范化 ref、getter 或普通值为值:

import { ref, toValue } from 'vue'

const count = ref(0)

toValue(count) // 0
toValue(() => 1) // 1
toValue(2) // 2

在组合式函数中处理参数时很有用,可以让函数接受多种形式的参数:

import { ref, toValue, watchEffect } from 'vue'

// 组合式函数可以接受 ref、getter 或普通值
function useFeature(maybeRefOrGetter) {
// toValue 会自动解包 ref 并调用 getter
watchEffect(() => {
// 在副作用中使用 toValue,确保响应式追踪
const value = toValue(maybeRefOrGetter)
console.log('值变化:', value)
})
}

// 使用示例
const count = ref(0)

// 可以接受 ref
useFeature(count)

// 可以接受 getter 函数
useFeature(() => count.value + 1)

// 可以接受普通值
useFeature('hello')

// 这使得组合式函数非常灵活
function useFetch(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(false)

watchEffect(async () => {
// toValue 让我们可以接受 ref 或 getter
const urlValue = toValue(url)
if (!urlValue) return

loading.value = true
try {
const response = await fetch(urlValue)
data.value = await response.json()
} catch (e) {
error.value = e
} finally {
loading.value = false
}
})

return { data, error, loading }
}

// 使用
const endpoint = ref('/api/users')
const { data } = useFetch(endpoint) // 接受 ref

const { data: data2 } = useFetch(() => `/api/users/${userId.value}`) // 接受 getter

isReactive()

判断对象是否是 reactive() 创建的响应式代理:

import { reactive, isReactive, ref } from 'vue'

const state = reactive({ count: 0 })
const count = ref(0)

console.log(isReactive(state)) // true
console.log(isReactive(count)) // false
console.log(isReactive({})) // false

isProxy()

判断对象是否是 Vue 创建的响应式代理(reactive 或 readonly):

import { reactive, readonly, ref, isProxy } from 'vue'

const state = reactive({ count: 0 })
const readonlyState = readonly({ count: 0 })
const count = ref(0)

console.log(isProxy(state)) // true
console.log(isProxy(readonlyState)) // true
console.log(isProxy(count)) // false

完整示例

计数器示例

<script setup>
import { ref, reactive } from 'vue'

// 使用 ref
const count = ref(0)

function increment() {
count.value++
}

function reset() {
count.value = 0
}

// 使用 reactive
const counter = reactive({
count: 0,
lastUpdate: null,
history: []
})

function incrementReactive() {
counter.history.push(counter.count)
counter.count++
counter.lastUpdate = new Date().toLocaleTimeString()
}
</script>

<template>
<div>
<h3>使用 ref</h3>
<p>计数: {{ count }}</p>
<button @click="increment">增加</button>
<button @click="reset">重置</button>

<h3>使用 reactive</h3>
<p>计数: {{ counter.count }}</p>
<p>最后更新: {{ counter.lastUpdate || '未更新' }}</p>
<p>历史: {{ counter.history.join(', ') || '无' }}</p>
<button @click="incrementReactive">增加</button>
</div>
</template>

表单状态管理

<script setup>
import { reactive, toRefs } from 'vue'

const form = reactive({
username: '',
email: '',
password: '',
remember: false
})

function submit() {
console.log('提交表单:', { ...form })
}

function reset() {
Object.assign(form, {
username: '',
email: '',
password: '',
remember: false
})
}

// 可以解构使用
const { username, email, password, remember } = toRefs(form)
</script>

<template>
<form @submit.prevent="submit">
<div>
<label>用户名:</label>
<input v-model="form.username" />
</div>
<div>
<label>邮箱:</label>
<input v-model="form.email" type="email" />
</div>
<div>
<label>密码:</label>
<input v-model="form.password" type="password" />
</div>
<div>
<label>
<input v-model="form.remember" type="checkbox" />
记住我
</label>
</div>
<button type="submit">提交</button>
<button type="button" @click="reset">重置</button>
</form>
</template>

响应式 Props 解构(Vue 3.5+)

Vue 3.5 引入了响应式 Props 解构功能,这是对 defineProps() 返回值解构的重大改进。在此之前,解构 props 会丢失响应式连接。

传统问题

在 Vue 3.4 及更早版本中,解构 props 会丢失响应式:

// Vue 3.4 及更早版本
const { foo } = defineProps(['foo'])

watchEffect(() => {
console.log(foo) // 只执行一次,后续 props.foo 变化不会触发
})

这是因为 foo 只是一个普通常量,保存的是解构时的值快照,与 props.foo 的响应式连接已断开。

Vue 3.5 的解决方案

Vue 3.5 的编译器会自动处理解构的 props 变量,在访问时自动添加 props. 前缀:

<script setup>
// 编译器会自动处理
const { foo } = defineProps(['foo'])

// 等价于:console.log(props.foo)
console.log(foo)

watchEffect(() => {
// 等价于:console.log(props.foo)
// 现在 foo 变化时会重新执行!
console.log(foo)
})
</script>

声明默认值

响应式 Props 解构让默认值的声明更加简洁,直接使用 JavaScript 原生语法:

<script setup lang="ts">
// 方式一:使用类型声明 + 解构默认值
const {
count = 0, // 默认值为 0
message = 'hello', // 默认值为 'hello'
enabled = true // 默认值为 true
} = defineProps<{
count?: number
message?: string
enabled?: boolean
}>()
</script>

对比传统的 withDefaults 语法:

<script setup lang="ts">
// 传统方式:更冗长
const props = withDefaults(
defineProps<{
count?: number
message?: string
enabled?: boolean
}>(),
{
count: 0,
message: 'hello',
enabled: true
}
)
</script>

使用限制

虽然解构后的变量保持响应式,但在某些场景下需要特殊处理:

传递给 watch

const { foo } = defineProps(['foo'])

// ❌ 错误:watch 接收的是值而非响应式源
watch(foo, (newVal) => { /* ... */ })
// 等价于 watch(props.foo, ...),props.foo 是一个值

// ✅ 正确:使用 getter 函数
watch(() => foo, (newVal) => { /* ... */ })

传递给 Composable

const { userId } = defineProps(['userId'])

// ❌ 错误:传递的是值,失去响应式
useUser(userId)

// ✅ 正确:使用 getter 函数保持响应式
useUser(() => userId)

// composable 内部使用 toValue() 处理
function useUser(getter) {
watchEffect(() => {
fetch(`/api/user/${toValue(getter)}`)
})
}

实际应用示例

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

// 响应式解构 props,带默认值
const {
title = '默认标题',
pageSize = 10,
currentPage = 1
} = defineProps<{
title?: string
pageSize?: number
currentPage?: number
}>()

// 计算属性可以直接使用解构的变量
const displayTitle = computed(() => `[${title}]`)

// watchEffect 可以正常追踪变化
watchEffect(() => {
console.log('当前页:', currentPage)
})

// 传递给其他函数时使用 getter
const { data } = useFetch(() => `/api/posts?page=${currentPage}&size=${pageSize}`)
</script>

<template>
<h1>{{ displayTitle }}</h1>
<p>第 {{ currentPage }} 页,每页 {{ pageSize }} 条</p>
</template>

与 toRefs 的对比

响应式 Props 解构与 toRefs 作用于 reactive 对象的行为相似,但有本质区别:

// reactive 对象:解构后需要 toRefs 保持响应式
const state = reactive({ count: 0 })
const { count } = toRefs(state) // count 是 ref,需要 .value

// props:Vue 3.5 解构后直接保持响应式
const { count } = defineProps(['count']) // count 是普通值访问方式

关键区别

特性toRefs(reactive)响应式 Props 解构
返回值类型ref(需要 .value编译器自动处理(不需要 .value
响应式保持通过 ref 的 getter/setter编译时自动添加 props. 前缀
传递给函数需要传递 ref 本身使用 getter 函数

小结

  1. reactive():创建响应式对象,深层响应式
  2. ref vs reactive:了解两者的区别、局限性和使用场景
  3. toRef/toRefs:在解构时保持响应式连接
  4. ref 解包规则:reactive 对象中、数组/集合中、模板中的不同行为
  5. shallowRef/shallowReactive:浅层响应式,优化性能
  6. 工具函数:isRef、unref、toValue、isReactive、isProxy

最佳实践总结

  • 推荐使用 ref() 作为主要的响应式声明方式
  • 基本类型必须使用 ref()
  • 一组相关属性可使用 reactive()
  • 组合式函数返回值推荐使用 ref()
  • 解构 reactive 对象时使用 toRefs()
  • 大型数据结构考虑使用 shallow 版本优化性能

练习

  1. 分别用 refreactive 创建一个待办事项列表
  2. 实现一个使用 toRefs 的表单状态管理
  3. 创建一个组合式函数,返回 ref 类型的状态

参考资源