学习参考视频:尚硅谷Vue3入门到实战,最新版vue3+TypeScript前端开发教程_哔哩哔哩_bilibili
vue3学习目标:
| VUE 3 | 1、Vue3架构与设计理念 |
| 2、组合式API(Composition API) | |
| 3、常用API:ref、reactive、watch、computed | |
| 4、Vue3的生命周期 | |
| 5、组件间通信(props、emit、defineModel) | |
| 6、了解插槽 |
Vue 生命周期钩子详解(Vue 2 & Vue 3 对比)
生命周期概念图解
https://cn.vuejs.org/assets/lifecycle_zh-CN.W0MNXI0C.png
生命周期阶段对比表
| 阶段 | Vue 2 钩子 | Vue 3 钩子 | 触发时机 | 典型用途 |
|---|---|---|---|---|
| 创建 | beforeCreate | setup() | 组件实例刚被创建 | 初始化响应式数据 |
created | setup() | 数据观测/事件配置完成 | 异步请求数据 | |
| 挂载 | beforeMount | onBeforeMount | 模板编译/挂载前 | 访问/操作DOM前的最后机会 |
mounted | onMounted | 组件挂载到DOM后 | 操作DOM、初始化第三方库 | |
| 更新 | beforeUpdate | onBeforeUpdate | 数据变化,DOM更新前 | 获取更新前的DOM状态 |
updated | onUpdated | 虚拟DOM重新渲染后 | 执行依赖DOM的操作 | |
| 销毁 | beforeDestroy | onBeforeUnmount | 实例销毁前 | 清除定时器/取消订阅 |
destroyed | onUnmounted | 实例销毁后 | 清理内存/移除事件监听 |
Vue 3 生命周期示例
<template>
<div class="demo">
<h2>计数器: {{ count }}</h2>
<button @click="increment">增加</button>
<button @click="showComponent = false">卸载组件</button>
<ChildComponent v-if="showComponent" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUpdated, onUnmounted } from 'vue'
import ChildComponent from './ChildComponent.vue'
// 1. 创建阶段(setup)
const count = ref(0)
const showComponent = ref(true)
console.log('1. setup阶段 - 数据初始化完成')
// 3. 挂载阶段
onMounted(() => {
console.log('3. mounted - 组件已挂载到DOM')
// 这里可以安全地操作DOM
document.title = `当前计数: ${count.value}`
})
// 4. 更新阶段
onUpdated(() => {
console.log('4. updated - DOM已更新')
// 避免在这里修改状态,可能导致无限循环!
})
// 方法
function increment() {
count.value++
console.log('2. 数据变化触发更新流程')
}
// 子组件示例
const ChildComponent = {
setup() {
onMounted(() => {
console.log('子组件 mounted')
})
onUnmounted(() => {
console.log('子组件 unmounted')
})
return {}
},
template: '<div>子组件内容</div>'
}
</script>
生命周期执行顺序演示
-
组件初始化时:
1. setup阶段 - 数据初始化完成 3. mounted - 组件已挂载到DOM -
点击"增加"按钮时:
2. 数据变化触发更新流程 4. updated - DOM已更新 -
点击"卸载组件"时:
子组件 unmounted

