Vue3 监听器与组件进阶
本页关键词:watch、watchEffect、ref 模板引用、defineExpose、TypeScript、defineProps、生命周期、自定义 Hook、Vue Router
一、Watch 与 watchEffect
watch:显式指定被监听的响应式源(可监听 ref、reactive、getter、数组等)。适合需获得旧值/新值或做异步/副作用清理的场景。watchEffect:立即执行一次,自动收集在回调中读取的响应式依赖;当依赖变化时自动重新运行。便捷但无法直接拿到旧值,需显式停止返回的stop。
watchEffect 示例
vue
<template>
<div>
<h3 id="demo">水温:{{ temp }} ℃,水位:{{ height }} cm</h3>
<button @click="temp += 10">温度 +10</button>
<button @click="height += 1">水位 +1</button>
</div>
</template>
<script setup lang="ts">
import { ref, watch, watchEffect } from 'vue'
const temp = ref(0)
const height = ref(0)
// watch:明确监听数组里的值
watch([temp, height], (values, oldValues) => {
const [newTemp, newHeight] = values
if (newTemp >= 50 || newHeight >= 20) {
console.log('watch: 联系服务器')
}
})
// watchEffect:自动跟踪依赖
const stop = watchEffect(() => {
if (temp.value >= 50 || height.value >= 20) {
console.log('watchEffect: 联系服务器')
}
if (temp.value >= 100 || height.value >= 50) {
console.log('watchEffect: 停止监控')
stop()
}
})
</script>面试要点:watch 显式指定监听源、可拿旧值;watchEffect 自动收集依赖、立即执行,需手动 stop 停止。
二、ref 模板引用与 defineExpose
- 把
ref放在普通 DOM 标签上,得到 DOM 节点引用(titleRef.value是 DOM)。 - 把
ref放在子组件标签上,得到子组件实例(默认实例有proxy成员和暴露的属性/方法)。 - 组件想让父组件通过
ref访问内部值/方法,需在子组件中用defineExpose明确暴露。
DOM ref 示例
vue
<template>
<div>
<h1 ref="titleRef">标题</h1>
<input ref="inputRef" />
<button @click="logRefs">打印 refs</button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const titleRef = ref<HTMLElement | null>(null)
const inputRef = ref<HTMLInputElement | null>(null)
function logRefs() {
console.log('DOM title:', titleRef.value?.innerText)
console.log('input value:', inputRef.value?.value)
}
</script>组件 ref + defineExpose 示例
vue
<!-- Child.vue -->
<script setup lang="ts">
import { ref, defineExpose } from 'vue'
const name = ref('张三')
const age = ref(18)
function greet() { return `Hello ${name.value}` }
// 暴露给父组件
defineExpose({ name, age, greet })
</script>
<template>
<div>{{ name }} - {{ age }}</div>
</template>vue
<!-- Parent.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import Child from './Child.vue'
const childRef = ref<any>(null)
function inspectChild() {
console.log('child name:', childRef.value?.name) // '张三'
console.log('child greet:', childRef.value?.greet()) // 'Hello 张三'
}
</script>
<template>
<Child ref="childRef" />
<button @click="inspectChild">检查子组件</button>
</template>面试要点:
ref是模板引用指令,不是 prop;子组件需用defineExpose暴露才能被父组件通过 ref 访问。
三、TypeScript 类型与泛型
interface/type:用来声明数据结构与复用类型。export导出以便组件/模块间复用。- 泛型(generic):使类型可参数化,提高复用性。可用于
reactive<T>()、函数等。 - 在 Props、hooks、API 返回值等处推荐显式声明类型。
类型与泛型示例
ts
// types.ts
export interface PersonInter {
id: string
name: string
age: number
}
export type Persons = PersonInter[]
// 泛型示例:reactive 指定类型
import { reactive } from 'vue'
const persons = reactive<Persons>([
{ id: 'a1', name: '张三', age: 18 }
])
// 可选字段:x?: number
export interface MaybeNumber {
x?: number
}四、defineProps 与 withDefaults
defineProps<T>():在script setup中声明 props 的类型。withDefaults(defineProps<...>(), { ... }):指定默认值(当 props 可选时)。defineProps也可传字符串数组或对象做更细校验(TS 下更常用泛型/类型声明)。
常见写法
vue
<script setup lang="ts">
import { withDefaults, defineProps } from 'vue'
import type { Persons } from '@/types'
// 3) 接收 + 类型可选 + 默认值
const props = withDefaults(
defineProps<{ list?: Persons }>(),
{
list: () => [{ id: 'default01', name: '默认', age: 99 }]
}
)
</script>
<template>
<ul>
<li v-for="item in props.list" :key="item.id">{{ item.name }} - {{ item.age }}</li>
</ul>
</template>面试要点:
ref是模板引用指令,不是 prop。传响应式数据用:data="reactiveObj"或:count="count",在子组件defineProps中声明类型。
五、生命周期(Vue3 钩子与执行顺序)
- Vue 生命周期四阶段:创建(setup) → 挂载 → 更新 → 卸载。
- Vue3 对应钩子(每阶段前后各一个):
- 创建:
setup(执行最先) - 挂载:
onBeforeMount、onMounted - 更新:
onBeforeUpdate、onUpdated - 卸载:
onBeforeUnmount、onUnmounted
- 创建:
- 挂载顺序:父创建 → 子创建 → 子挂载 → 父挂载。
生命周期示例
vue
<script setup lang="ts">
import { ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue'
const sum = ref(0)
function inc() { sum.value++ }
console.log('setup 执行')
onBeforeMount(() => console.log('onBeforeMount'))
onMounted(() => console.log('onMounted'))
onBeforeUpdate(() => console.log('onBeforeUpdate'))
onUpdated(() => console.log('onUpdated'))
onBeforeUnmount(() => console.log('onBeforeUnmount'))
onUnmounted(() => console.log('onUnmounted'))
</script>
<template>
<div>
<p>sum: {{ sum }}</p>
<button @click="inc">+1</button>
</div>
</template>六、自定义 Hook(复用逻辑 / 封装副作用)
- 本质:把
setup中的组合逻辑封装为函数(通常放src/hooks),返回数据与方法以供组件使用。 - 优点:解耦、复用、单元测试友好。
简单 Hook 示例(useSum / useDog)
ts
// src/hooks/useSum.ts
import { ref, onMounted } from 'vue'
export default function useSum() {
const sum = ref(0)
const increment = () => sum.value++
const decrement = () => sum.value--
onMounted(() => increment())
return { sum, increment, decrement }
}ts
// src/hooks/useDog.ts
import { reactive, onMounted } from 'vue'
import axios from 'axios'
export default function useDog() {
const dogList = reactive<{ urlList: string[]; isLoading: boolean }>({
urlList: [],
isLoading: false
})
async function getDog() {
dogList.isLoading = true
try {
const { data } = await axios.get('https://dog.ceo/api/breed/pembroke/images/random')
dogList.urlList.push(data.message)
} catch (err) {
console.error(err)
} finally {
dogList.isLoading = false
}
}
onMounted(getDog)
return { dogList, getDog }
}vue
<!-- 组件使用 -->
<script setup lang="ts">
import useSum from '@/hooks/useSum'
import useDog from '@/hooks/useDog'
const { sum, increment, decrement } = useSum()
const { dogList, getDog } = useDog()
</script>七、Vue Router 4 基础
基本概念
- 路由:路径(key)和组件(value)之间的映射关系。
- SPA(单页应用):通过路由切换组件视图而不做完整页面刷新。
安装与创建(TypeScript)
ts
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/pages/Home.vue'
import News from '@/pages/News.vue'
import About from '@/pages/About.vue'
const routes = [
{ path: '/home', name: 'home', component: Home },
{ path: '/news', name: 'news', component: News },
{ path: '/about', name: 'about', component: About }
]
const router = createRouter({
history: createWebHistory(), // 或 createWebHashHistory()
routes
})
export default router在 main.ts 中安装:
ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
createApp(App).use(router).mount('#app')App.vue 使用 <RouterLink> 与 <RouterView>:
vue
<template>
<nav>
<RouterLink to="/home">首页</RouterLink>
<RouterLink :to="{ name: 'news' }">新闻</RouterLink>
<RouterLink to="/about">关于</RouterLink>
</nav>
<RouterView />
</template>history vs hash
history:更美观(无#),需服务器支持路由重写(否则刷新 404)。hash:兼容更好,不需服务端配置,但 URL 会带#。
路由传参:query 与 params
query(URL 中?a=1&b=2):接收用useRoute().query。params(基于 path 占位符/detail/:id):传params时建议用命名路由(name),接收用useRoute().params。
vue
<!-- query 传参 -->
<RouterLink :to="{ path: '/news/detail', query: { id: 1, title: 'xxx' } }">跳转</RouterLink>
<!-- params 传参(命名路由) -->
<RouterLink :to="{ name: 'detail', params: { id: 1 } }">详情</RouterLink>路由 props 配置
props: true:将params作为 props。props: (route) => ({ ...route.query }):自定义转换。props: { a: 1 }:静态对象作为 props。
ts
{
name: 'detail',
path: '/news/detail/:id',
component: Detail,
props: route => ({ id: route.params.id, q: route.query.q })
}嵌套路由
- 在父路由配置
children: [...],子路由 path 不以/开头。 - 父组件中需包含
<RouterView />作为子路由容器。
ts
{
path: '/news',
component: News,
children: [
{ path: 'detail', name: 'newsDetail', component: Detail }
]
}编程式导航
ts
import { useRouter } from 'vue-router'
const router = useRouter()
router.push({ name: 'news' }) // 入栈
router.replace({ name: 'home' }) // 替换当前记录八、常见问题汇总
defineExpose:父组件通过ref访问子组件时,子组件需显式暴露属性/方法。defineProps与withDefaults:常配合使用以提供类型与默认值。ref在模板上是“引用”,不是 prop;要把值传下去需要:(绑定)。- 解构
reactive的对象会导致响应性丢失,使用toRefs/toRef转换后再解构。 - 自定义 Hook 放
src/hooks,保持纯粹、低耦合。 - 路由组件通常放
pages或views,普通复用组件放components。 - 使用
history模式部署时,需配置服务端 fallback(路由重写)。