第三章 Vue3核心基础回顾:Composition API
好,咱们直接进入正题。这一章我打算带大家快速过一遍Vue3最核心的几个概念。说实话,很多同学觉得Composition API难,其实是因为没搞懂它到底解决了什么问题。
我个人习惯把Composition API看作是一种「组织代码的新思路」。以前Options API是把数据、方法、生命周期拆成不同的盒子,现在Composition API允许你把相关逻辑聚在一起。说白了,就是让代码更「内聚」。
3.1 setup:一切从这里开始
setup是Composition API的入口。组件创建之前,它会先执行。嗯,这里要注意:setup里不能使用this。为什么?因为组件实例还没创建完呢。
import { ref, onMounted } from 'vue'
export default {
setup() {
const count = ref(0)
const increment = () => {
count.value++
}
onMounted(() => {
console.log('组件挂载了,当前count:', count.value)
})
return {
count,
increment
}
}
}
我在项目中遇到过一个问题:有同事在setup里用了async,结果模板渲染不出来。记住,setup可以是async,但组件需要配合Suspense使用。否则,你返回的是一个Promise,模板拿不到数据。
3.2 ref vs reactive:该用哪个?
这两个API是Vue3响应式系统的基石。我经常被问到:「到底用ref还是reactive?」
我的建议很简单:
- 基本类型(string、number、boolean):用ref
- 对象或数组:用reactive更自然
- 不确定未来会变成什么类型:用ref,它更灵活
import { ref, reactive } from 'vue'
// ref:包裹基本类型
const name = ref('张三')
const age = ref(25)
// reactive:包裹对象
const user = reactive({
name: '李四',
age: 30,
hobbies: ['篮球', '编程']
})
// 修改ref需要.value
name.value = '王五'
// 修改reactive直接改属性
user.name = '赵六'
你想想看,ref每次都要写.value,是不是有点烦?其实Vue3的模板会自动解包,所以在模板里直接用name就行。但在setup里,必须写.value。
3.3 生命周期钩子:名字变了,用法没变
Vue3的生命周期钩子,说白了就是把Vue2的beforeDestroy改成了onBeforeUnmount,把destroyed改成了onUnmounted。其他基本一样。
| Vue2 | Vue3 | 说明 |
|---|---|---|
| created | setup | 组件创建时执行 |
| mounted | onMounted | DOM挂载后执行 |
| beforeDestroy | onBeforeUnmount | 组件销毁前执行 |
| destroyed | onUnmounted | 组件销毁后执行 |
import { onMounted, onUnmounted, onBeforeUnmount } from 'vue'
setup() {
onMounted(() => {
console.log('组件挂载了')
// 可以在这里做DOM操作或请求数据
})
onBeforeUnmount(() => {
console.log('组件即将销毁')
// 清理定时器、取消订阅等
})
onUnmounted(() => {
console.log('组件已销毁')
})
}
我在项目中遇到过一个问题:在onMounted里注册了事件监听,但忘记在onBeforeUnmount里移除。结果组件切换后,事件还在触发,导致内存泄漏。嗯,这个坑我踩过两次才长记性。
3.4 组件通信:props/emit 和 provide/inject
组件通信是Vue里最基础也最重要的能力。我见过很多项目,父子组件通信用Vuex,其实大可不必。
3.4.1 props/emit:父子通信的标准姿势
父组件通过props传数据给子组件,子组件通过emit发事件给父组件。这是最常用、最清晰的方式。
// 父组件
<template>
<ChildComponent
:title="pageTitle"
@update="handleUpdate"
/>
</template>
<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'
const pageTitle = ref('首页')
const handleUpdate = (newTitle) => {
pageTitle.value = newTitle
}
</script>
// 子组件
<template>
<div>
<h2>{{ title }}</h2>
<button @click="emit('update', '新标题')">修改标题</button>
</div>
</template>
<script setup>
defineProps({
title: {
type: String,
required: true
}
})
const emit = defineEmits(['update'])
</script>
我个人习惯在子组件里用defineProps和defineEmits,这样类型推断更友好。而且不用手动import,Vue会自动处理。
3.4.2 provide/inject:跨层级通信的利器
当组件层级很深时,一层层传props会让人崩溃。这时候provide/inject就派上用场了。
// 祖先组件
import { provide, ref } from 'vue'
const theme = ref('dark')
const updateTheme = (newTheme) => {
theme.value = newTheme
}
provide('theme', {
theme,
updateTheme
})
// 后代组件
import { inject } from 'vue'
const { theme, updateTheme } = inject('theme')
// 直接使用
console.log(theme.value) // 'dark'
updateTheme('light')
3.5 实战中的组合技巧
好了,知识点讲完了。咱们来点实战的。我通常会把相关逻辑封装成自定义hook,这样复用性更好。
// useCounter.js
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const doubleCount = computed(() => count.value * 2)
const increment = () => count.value++
const decrement = () => count.value--
return {
count,
doubleCount,
increment,
decrement
}
}
// 在组件中使用
import { useCounter } from './useCounter'
const { count, doubleCount, increment, decrement } = useCounter(10)
你想想看,这样写的好处是什么?逻辑可以复用,测试也方便。我在项目中就把用户登录状态、权限判断、主题切换这些都封装成了hook,代码瞬间清爽了很多。
好,这一章就到这里。下一章咱们会深入探讨Vue3的响应式原理,看看ref和reactive背后到底是怎么工作的。到时候我会用一些实际案例,帮大家彻底搞懂「响应式」这三个字。