在 Vue 3 的 <script setup>
语法糖中计算属性和监听的写法
一、计算属性 (Computed)
使用 computed
函数创建响应式的计算值。
1. 基本用法
<script setup lang="ts">
import { ref, computed } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
// 计算属性:全名
const fullName = computed(() => {
return firstName.value + lastName.value
})
// 修改名字
function changeName() {
firstName.value = '李'
lastName.value = '四'
}
</script>
<template>
<div>
<p>firstName: {{ firstName }}</p>
<p>lastName: {{ lastName }}</p>
<p>fullName: {{ fullName }}</p>
<button @click="changeName">修改名字</button>
</div>
</template>
2. 带 setter 的计算属性
<script setup lang="ts">
import { ref, computed } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
// 可写的计算属性
const fullName = computed({
get() {
return firstName.value + '·' + lastName.value
},
set(newValue: string) {
const [first, last] = newValue.split('·')
firstName.value = first
lastName.value = last
}
})
// 修改计算属性
function updateFullName() {
fullName.value = '诸葛·亮' // 触发 setter
}
</script>
二、监听 (Watch)
使用 watch
和 watchEffect
响应数据变化。
1. watch
基础用法
<script setup lang="ts">
import { ref, watch } from 'vue'
const count = ref(0)
const doubleCount = ref(0)
// 监听单个 ref
watch(count, (newValue, oldValue) => {
console.log(`count变化: ${oldValue} → ${newValue}`)
doubleCount.value = newValue * 2
})
// 监听多个源
watch([count, doubleCount], ([newCount, newDouble], [oldCount, oldDouble]) => {
console.log(`count: ${oldCount}→${newCount}, double: ${oldDouble}→${newDouble}`)
})
function increment() {
count.value++
}
</script>
2. watch
高级选项
<script setup lang="ts">
import { ref, watch } from 'vue'
const user = ref({
name: '张三',
address: {
city: '北京'
}
})
// 深度监听对象
watch(
user,
(newVal) => {
console.log('用户信息变化:', newVal)
},
{ deep: true, immediate: true } // 立即执行+深度监听
)
// 监听特定属性
watch(
() => user.value.address.city,
(newCity, oldCity) => {
console.log(`城市变化: ${oldCity} → ${newCity}`)
}
)
function changeCity() {
user.value.address.city = '上海'
}
</script>
3. watchEffect
自动依赖追踪
<script setup lang="ts">
import { ref, watchEffect } from 'vue'
const count = ref(0)
const double = ref(0)
// 自动追踪依赖
watchEffect(() => {
console.log(`count: ${count.value}`)
double.value = count.value * 2
})
// 带清理副作用的示例
const data = ref(null)
watchEffect(async (onCleanup) => {
const token = setTimeout(() => {
data.value = await fetchData(count.value)
}, 1000)
// 清理函数(取消未完成的请求)
onCleanup(() => {
clearTimeout(token)
console.log('清理上一次的请求')
})
})
</script>
三、计算属性 vs 监听:使用场景对比
场景 | 推荐方案 | 示例 |
---|---|---|
派生状态 | 计算属性 | 全名 = 姓 + 名 |
依赖多个数据 | 计算属性 | 总价 = 单价 × 数量 |
数据变化执行异步操作 | 监听 (watch ) | 搜索建议、表单验证 |
响应多个数据变化 | 监听 (watch ) | 同时监听多个过滤条件 |
需要立即执行的副作用 | watchEffect | 初始化数据、DOM 操作 |
需要清理的资源管理 | watchEffect | 定时器、事件监听、请求取消 |
四、最佳实践和常见问题
1. 计算属性最佳实践
<script setup lang="ts">
import { computed, ref } from 'vue'
const price = ref(100)
const quantity = ref(2)
// ✅ 推荐:纯函数,无副作用
const total = computed(() => price.value * quantity.value)
// ❌ 避免:在计算属性中修改状态
const badExample = computed(() => {
// 不要这样做!
quantity.value++ // 副作用
return price.value * quantity.value
})
</script>
2. 监听最佳实践
<script setup lang="ts">
import { watch, ref } from 'vue'
const searchQuery = ref('')
const searchResults = ref([])
// ✅ 推荐:使用 watch 处理异步
watch(searchQuery, async (newQuery) => {
if (!newQuery.trim()) {
searchResults.value = []
return
}
searchResults.value = await fetchResults(newQuery)
}, { debounce: 300 }) // 添加防抖
// ❌ 避免:在 watchEffect 中直接修改依赖
const count = ref(0)
watchEffect(() => {
// 可能导致无限循环!
count.value = count.value + 1
})
</script>
3. 性能优化技巧
<script setup lang="ts">
import { watch, ref } from 'vue'
const largeList = ref([...Array(10000).keys()])
const filter = ref('')
// 优化:避免深度监听大型数组
watch(
() => [...largeList.value], // 创建副本避免深度监听
(newList) => {
console.log('列表变化')
}
)
// 优化:使用 flush: 'post' 确保 DOM 更新后执行
watch(
someRef,
() => {
// 在 DOM 更新后测量元素
measureDOMElement()
},
{ flush: 'post' }
)
</script>
五、完整案例:购物车计算
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
// 商品数据
const products = ref([
{ id: 1, name: '商品A', price: 100, quantity: 2 },
{ id: 2, name: '商品B', price: 200, quantity: 1 },
{ id: 3, name: '商品C', price: 300, quantity: 3 }
])
// 计算属性:总价
const totalPrice = computed(() => {
return products.value.reduce((sum, product) => {
return sum + product.price * product.quantity
}, 0)
})
// 计算属性:折后价(满1000减200)
const discountedPrice = computed(() => {
return totalPrice.value >= 1000 ? totalPrice.value - 200 : totalPrice.value
})
// 监听:当总价超过2000时提示
watch(totalPrice, (newTotal) => {
if (newTotal > 2000) {
console.warn('总价超过2000元')
alert('您购买的商品总价已超过2000元')
}
})
// 增加商品数量
function increaseQuantity(id: number) {
const product = products.value.find(p => p.id === id)
if (product) product.quantity++
}
// 减少商品数量
function decreaseQuantity(id: number) {
const product = products.value.find(p => p.id === id)
if (product && product.quantity > 1) product.quantity--
}
</script>
<template>
<div class="cart">
<div v-for="product in products" :key="product.id" class="cart-item">
<h3>{{ product.name }}</h3>
<p>单价: {{ product.price }}元</p>
<div class="quantity-control">
<button @click="decreaseQuantity(product.id)">-</button>
<span>{{ product.quantity }}</span>
<button @click="increaseQuantity(product.id)">+</button>
</div>
<p>小计: {{ product.price * product.quantity }}元</p>
</div>
<div class="summary">
<p>总价: {{ totalPrice }}元</p>
<p v-if="totalPrice >= 1000" class="discount">
优惠价: {{ discountedPrice }}元 (已减200元)
</p>
</div>
</div>
</template>
<style scoped>
.cart-item {
border: 1px solid #eee;
padding: 1rem;
margin-bottom: 1rem;
}
.quantity-control {
display: flex;
gap: 0.5rem;
align-items: center;
}
.discount {
color: red;
font-weight: bold;
}
</style>
六、关键区别总结
特性 | 计算属性 (computed) | 监听 (watch/watchEffect) |
---|---|---|
目的 | 派生新数据 | 响应数据变化执行操作 |
返回值 | 返回一个响应式引用 | 无返回值 |
依赖追踪 | 自动追踪 | watch 需显式指定,watchEffect 自动 |
执行时机 | 惰性计算(使用时求值) | 立即或异步执行 |
异步支持 | 不支持 | 支持 |
使用场景 | 模板中使用的派生数据 | 副作用操作、异步任务 |
性能 | 高效缓存 | 可能更重 |
是否可写 | 可通过 setter 实现 | 只读 |
最佳选择指南:
-
使用计算属性:
- 当需要基于现有状态计算新值时
- 当需要在模板中使用派生数据时
- 当需要缓存计算结果提高性能时
-
使用监听:
- 当需要在状态变化时执行副作用(如API调用)时
- 当需要响应异步操作时
- 当需要更细粒度的控制(如防抖、深度监听)时
-
使用 watchEffect:
- 当依赖关系复杂且动态变化时
- 当需要立即执行并自动收集依赖时
- 当需要清理资源(定时器、订阅)时