响应式基础
Vue 的响应式系统是其核心特性之一。当你修改应用状态时,视图会自动更新。这种响应式的数据绑定让开发变得更加直观和高效。
声明响应式状态
选项式 API
在选项式 API 中,我们用 data
选项来声明组件的响应式状态:
<template>
<div>
<button @click="increment">{{ count }}</button>
</div>
</template>
<script>
export default {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
}
}
}
</script>
data
选项的值应该是一个函数,返回一个对象。Vue 会在创建新组件实例的时候调用此函数,并将函数返回的对象用响应式系统进行包装。此对象的所有顶层属性都会被代理到组件实例上。
组合式 API
在组合式 API 中,我们可以使用 ref()
函数来声明响应式状态:
<template>
<div>
<button @click="increment">{{ count }}</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
</script>
ref()
接收参数,并将其包裹在一个带有 .value
属性的 ref 对象中返回。
ref()
ref()
让我们能创造一种对任意值的 "引用",并能够在不丢失响应性的前提下传递这些引用。
基本用法
import { ref } from 'vue'
const count = ref(0)
const message = ref('Hello')
const isVisible = ref(true)
console.log(count.value) // 0
count.value = 1
console.log(count.value) // 1
在模板中访问
当 ref 在模板中作为顶层属性被访问时,它们会被自动"解包",所以不需要使用 .value
:
<template>
<div>
<p>{{ count }}</p> <!-- 无需 .value -->
<button @click="count++">增加</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
ref 解包的注意事项
只有当 ref 是模板渲染上下文的顶层属性时才适用自动"解包"。例如,foo
是顶层属性,但 object.foo
不是。
const object = { foo: ref(1) }
下面的表达式将不会按预期工作:
<template>
{{ object.foo + 1 }} <!-- 这不会按预期工作 -->
</template>
渲染的结果会是 [object Object]1
,因为 object.foo
是一个 ref 对象。我们可以通过将 foo
改为顶层属性来解决这个问题:
const { foo } = object
<template>
{{ foo + 1 }} <!-- 现在可以正常工作了 -->
</template>
reactive()
还有另一种声明响应式状态的方式,即使用 reactive()
API。与将内部值包装在特殊对象中的 ref 不同,reactive()
将使对象本身具有响应性:
import { reactive } from 'vue'
const state = reactive({ count: 0 })
在模板中使用:
<template>
<div>
<button @click="state.count++">
{{ state.count }}
</button>
</div>
</template>
<script setup>
import { reactive } from 'vue'
const state = reactive({ count: 0 })
</script>
reactive() 的局限性
reactive()
API 有一些局限性:
有限的值类型:它只能用于对象类型(对象、数组和如
Map
、Set
这样的集合类型)。它不能持有如string
、number
或boolean
这样的原始类型。不能替换整个对象:由于 Vue 的响应式跟踪是通过属性访问实现的,我们必须始终保持对响应式对象的相同引用。这意味着我们不能轻易地"替换"响应式对象,因为这样的话与第一个引用的响应性连接将丢失:
let state = reactive({ count: 0 })
// 上面的 ({ count: 0 }) 引用将不再被追踪
// (响应性连接已丢失!)
state = reactive({ count: 1 })
- 对解构操作不友好:当我们将响应式对象的原始类型属性解构为本地变量时,或者将该属性传递给函数时,我们将丢失响应性连接:
const state = reactive({ count: 0 })
// 当解构时,count 已经与 state.count 断开连接
let { count } = state
// 不会影响原始的 state
count++
// 该函数接收到的是一个普通的数字
// 并且无法追踪 state.count 的变化
callSomeFunction(state.count)
ref() vs reactive()
何时使用 ref()
- 声明原始类型的响应式数据
- 需要重新分配整个值
- 在组合函数中返回响应式数据
// 原始类型
const count = ref(0)
const message = ref('hello')
// 重新分配
const todos = ref([])
todos.value = newTodos
// 组合函数
function useCounter() {
const count = ref(0)
return { count }
}
何时使用 reactive()
- 声明对象类型的响应式数据
- 不需要重新分配整个对象
- 本地状态管理
// 对象状态
const state = reactive({
user: {
name: 'John',
age: 30
},
settings: {
theme: 'dark'
}
})
// 表单数据
const form = reactive({
name: '',
email: '',
message: ''
})
响应式代理 vs 原始对象
值得注意的是,reactive()
返回的是一个原始对象的 Proxy,它和原始对象是不相等的:
const raw = {}
const proxy = reactive(raw)
// 代理对象和原始对象不是全等的
console.log(proxy === raw) // false
只有代理对象是响应式的,更改原始对象不会触发更新。因此,使用 Vue 的响应式系统的最佳实践是仅使用你声明对象的代理版本。
为保证访问代理的一致性,对同一个原始对象调用 reactive()
会总是返回同样的代理对象,而对一个已存在的代理对象调用 reactive()
会返回其本身:
// 在同一个对象上调用 reactive() 会返回相同的代理
console.log(reactive(raw) === proxy) // true
// 在一个代理上调用 reactive() 会返回它自己
console.log(reactive(proxy) === proxy) // true
实际应用示例
计数器组件
<template>
<div class="counter">
<h2>计数器: {{ count }}</h2>
<div class="buttons">
<button @click="decrement" :disabled="count <= 0">-</button>
<button @click="increment">+</button>
<button @click="reset">重置</button>
</div>
<p>计数器状态: {{ count > 0 ? '正数' : count < 0 ? '负数' : '零' }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
function decrement() {
count.value--
}
function reset() {
count.value = 0
}
</script>
<style scoped>
.counter {
text-align: center;
padding: 20px;
}
.buttons {
margin: 20px 0;
}
button {
margin: 0 5px;
padding: 8px 16px;
font-size: 16px;
cursor: pointer;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
用户信息表单
<template>
<div class="user-form">
<h2>用户信息</h2>
<form @submit.prevent="submitForm">
<div class="form-group">
<label for="name">姓名:</label>
<input
id="name"
v-model="form.name"
type="text"
required
>
</div>
<div class="form-group">
<label for="email">邮箱:</label>
<input
id="email"
v-model="form.email"
type="email"
required
>
</div>
<div class="form-group">
<label for="age">年龄:</label>
<input
id="age"
v-model.number="form.age"
type="number"
min="1"
max="120"
>
</div>
<div class="form-group">
<label>
<input
v-model="form.subscribe"
type="checkbox"
>
订阅邮件通知
</label>
</div>
<button type="submit" :disabled="!isFormValid">提交</button>
<button type="button" @click="resetForm">重置</button>
</form>
<div v-if="submitted" class="result">
<h3>提交的信息:</h3>
<pre>{{ JSON.stringify(form, null, 2) }}</pre>
</div>
</div>
</template>
<script setup>
import { reactive, ref, computed } from 'vue'
const form = reactive({
name: '',
email: '',
age: null,
subscribe: false
})
const submitted = ref(false)
const isFormValid = computed(() => {
return form.name.trim() && form.email.trim()
})
function submitForm() {
console.log('提交表单:', form)
submitted.value = true
}
function resetForm() {
form.name = ''
form.email = ''
form.age = null
form.subscribe = false
submitted.value = false
}
</script>
<style scoped>
.user-form {
max-width: 400px;
margin: 0 auto;
padding: 20px;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input[type="text"],
input[type="email"],
input[type="number"] {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
input[type="checkbox"] {
margin-right: 8px;
}
button {
margin-right: 10px;
padding: 10px 20px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:disabled {
background-color: #6c757d;
cursor: not-allowed;
}
button[type="button"] {
background-color: #6c757d;
}
.result {
margin-top: 20px;
padding: 15px;
background-color: #f8f9fa;
border-radius: 4px;
}
pre {
background-color: #e9ecef;
padding: 10px;
border-radius: 4px;
overflow-x: auto;
}
</style>
待办事项列表
<template>
<div class="todo-app">
<h2>待办事项</h2>
<div class="add-todo">
<input
v-model="newTodo"
@keyup.enter="addTodo"
placeholder="添加新的待办事项..."
class="todo-input"
>
<button @click="addTodo" :disabled="!newTodo.trim()">添加</button>
</div>
<div class="filters">
<button
v-for="filter in filters"
:key="filter.key"
@click="currentFilter = filter.key"
:class="{ active: currentFilter === filter.key }"
>
{{ filter.label }}
</button>
</div>
<ul class="todo-list">
<li
v-for="todo in filteredTodos"
:key="todo.id"
:class="{ completed: todo.completed }"
>
<input
v-model="todo.completed"
type="checkbox"
class="todo-checkbox"
>
<span class="todo-text">{{ todo.text }}</span>
<button @click="removeTodo(todo.id)" class="remove-btn">删除</button>
</li>
</ul>
<div v-if="todos.length === 0" class="empty-state">
暂无待办事项
</div>
<div v-if="todos.length > 0" class="stats">
<span>总计: {{ todos.length }}</span>
<span>已完成: {{ completedCount }}</span>
<span>未完成: {{ remainingCount }}</span>
</div>
</div>
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
const newTodo = ref('')
const currentFilter = ref('all')
const todos = ref([])
const nextId = ref(1)
const filters = reactive([
{ key: 'all', label: '全部' },
{ key: 'active', label: '未完成' },
{ key: 'completed', label: '已完成' }
])
const filteredTodos = computed(() => {
switch (currentFilter.value) {
case 'active':
return todos.value.filter(todo => !todo.completed)
case 'completed':
return todos.value.filter(todo => todo.completed)
default:
return todos.value
}
})
const completedCount = computed(() => {
return todos.value.filter(todo => todo.completed).length
})
const remainingCount = computed(() => {
return todos.value.length - completedCount.value
})
function addTodo() {
if (newTodo.value.trim()) {
todos.value.push({
id: nextId.value++,
text: newTodo.value.trim(),
completed: false
})
newTodo.value = ''
}
}
function removeTodo(id) {
const index = todos.value.findIndex(todo => todo.id === id)
if (index > -1) {
todos.value.splice(index, 1)
}
}
</script>
<style scoped>
.todo-app {
max-width: 500px;
margin: 0 auto;
padding: 20px;
}
.add-todo {
display: flex;
margin-bottom: 20px;
}
.todo-input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px 0 0 4px;
font-size: 16px;
}
.add-todo button {
padding: 10px 20px;
background-color: #007bff;
color: white;
border: none;
border-radius: 0 4px 4px 0;
cursor: pointer;
}
.filters {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.filters button {
padding: 8px 16px;
border: 1px solid #ddd;
background-color: white;
border-radius: 4px;
cursor: pointer;
}
.filters button.active {
background-color: #007bff;
color: white;
}
.todo-list {
list-style: none;
padding: 0;
}
.todo-list li {
display: flex;
align-items: center;
padding: 10px;
border-bottom: 1px solid #eee;
}
.todo-list li.completed {
opacity: 0.6;
}
.todo-checkbox {
margin-right: 10px;
}
.todo-text {
flex: 1;
font-size: 16px;
}
.todo-list li.completed .todo-text {
text-decoration: line-through;
}
.remove-btn {
padding: 4px 8px;
background-color: #dc3545;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.empty-state {
text-align: center;
color: #666;
font-style: italic;
padding: 40px;
}
.stats {
display: flex;
justify-content: space-between;
margin-top: 20px;
padding: 10px;
background-color: #f8f9fa;
border-radius: 4px;
font-size: 14px;
}
</style>
最佳实践
优先使用 ref():对于大多数情况,推荐使用
ref()
,因为它更灵活且没有reactive()
的限制。保持响应式引用:避免解构响应式对象,如果需要解构,使用
toRefs()
或toRef()
。合理组织状态:将相关的状态组织在一起,使用
reactive()
管理复杂的对象状态。避免深层嵌套:过深的响应式对象嵌套可能影响性能,考虑扁平化数据结构。
使用 readonly:对于不应该被修改的数据,使用
readonly()
创建只读代理。
下一步
现在你已经了解了 Vue 的响应式基础,让我们继续学习:
响应式系统是 Vue 的核心,掌握它将让你能够构建高效、动态的应用程序!