一、setup 配置详解
1.1 Vue2 中的 attrs 和 slots 回顾
在 Vue2 中,父组件通过标签属性向子组件传递数据时,通常需要在子组件中使用 props
接收。但即使不声明接收,这些属性也会存在于子组件实例的 $attrs
中。
示例对比:
javascript
// Vue2 子组件
export default {
props: ['receivedProp'], // 声明接收的属性
created() {
console.log(this.$attrs) // 未声明接收的属性会出现在这里
console.log(this.$slots) // 父组件传递的插槽内容
}
}
1.2 Vue3 setup 的两个关键点
执行时机
-
setup
在beforeCreate
之前执行 -
此时组件实例尚未创建,
this
为undefined
参数解析
setup
接收两个参数:
-
props
:包含组件外部传递且内部声明接收的属性 -
context
:上下文对象,包含:-
attrs
:未在 props 中声明的属性(相当于 Vue2 的$attrs
) -
slots
:接收的插槽内容(相当于 Vue2 的$slots
) -
emit
:触发自定义事件的函数(相当于 Vue2 的$emit
)
-
完整示例:
javascript
import { defineComponent } from 'vue'
export default defineComponent({
props: ['title'],
emits: ['change'], // 必须声明自定义事件
setup(props, { attrs, slots, emit }) {
console.log(props.title) // 访问声明的 prop
console.log(attrs.customAttr) // 访问未声明的属性
const handleClick = () => {
emit('change', 'new value') // 触发事件
}
return {
handleClick
}
}
})
二、响应式进阶:计算属性与监视
2.1 computed 函数
Vue3 的 computed
用法与 Vue2 类似,但更灵活:
javascript
import { ref, computed } from 'vue'
setup() {
const firstName = ref('张')
const lastName = ref('三')
// 只读计算属性
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
// 可写计算属性
const editableName = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (newValue) => {
const [first, last] = newValue.split(' ')
firstName.value = first
lastName.value = last
}
})
return { fullName, editableName }
}
2.2 watch 函数详解
Vue3 的 watch
功能强大但有一些注意事项:
javascript
import { ref, reactive, watch } from 'vue'
setup() {
const count = ref(0)
const state = reactive({
user: {
name: 'Alice',
age: 25
}
})
// 情况1:监视 ref
watch(count, (newVal, oldVal) => {
console.log(`count变化: ${oldVal} -> ${newVal}`)
})
// 情况2:监视多个 ref
watch([count, anotherRef], ([newCount, newAnother], [oldCount, oldAnother]) => {
// 处理变化
})
// 情况3:监视 reactive 对象(注意 oldValue 问题)
watch(
() => state.user,
(newUser, oldUser) => {
// oldUser 可能与 newUser 相同!
},
{ deep: true } // 虽然默认开启,但显式声明更清晰
)
// 情况4:监视 reactive 对象的特定属性
watch(
() => state.user.age,
(newAge, oldAge) => {
// 这里能正确获取 oldAge
}
)
}
2.3 watchEffect 智能监听
watchEffect
自动追踪回调中的响应式依赖:
javascript
import { ref, watchEffect } from 'vue'
setup() {
const count = ref(0)
const message = ref('')
watchEffect(() => {
console.log(`count: ${count.value}, message: ${message.value}`)
// 会自动追踪 count 和 message 的变化
})
// 实际应用:自动取消之前的请求
const searchQuery = ref('')
watchEffect(async (onCleanup) => {
const query = searchQuery.value
const controller = new AbortController()
onCleanup(() => controller.abort()) // 清除副作用
if (query) {
const results = await fetchResults(query, {
signal: controller.signal
})
// 处理结果
}
})
}
三、生命周期全解析
3.1 Vue2 与 Vue3 生命周期对比
Vue2 生命周期 | Vue3 生命周期 (Options API) | Vue3 Composition API |
---|---|---|
beforeCreate | beforeCreate | setup() |
created | created | setup() |
beforeMount | beforeMount | onBeforeMount |
mounted | mounted | onMounted |
beforeUpdate | beforeUpdate | onBeforeUpdate |
updated | updated | onUpdated |
beforeDestroy | beforeUnmount | onBeforeUnmount |
destroyed | unmounted | onUnmounted |
3.2 组合式 API 生命周期使用
javascript
import { onMounted, onUnmounted } from 'vue'
setup() {
// 鼠标位置跟踪示例
const x = ref(0)
const y = ref(0)
const updatePosition = (e) => {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => {
window.addEventListener('mousemove', updatePosition)
})
onUnmounted(() => {
window.removeEventListener('mousemove', updatePosition)
})
return { x, y }
}
四、自定义 Hook 实践
自定义 Hook 是 Vue3 代码复用的利器:
4.1 鼠标位置跟踪 Hook
javascript
// hooks/useMousePosition.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useMousePosition() {
const x = ref(0)
const y = ref(0)
const updatePosition = (e) => {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => {
window.addEventListener('mousemove', updatePosition)
})
onUnmounted(() => {
window.removeEventListener('mousemove', updatePosition)
})
return { x, y }
}
// 在组件中使用
import { useMousePosition } from './hooks/useMousePosition'
setup() {
const { x, y } = useMousePosition()
return { x, y }
}
4.2 数据请求 Hook
javascript
// hooks/useFetch.js
import { ref, isRef, unref, watchEffect } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(false)
const fetchData = async () => {
loading.value = true
try {
const response = await fetch(unref(url))
data.value = await response.json()
error.value = null
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
if (isRef(url)) {
watchEffect(fetchData)
} else {
fetchData()
}
return { data, error, loading, retry: fetchData }
}
五、toRef 与 toRefs 深度解析
5.1 toRef 使用场景
javascript
import { reactive, toRef } from 'vue'
setup() {
const state = reactive({
name: 'Alice',
age: 25
})
// 保持响应式连接
const nameRef = toRef(state, 'name')
setTimeout(() => {
state.name = 'Bob' // nameRef 也会更新
}, 1000)
return {
nameRef // 可以在模板中直接使用
}
}
5.2 toRefs 解构响应式对象
javascript
import { reactive, toRefs } from 'vue'
setup() {
const state = reactive({
name: 'Alice',
age: 25,
address: {
city: 'Beijing'
}
})
// 解构后仍保持响应式
const { name, age } = toRefs(state)
// 嵌套对象需要单独处理
const { city } = toRefs(state.address)
return {
name,
age,
city
}
}
六、Vue3 新组件实战
6.1 Fragment 片段组件
Vue3 不再需要根标签:
vue
<template>
<header>...</header>
<main>...</main>
<footer>...</footer>
</template>
6.2 Teleport 传送门
将组件渲染到 DOM 中的其他位置:
vue
<template>
<button @click="showModal = true">打开弹窗</button>
<Teleport to="body">
<div v-if="showModal" class="modal">
<div class="modal-content">
<h2>标题</h2>
<p>内容...</p>
<button @click="showModal = false">关闭</button>
</div>
</div>
</Teleport>
</template>
<script>
import { ref } from 'vue'
export default {
setup() {
const showModal = ref(false)
return { showModal }
}
}
</script>
6.3 Suspense 异步组件
优雅处理异步组件加载状态:
vue
<template>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<div class="loading-spinner">
加载中...
</div>
</template>
</Suspense>
</template>
<script>
import { defineAsyncComponent } from 'vue'
const AsyncComponent = defineAsyncComponent(() =>
import('./components/AsyncComponent.vue')
)
export default {
components: {
AsyncComponent
}
}
</script>
七、Composition API 优势总结
-
更好的代码组织:相关功能代码集中在一起
-
更好的逻辑复用:通过自定义 Hook 实现
-
更好的类型推断:对 TypeScript 支持更友好
-
更小的生产包:Tree-shaking 友好
对比示例:
javascript
// Options API 方式
export default {
data() {
return {
count: 0,
searchQuery: ''
}
},
computed: {
filteredList() {
// 基于 searchQuery 过滤列表
}
},
methods: {
increment() {
this.count++
}
},
mounted() {
// 初始化代码
}
}
// Composition API 方式
import { ref, computed, onMounted } from 'vue'
export default {
setup() {
const count = ref(0)
const searchQuery = ref('')
const increment = () => {
count.value++
}
const filteredList = computed(() => {
// 过滤逻辑
})
onMounted(() => {
// 初始化代码
})
return {
count,
searchQuery,
increment,
filteredList
}
}
}
结语
Vue3 的 Composition API 为开发者提供了更灵活、更强大的代码组织方式。通过本文的详细解析和丰富示例,相信你已经掌握了其核心概念和使用技巧。在实际开发中,建议从简单功能开始逐步尝试组合式 API,体验其带来的开发效率提升和代码可维护性优势。