挂载阶段完整顺序(父 → 子)
- 父组件
setup()执行 - 父组件
onBeforeMount - 子组件
setup()执行 - 子组件
onBeforeMount - 子组件
onMounted(子组件先完成挂载) - 父组件
onMounted(父组件最后完成挂载)
常用钩子使用场景
1. onMounted - 最常用
onMounted(async () => {
// 加载初始数据
const response = await fetch('/api/data')
data.value = await response.json()
// 初始化图表
chart = new Chart(document.getElementById('chart'), {
// 配置项
})
})
2. onUpdated - 谨慎使用
onUpdated(() => {
// 典型用例:自动滚动到底部
listContainer.value.scrollTop = listContainer.value.scrollHeight
})
3. onUnmounted - 资源清理
onUnmounted(() => {
// 清除定时器
clearInterval(timer)
// 取消事件监听
window.removeEventListener('resize', handleResize)
// 销毁第三方库实例
chart?.destroy()
})
Vue 2 与 Vue 3 生命周期对比
关键区别:
- Vue 3 使用
setup替代了beforeCreate和created - 所有钩子都需要显式导入
- 命名更语义化(unmount 替代 destroy)
- 组合式API让相关代码更集中
补充 自定义Hooks
什么是自定义 Hook?
自定义 Hook 是 Vue 3 组合式 API 的核心实践方式,本质是一个封装了响应式逻辑的函数。它解决了两个核心问题:
- 逻辑复用 - 避免重复代码
- 关注点分离 - 让组件更专注于模板渲染
就比如,在写到一个组件里面存着在不同逻辑的数据和处理数据的方法时,全部都混用了
<template>
<div class="hooks-test">
<h2>当前求和为:{{sum}}</h2>
<button @click="add">点我+1</button>
<hr>
<img v-for="(dog,index) in dogList" :src="dog" :key="index">
<br>
<button @click="getDog">再来一只狗</button>
</div>
</template>
<script lang="ts" setup>
import { ref,reactive } from 'vue'
import axios from 'axios'
let sum = ref(0)
let dogList = reactive([
'https:\/\/images.dog.ceo\/breeds\/pembroke\/n02113023_2390.jpg'
])
function add(){
sum.value += 1
}
// function getDog(){
// axios.get('https://dog.ceo/api/breeds/image/random')
// .then(res => {
// dogList.push(res.data.message)
// })
// }
async function getDog(){
try {
let result = await axios.get('https://dog.ceo/api/breeds/image/random')
dogList.push(result.data.message)
} catch (error) {
alert(error)
}
}
</script>
<style scoped>
.hooks-test{
background-color: beige;
padding: 20px;
text-align: center;
border: 1px solid black;
}
img{
height: 200px;
margin: 20px;
}
</style>
图示代码又处理数字数据又处理狗狗数据,数据和处理逻辑没有分开,导致代码阅读时有一定的障碍,这个时候我们就可以使用hooks自定义,将实现同一个功能所需要的数据和方法单独封装在一起
Hook 基础结构
首先注意,hook命名规范是useXxxx.js/ts
1. 计数器 Hook 示例 (useCounter.ts)
import { ref, onMounted } from 'vue'
export default function(initialValue = 0) {
// 响应式数据
const count = ref(initialValue)
// 操作方法
const increment = () => count.value++
const decrement = () => count.value--
const reset = () => count.value = initialValue
// 生命周期
onMounted(() => {
console.log('计数器Hook已挂载')
})
// 暴露给组件的API
return {
count,
increment,
decrement,
reset
}
}
2. 异步数据 Hook 示例 (useFetch.ts)
import { reactive, onMounted } from 'vue'
import axios from 'axios'
export default function(url: string) {
const state = reactive({
data: null as any,
loading: false,
error: null as Error | null
})
const fetchData = async () => {
state.loading = true
try {
const response = await axios.get(url)
state.data = response.data
} catch (err) {
state.error = err as Error
} finally {
state.loading = false
}
}
onMounted(fetchData)
return {
...state,
refetch: fetchData
}
}
组件中使用 Hook
1. 基础用法
<template>
<div>
<p>Count: {{ counter.count }}</p>
<button @click="counter.increment">+</button>
<button @click="counter.decrement">-</button>
<button @click="counter.reset">Reset</button>
</div>
</template>
<script setup lang="ts">
import useCounter from '@/hooks/useCounter'
const counter = useCounter(10) // 初始值10
</script>
2. 组合多个 Hook
<template>
<div>
<!-- 计数器 -->
<div>{{ counter.count }}</div>
<!-- 异步数据 -->
<div v-if="posts.loading">Loading...</div>
<div v-else>{{ posts.data }}</div>
</div>
</template>
<script setup lang="ts">
import useCounter from '@/hooks/useCounter'
import useFetch from '@/hooks/useFetch'
const counter = useCounter()
const posts = useFetch('/api/posts')
</script>
最佳实践
1. 命名规范
- 始终以
use开头 (useXxx) - 一个文件只包含一个 Hook
- 放在
src/hooks目录下
2. 类型安全
// useUser.ts
import { ref } from 'vue'
interface User {
id: number
name: string
email: string
}
export default function() {
const user = ref<User | null>(null)
return {
user
}
}
3. 复杂 Hook 示例 (usePagination.ts)
import { reactive, computed } from 'vue'
export default function(totalItems: number, perPage = 10) {
const state = reactive({
currentPage: 1,
perPage
})
const totalPages = computed(() =>
Math.ceil(totalItems / state.perPage)
)
const nextPage = () => {
if (state.currentPage < totalPages.value) {
state.currentPage++
}
}
return {
...state,
totalPages,
nextPage
}
}
与 Vue 2 Mixins 对比
| 特性 | 自定义 Hook | Mixins |
|---|---|---|
| 代码组织 | 按功能组织 | 按选项合并 |
| 命名冲突 | 无(可重命名解构) | 容易冲突 |
| 类型支持 | 完整 TypeScript 支持 | 支持有限 |
| 逻辑复用 | 显式导入 | 全局混入 |
| 调试 | 清晰的调用栈 | 难以追踪逻辑来源 |
高级技巧
1. 本地存储 Hook (useLocalStorage.ts)
import { ref, watch } from 'vue'
export default function(key: string, defaultValue: any) {
const data = ref(JSON.parse(localStorage.getItem(key) || 'null') || defaultValue)
watch(data, (newVal) => {
localStorage.setItem(key, JSON.stringify(newVal))
}, { deep: true })
return data
}
2. 鼠标位置 Hook (useMouse.ts)
import { ref, onMounted, onUnmounted } from 'vue'
export default function() {
const x = ref(0)
const y = ref(0)
const update = (e: MouseEvent) => {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y }
}
常见问题解决
1. Hook 之间共享状态
// useSharedState.ts
import { reactive } from 'vue'
const state = reactive({
count: 0
})
export function useSharedCounter() {
const increment = () => state.count++
return {
count: state.count,
increment
}
}
2. 响应式解构问题
// ❌ 错误 - 失去响应性
const { count, increment } = useCounter()
// ✅ 正确 - 保持响应性
const counter = useCounter()
// 在模板中使用 counter.count
组件通信
为什么需要组件通信
在 Vue 中,组件是独立、可复用的模块,每个组件都有自己的数据和方法。但实际开发中,组件之间往往需要共享数据或相互影响,比如:
- 父组件 需要传递数据给 子组件(比如列表数据传给子组件渲染)。
- 子组件 需要通知 父组件 某些事件(比如按钮点击、表单提交)。
- 兄弟组件 之间需要同步状态(比如购物车的商品数量变化)。
组件通信方式以及原理
(1)Props(父传子)—— 像“传纸条”
原理:父组件通过属性(props)把数据传给子组件,子组件声明接收。
- 爸爸(父组件)给儿子(子组件)一张纸条(props),上面写着:“今天作业是数学”。
- 儿子必须提前说好:“我能接收纸条(
defineProps)”,才能看到内容。
<!-- 父组件(爸爸) -->
<Child :homework="mathHomework" />
<!-- 子组件(儿子) -->
<script setup>
const props = defineProps({
homework: String // 声明接收纸条内容
})
console.log(props.homework) // "数学"
</script>
(2)自定义事件(子传父)—— 像“按门铃”
原理:子组件触发事件(emit),父组件监听并处理。
- 儿子(子组件)写完作业后,按一下门铃(
emit)喊爸爸(父组件):“爸,我写完了!” - 爸爸提前在门口装了个监听器(
@done),听到后就去检查作业。
<!-- 子组件(儿子) -->
<button @click="$emit('done', '数学作业写完了')">提交作业</button>
<!-- 父组件(爸爸) -->
<Child @done="handleDone" />
<script setup>
function handleDone(message) {
console.log(message) // "数学作业写完了"
}
</script>
(3)v-model(双向绑定)—— 像“父子共享笔记本”
原理:v-model = props(父传子) + emit(子传父)的语法糖。
- 爸爸和儿子共用一个笔记本(
v-model),爸爸写一句话,儿子能看到;儿子修改后,爸爸也能同步看到。
<!-- 父组件 -->
<Child v-model="message" /> <!-- 相当于下面两行 -->
<!-- <Child :modelValue="message" @update:modelValue="newVal => message = newVal" /> -->
<!-- 子组件 -->
<script setup>
const modelValue = defineModel() // 直接绑定父组件的 v-model
</script>
<input v-model="modelValue" /> <!-- 修改会自动同步到父组件 -->
(4)事件总线(跨组件通信)—— 像“班级广播”
原理:用一个全局的“广播站”(mitt 或 $bus),任何组件都能发消息或收听。
- 班长(组件A)广播:“今天大扫除!”
- 所有同学(其他组件)只要提前订阅了广播,就能听到并行动。
// 事件中心(广播站)
import mitt from 'mitt'
const emitter = mitt()
// 组件A(班长):发广播
emitter.emit('clean', '擦黑板')
// 组件B(学生):提前订阅
emitter.on('clean', (task) => {
console.log(`收到任务:${task}`) // "擦黑板"
})
(5)$attrs(祖传孙)—— 像“快递代收点”
原理:父组件不处理的 props 会自动往下传递(像快递站不拆包,直接转给下一站)。
- 爷爷(祖组件)给爸爸(父组件)一箱水果(
props),但爸爸不吃,直接原样传给孙子(孙组件)。
<!-- 爷爷 -->
<Parent gift="苹果" />
<!-- 爸爸(不接收 gift,自动传给孙子) -->
<Child v-bind="$attrs" /> <!-- 相当于 <Child gift="苹果" /> -->
<!-- 孙子 -->
<script setup>
defineProps(['gift']) // 收到 "苹果"
</script>
(6)provide/inject(跨层直传)—— 像“家族信托基金”
原理:祖先组件提供数据(provide),后代组件直接领取(inject),中间组件不用管。
- 爷爷(祖组件)设立一笔“零花钱基金”(
provide),孙子(孙组件)可以直接领,不需要经过爸爸(父组件)。
// 爷爷(提供数据)
provide('allowance', 100)
// 孙子(直接领取)
const money = inject('allowance') // 拿到 100
(7)defineModel
有一篇很好的文章:一文搞懂 Vue3 defineModel 双向绑定:告别繁琐代码!随着vue3.4版本的发布,defineModel也 - 掘金
defineModel是否破坏了vue3的单向数据流呢?使用defineModel时,为什么子组件内没有任何关于prop - 掘金
核心作用
- 解决的问题:传统
v-model需要手动定义props和emit,代码冗余。 - 简化后的效果:一行代码即可实现双向绑定。
传统 v-model vs defineModel**
传统方式(Vue 3.3 之前)
<!-- 子组件 -->
<script setup>
const props = defineProps(['modelValue']) // 接收父组件的 v-model
const emit = defineEmits(['update:modelValue']) // 定义更新事件
function handleChange(e) {
emit('update:modelValue', e.target.value) // 通知父组件更新
}
</script>
<template>
<input
:value="props.modelValue"
@input="handleChange"
/>
</template>
使用 defineModel(Vue 3.3+)
<!-- 子组件 -->
<script setup>
const modelValue = defineModel() // 自动绑定父组件的 v-model
</script>
<template>
<input v-model="modelValue" /> <!-- 直接绑定,无需手动 emit -->
</template>
使用场景
场景 1:基础双向绑定
<!-- 父组件 -->
<ChildComponent v-model="message" />
<!-- 子组件 -->
<script setup>
const modelValue = defineModel() // 默认绑定父组件的 v-model
</script>
<template>
<input v-model="modelValue" />
</template>
场景 2:自定义 v-model 参数
支持修改 v-model 的参数名(默认是 modelValue):
<!-- 父组件 -->
<ChildComponent v-model:title="pageTitle" />
<!-- 子组件 -->
<script setup>
const title = defineModel('title') // 绑定 v-model:title
</script>
场景 3:多 v-model 绑定
<!-- 父组件 -->
<UserForm
v-model:name="userName"
v-model:age="userAge"
/>
<!-- 子组件 -->
<script setup>
const name = defineModel('name')
const age = defineModel('age')
</script>
实现原理
defineModel 在编译时会展开为以下代码:
// 编译后的等价代码
const props = defineProps({ modelValue: {} })
const emit = defineEmits(['update:modelValue'])
const modelValue = computed({
get() { return props.modelValue },
set(value) { emit('update:modelValue', value) }
})
所以它本质上仍是props + emit的封装。
高级用法
(1)添加类型和默认值
<script setup>
const modelValue = defineModel({
type: String, // 类型校验
default: '默认值' // 默认值
})
</script>
(2)修饰符处理
父组件使用修饰符(如 v-model.trim)时,可通过 defineModel 的返回值获取:
<script setup>
const [modelValue, modifiers] = defineModel()
// 如果父组件用 v-model.trim,modifiers 值为 { trim: true }
watchEffect(() => {
if (modifiers.trim) {
modelValue.value = modelValue.value.trim()
}
})
</script>
注意事项
- Vue 版本要求:必须 ≥ 3.3。
- 仅适用于
<script setup>:不能在 Options API 中使用。- 底层仍是单向数据流:只是简化了代码,数据更新的原则不变。
| 功能 | 传统方式 (props + emit) | defineModel |
|---|---|---|
| 代码量 | 需要手动定义 props 和 emit | 一行搞定 |
多 v-model* | 需定义多个 props/emit | 直接指定参数名 |
| 修饰符支持 | 需手动处理 | 自动暴露 modifiers |
| 类型检查 | 需单独配置 | 内置支持 |
插槽(Slots)
什么是插槽
插槽(Slot)是 Vue 组件的一种机制,允许父组件向子组件传递内容(HTML 结构、组件、文本等),子组件通过 <slot> 标签决定这些内容的显示位置。
让组件更灵活,可以动态填充内容,而不是写死在子组件内部。
什么时候需要用插槽
- 组件内容需要动态定制:一个弹窗组件,标题和按钮文字由父组件决定。
- 布局组件需要插入可变内容:一个卡片组件,卡片的头部、主体、底部由父组件定义。
- 需要子组件向父组件传递数据(作用域插槽):列表组件渲染数据,但父组件控制每一项的样式。
插槽的分类及示例
Vue 的插槽分为三类:默认插槽、具名插槽、作用域插槽。
(1)默认插槽(无名插槽)
- 特点:只有一个插槽,父组件的内容会全部填充到
<slot>的位置。 - 示例:一个按钮组件,允许父组件自定义按钮文字。
<!-- 子组件 MyButton.vue -->
<template>
<button class="my-btn">
<slot>默认文字</slot> <!-- 父组件不传内容时显示“默认文字” -->
</button>
</template>
<!-- 父组件 -->
<MyButton>提交</MyButton> <!-- 渲染:<button class="my-btn">提交</button> -->
(2)具名插槽(Named Slots)
- 特点:多个插槽通过
name区分,父组件用v-slot:name(或#name)指定内容分发给哪个插槽。 - 示例:一个卡片组件,分头部、主体、底部三部分。
<!-- 子组件 MyCard.vue -->
<template>
<div class="card">
<header>
<slot name="header"></slot> <!-- 具名插槽 -->
</header>
<main>
<slot></slot> <!-- 默认插槽 -->
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>
<!-- 父组件 -->
<MyCard>
<template #header>标题</template> <!-- v-slot:header 的简写 -->
<p>这里是主体内容</p> <!-- 默认插槽内容 -->
<template #footer>
<button>确定</button>
</template>
</MyCard>
(3)作用域插槽(Scoped Slots)
- 特点:子组件通过插槽向父组件传递数据,父组件可以自定义渲染逻辑。
- 示例:一个列表组件,子组件提供数据,父组件决定如何渲染每一项。
<!-- 子组件 MyList.vue -->
<template>
<ul>
<li v-for="item in items" :key="item.id">
<slot :item="item"></slot> <!-- 将 item 数据传给父组件 -->
</li>
</ul>
</template>
<script setup>
const items = [
{ id: 1, name: '苹果' },
{ id: 2, name: '香蕉' }
]
</script>
<!-- 父组件 -->
<MyList>
<template v-slot="slotProps"> <!-- 接收子组件传递的数据 -->
<span style="color: red">{{ slotProps.item.name }}</span>
</template>
</MyList>
<!-- 渲染结果:
<ul>
<li><span style="color: red">苹果</span></li>
<li><span style="color: red">香蕉</span></li>
</ul>
-->
注意
Q1:v-slot 可以简写吗?
- 可以!
v-slot:header简写为#header。
Q2:默认插槽和作用域插槽能一起用吗?
- 可以!但需要分开写:
<MyComponent> <template #default="slotProps">默认插槽内容</template> <template #footer>页脚</template> </MyComponent>
Q3:插槽内容能访问父组件的数据吗?
- 可以!插槽内容属于父组件的作用域,直接使用父组件的变量即可。
总结
| 插槽类型 | 作用 | 示例场景 |
|---|---|---|
| 默认插槽 | 父组件传递单一内容 | 按钮文字、弹窗内容 |
| 具名插槽 | 父组件分发内容到多个指定位置 | 卡片的头部、底部 |
| 作用域插槽 | 子组件提供数据,父组件自定义渲染逻辑 | 列表项渲染、表格自定义列 |
&spm=1001.2101.3001.5002&articleId=147424267&d=1&t=3&u=e397b1c0bc5c4e0ba9f5282de5ba16c7)
3825

被折叠的 条评论
为什么被折叠?



