Vue3 setup、计算属性、侦听器、响应式API
一、setup
一、setup
函数基础
作用:组合式 API 的入口,用于定义响应式数据、方法和生命周期钩子
执行时机:在 beforeCreate
之前调用,此时组件实例尚未创建
基本结构:
export default {setup(props, context) {// 响应式数据// 方法定义// 生命周期钩子return {// 暴露给模板的内容}}
}
-
setup
中访问this
是undefined
二、核心参数解析
1. props
参数
props: {title: String
},
setup(props) {// 访问 propsconsole.log(props.title)// 不要直接解构 props!const { title } = toRefs(props) // 保持响应性
}
2. context
参数
包含三个重要属性:
setup(props, { attrs, slots, emit }) {// attrs: 非 props 的属性// slots: 插槽内容// emit: 触发自定义事件function handleClick() {emit('custom-event', payload)}
}
三、<script setup>
语法糖
更简洁的编译时语法(SFC 专用):
基础用法
<script setup>
import { ref } from 'vue'const count = ref(0)
</script><template><button @click="count++">{{ count }}</button>
</template>
特性对比
特性 | 普通 setup | <script setup> |
---|---|---|
代码量 | 需要 return 暴露 | 自动暴露顶层绑定 |
TypeScript 支持 | 需要类型断言 | 更好的类型推断 |
组件注册 | components 选项 | 自动注册 |
自定义指令 | directives 选项 | 自动注册 |
二、computed(计算属性)
作用:创建依赖其他响应式数据的缓存计算结果
1. 基础用法
import { ref, computed } from 'vue'// 组合式 API
const count = ref(0)
const double = computed(() => count.value * 2)
2. 可写计算属性
const firstName = ref('John')
const lastName = ref('Doe')const fullName = computed({get: () => `${firstName.value} ${lastName.value}`,set: (newValue) => {[firstName.value, lastName.value] = newValue.split(' ')}
})// 修改计算属性
fullName.value = 'Jane Smith' // 自动触发 setter
3. 最佳实践
-
用于派生状态(如过滤列表、格式显示)
-
避免在计算属性中进行异步操作或副作用
-
复杂计算应拆分到独立函数中
三、watch(侦听器)
作用:观察特定数据源变化,执行副作用
1. 侦听 ref 对象
const count = ref(0)// 侦听 ref
watch(count, (newVal, oldVal) => {console.log(`从 ${oldVal} 变为 ${newVal}`)
})// 立即执行
watch(count, callback, { immediate: true })
2. 侦听 reactive 对象
const state = reactive({ user: { name: 'Alice' }
})// 需要函数返回具体值
watch(() => state.user.name,(newName) => {console.log('用户名变化:', newName)}
)// 深度侦听
watch(() => state.user,(newUser) => {console.log('用户对象深度变化:', newUser)},{ deep: true }
)
3. 多源侦听
watch([refA, () => objB.prop],([newA, newB], [oldA, oldB]) => {// 处理变化}
)
四、watchEffect(副作用侦听)
作用:立即执行函数,自动追踪依赖并响应变化
1. 基础用法
const count = ref(0)
const stop = watchEffect(() => {console.log('当前值:', count.value)// 自动追踪 count.value 作为依赖
})// 停止侦听
stop()
2. 副作用清理
watchEffect((onCleanup) => {const timer = setInterval(() => {console.log('定时器运行')}, 1000)onCleanup(() => {clearInterval(timer)})
})
3. 调试选项
watchEffect(() => { /* ... */ },{flush: 'post', // 回调在 DOM 更新后触发onTrack(e) { /* 调试依赖追踪 */ },onTrigger(e) { /* 调试依赖变化 */ }}
)
核心对比
特性 | computed | watch | watchEffect |
---|---|---|---|
返回值 | 返回计算值 | 无返回值 | 无返回值 |
执行时机 | 惰性计算(依赖变化时) | 惰性执行(依赖变化时) | 立即执行 + 依赖变化时 |
主要用途 | 派生状态 | 响应特定数据变化 | 自动追踪依赖的副作用 |
访问旧值 | 不支持 | 支持 | 不支持 |
多源侦听 | 不支持 | 支持 | 自动处理多个依赖 |
性能优化 | 自动缓存 | 需要手动配置 | 自动依赖追踪 |
五、使用场景示例
1. 搜索过滤(computed)
const searchText = ref('')
const items = ref([/* ... */])const filteredItems = computed(() => {return items.value.filter(item => item.name.includes(searchText.value))
})
2. 路由参数变化(watch)
import { watch } from 'vue'
import { useRoute } from 'vue-router'const route = useRoute()watch(() => route.params.id,(newId) => {fetchData(newId)}
)
3. 自动保存(watchEffect)
const formData = reactive({ /* ... */ })// 任何字段修改后自动保存
const stopAutoSave = watchEffect(async () => {await api.saveDraft(formData)console.log('自动保存成功')
})// 组件卸载时停止
onUnmounted(stopAutoSave)
六、最佳实践
1. 优先选择
-
需要派生状态 →
computed
-
需要旧值 →
watch
-
多个依赖/自动追踪 →
watchEffect
2. 性能优化
// 避免不必要的深度侦听
watch(() => ({ ...obj }), // 创建新对象触发变化(newObj) => { /* ... */ }
)// 限制执行频率
import { debounce } from 'lodash-es'
watch(value,debounce((newVal) => {// 处理逻辑}, 300)
)
3. 组合使用
// 复杂场景组合
const userId = ref(null)
const userData = ref(null)// 自动获取用户数据
watchEffect(() => {if (!userId.value) returnfetchUser(userId.value).then(data => {userData.value = data})
})// 手动刷新
const refreshUser = () => {// 通过修改依赖触发重新执行userId.value = userId.value
}
七、常见问题
Q1: watch 和 watchEffect 如何选择?
-
需要明确知道哪些状态变化触发回调 →
watch
-
依赖动态变化或需要自动追踪 →
watchEffect
Q2: 为什么 computed 需要返回函数?
计算属性需要保持响应性,通过函数形式确保正确访问最新值
Q3: 如何停止侦听器?
// watch 返回停止函数
const stopWatch = watch(/* ... */)
stopWatch()// watchEffect 同理
const stopEffect = watchEffect(/* ... */)
stopEffect()
Q4: 如何处理异步操作?
// watch 中处理异步
watch(source,async (newVal, oldVal, onCleanup) => {let cancelled = falseonCleanup(() => { cancelled = true })const data = await fetchData(newVal)if (!cancelled) {// 处理数据}}
)
通过合理组合使用这些 API,可以实现高效、可维护的响应式逻辑。记住:
-
computed
用于派生数据 -
watch
用于精准控制侦听目标 -
watchEffect
用于自动追踪依赖的副作用
响应式API
一、核心 API 对比表
特性 | ref | reactive | toRefs | toRef |
---|---|---|---|---|
适用类型 | 基本类型/对象 | 对象/数组 | 响应式对象 | 响应式对象的单个属性 |
访问方式 | 通过 .value 访问 | 直接访问属性 | 解构后仍保持响应式 | 通过 .value 访问 |
响应性保持 | 始终响应式 | 始终响应式 | 保持解构后的响应式 | 保持源属性的响应式连接 |
典型使用场景 | 独立基本值、模板引用、组件引用 | 复杂对象状态管理 | 解构响应式对象 | 将 props 属性转为响应式引用 |
类型推断 | 明确类型(Ref<T>) | 深层响应式类型 | 将对象属性转为 Ref 类型 | 单个属性转为 Ref 类型 |
二、ref
与 reactive
深度解析
1. ref
最佳实践
import { ref } from 'vue'// 基本类型
const count = ref(0)
console.log(count.value) // 0// 引用类型(依然可用,但更推荐 reactive)
const user = ref({ name: 'Alice' })
console.log(user.value.name) // 'Alice'// 模板引用
const inputRef = ref(null)
使用场景:
-
独立的基本类型值(字符串、数字、布尔值)
-
需要重新赋值的对象(替换整个对象时保持响应性)
-
DOM 元素引用(结合模板中的
ref
属性)
2. reactive
最佳实践
import { reactive } from 'vue'const state = reactive({user: {name: 'Bob',age: 30},items: ['apple', 'banana']
})// 直接访问属性
state.user.name = 'Charlie'
state.items.push('orange')
使用场景:
-
需要深度嵌套的复杂对象
-
需要保持引用的数据结构(如数组、Map、Set)
-
逻辑相关的多个属性组合(如表单字段)
三、toRefs
与 toRef
核心差异
1. toRefs
使用示例
import { reactive, toRefs } from 'vue'const state = reactive({name: 'Eva',age: 25
})// 解构后保持响应性
const { name, age } = toRefs(state)name.value = 'David' // 修改会同步到源对象
console.log(state.name) // 'David'
核心特性:
-
转换整个响应式对象的所有属性为 ref
-
保持与源对象的响应式连接
-
常用于组合式函数返回值
2. toRef
使用示例
import { reactive, toRef } from 'vue'const state = reactive({ count: 0 })// 创建单个属性的 ref
const countRef = toRef(state, 'count')// 修改 ref 会同步源对象
countRef.value++
console.log(state.count) // 1
核心特性:
-
针对单个属性创建 ref
-
即使源属性不存在仍可创建(需第二个参数默认值)
-
常用于 props 属性转换
四、组合使用模式
1. 组合式函数中的标准模式
// useFeature.js
import { reactive, toRefs } from 'vue'export function useFeature() {const state = reactive({x: 0,y: 0})function update() {state.x = Math.random()state.y = Math.random()}return { ...toRefs(state), update }
}// 组件中使用
const { x, y, update } = useFeature()
2. Props 处理最佳实践
import { toRef } from 'vue'export default {props: ['userId'],setup(props) {// 保持响应性的正确方式const userId = toRef(props, 'userId')// 错误方式:直接解构会失去响应性// const { userId } = propsreturn { userId }}
}
五、常见问题解决方案
问题 1:如何选择 ref 和 reactive?
-
简单值 →
ref
-
复杂对象 →
reactive
+toRefs
-
需要重新赋值的对象 →
ref
问题 2:解构响应式对象失效怎么办?
-
使用
toRefs
包裹后再解构 -
直接操作原对象属性
问题 3:如何确保 props 的响应性?
-
使用
toRef(props, 'propName')
-
避免直接解构 props
六、TypeScript 类型处理技巧
1. ref
类型标注
const count = ref<number>(0) // Ref<number>
const user = ref<User>({ name: '' }) // Ref<User>
2. reactive
类型推断
interface State {name: stringage: number
}const state = reactive<State>({name: 'Alice',age: 25
})
3. toRefs
类型保留
const state = reactive({ x: 0, y: 0 })
const { x, y } = toRefs(state) // x: Ref<number>, y: Ref<number>
七、性能优化建议
-
避免深层嵌套:过度使用 reactive 会导致深层响应式代理影响性能
-
合理使用 shallowRef:对不需要深度监听的大对象使用
shallowRef
-
批量更新策略:
const state = reactive({ a: 1, b: 2 })// 推荐:单次修改多个属性 Object.assign(state, { a: 2, b: 3 })// 不推荐:多次触发更新 state.a = 2 state.b = 3
八、综合应用示例
<script setup>
import { reactive, ref, toRefs, toRef } from 'vue'// 基本类型使用 ref
const searchQuery = ref('')// 复杂对象使用 reactive
const pagination = reactive({page: 1,pageSize: 10,total: 0
})// 转换为 refs 供模板使用
const { page, pageSize } = toRefs(pagination)// props 处理
const props = defineProps(['initialData'])
const initialDataRef = toRef(props, 'initialData')// 方法
function nextPage() {pagination.page++
}
</script><template><input v-model="searchQuery"><div>当前页: {{ page }}每页数量: {{ pageSize }}</div><button @click="nextPage">下一页</button>
</template>
通过合理运用这些响应式 API,可以实现:
-
更清晰的代码结构
-
更好的类型推断(TypeScript)
-
更高效的响应式更新
-
更安全的 props 处理
-
更灵活的逻辑复用
记住黄金法则:
简单值用 ref
,复杂对象用 reactive
,解构对象用 toRefs
,处理 props 用 toRef