Vue 3 学习笔记
目录
- Vue 3 与 Vue 2 的区别
- Vite
- 组合式 API 基础
- Vue Router
- Pinia
- 组件通信
- 响应式进阶 API
- Vue 3 新组件与全局 API
1. Vue 3 与 Vue 2 的区别
1.1 性能提升
- 打包体积更小、渲染更快、内存占用更少。
1.2 源码升级
- 使用
Proxy替代Object.defineProperty实现响应式,能监听属性新增 / 删除、数组索引与length变化。 - 重写虚拟 DOM 的实现,并支持
Tree-Shaking(按需引入,未使用的 API 不进入产物)。
1.3 更好地支持 TypeScript
源码使用 TypeScript 重写,类型推导与 IDE 提示更完整。
1.4 新特性:选项式 API → 组合式 API
setup语法糖(<script setup>):允许把同一个功能逻辑写在一起,不再按data/methods/computed拆散。ref与reactive:定义响应式数据。computed与watch:计算属性与侦听器。- 新内置组件:
Fragment(支持多根节点)、Teleport、Suspense。
1.5 其他变化
- 生命周期钩子重命名:
beforeDestroy→beforeUnmount,destroyed→unmounted。 - 移除实例方法
$on、$off、$once,官方推荐使用mitt等外部库替代事件总线。 - 创建应用的方式从
new Vue()变为createApp()(从vue导入)。 - 过渡类名
v-enter改为v-enter-from,v-leave改为v-leave-from。 - 移除
keyCode作为v-on修饰符的支持,改用按键别名(如@keyup.enter)。 - 组件上的
v-model被重新设计,替换掉v-bind.sync,改用带参数的v-model。 v-if和v-for用在同一个元素上时,优先级发生变化:Vue 3 中v-if优先于v-for。- 移除过滤器
filter,改用计算属性或方法。 - 移除
$children实例属性(property)。
2. Vite
2.1 特点
- 支持 TS、JSX、CSS 等,开箱即用;轻量快速。
- 热更新(HMR),按需编译,只更新修改的模块。
2.2 Vite 与 Webpack 的区别
Webpack 是「先打包再启动」,Vite 是「先启动再按需编译」。
| Webpack | Vite | |
|---|---|---|
| 启动 | 先打包全部模块,再启动开发服务器 | 先启动开发服务器,浏览器请求到某模块时才编译 |
| 依据 | 打包后的 bundle | 浏览器原生 ESM |
| 体验 | 项目越大冷启动越慢 | 冷启动快,HMR 只更新改动模块 |
补充:Vite 的「不打包」只针对开发阶段;生产构建仍会用 Rollup 打包。
3. 组合式 API 基础
3.1 setup
setup函数会在beforeCreate之前调用,领先所有生命周期钩子执行。setup返回的对象中的内容,可直接在模板中使用。
3.2 toRefs 与 toRef
作用 :将一个响应式对象中的每一个属性转换为 ref 对象。
ts
let person = reactive({ name: '张三', age: 18, gender: '男' })
// 通过 toRefs 将 person 对象中的 n 个属性批量取出,且依然保持响应式能力
let { name, gender } = toRefs(person)
// 通过 toRef 将 person 对象中的 age 属性取出,且依然保持响应式能力
let age = toRef(person, 'age')
toRefs与toRef功能一致,但toRefs可以批量转换。
3.3 computed
computed追踪函数内部的所有响应式数据进行计算,只要其中一个发生变化就会重新计算;带有缓存机制。- 只读取、不修改时,直接写简写形式:
ts
const doubleSum = computed(() => sum.value * 2)
- 希望直接修改这个计算属性,并且修改能反向影响它依赖的源数据 时,需要写成对象形式,在
computed里提供get和set:
ts
const fullName = computed({
get() {
return firstName.value + '-' + lastName.value
},
set(value: string) {
const [first, last] = value.split('-')
firstName.value = first
lastName.value = last
}
})
怎么选 :需要根据已有数据「算」出一个新值且希望有缓存,用 computed;需要响应用户操作(如点击)执行动作,用 methods(或直接定义函数);需要监听某个数据变化后去做别的事(比如请求接口),用 watch。
3.4 watch
作用 :监视数据的变化(与 Vue 2 中的 watch 作用一致)。
Vue 3 中 watch 的第一个参数只能是以下四种数据:
ref定义的数据。reactive定义的数据(默认是深度监视)。- 函数返回一个值(
getter函数)。 - 一个包含上述内容的数组。
情况一:监视 ref 定义的【基本类型】数据
ts
const stopWatch = watch(sum, (newValue, oldValue) => {
console.log('sum变化了', newValue, oldValue)
if (newValue >= 10) {
stopWatch()
}
})
watch API 返回一个停止函数(stop handle),调用它就会停止监听。回调执行时 stopWatch 已经完成赋值,所以可以在回调内部通过闭包直接调用它。
第三个参数:配置对象
ts
watch(source, callback, { deep: true, immediate: true })
deep:深度监视。immediate:立即执行,在初始绑定时就先执行一次。
监视对象里的某个属性:最好写成函数式(getter)
ts
watch(() => person.car, (newValue, oldValue) => {
console.log('person.car变化了', newValue, oldValue)
}, { deep: true })
注意点:直接监视一个
reactive对象时默认就是深度监视;但用 getter 返回一个对象时,默认只比较引用值(地址),要关注对象内部属性的变化,需要手动开启deep: true。
3.5 watch 对比 watchEffect
两者都能监听响应式数据的变化,不同的只是监听数据的方式:
| watch | watchEffect | |
|---|---|---|
| 指定数据源 | 要明确指出监视的数据 | 不用明确指出,函数中用到哪些属性就监视哪些 |
| 执行时机 | 默认懒执行,数据变化才回调 | 立即执行一次,之后依赖变化再执行 |
| 拿到的值 | 能拿到 newValue / oldValue |
拿不到旧值 |
3.6 标签的 ref 属性
作用:用于注册模板引用。
- 用在普通
DOM标签上,获取的是DOM节点。 - 用在组件标签上,获取的是组件实例对象。
3.7 自定义 hook
- 本质是一个函数,里面封装了状态(数据)和操作状态的逻辑(方法)。
- 约定 以
use开头(如useUser、useFetch),本身是一个普通 JS 函数,并非语法强制。 - 内部可以使用 Vue 的 API,最后返回一个数据或者函数供组件使用。
4. Vue Router
4.1 基本使用
在 main.ts 里挂载 router:
ts
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/home', component: Home },
{ path: '/about', component: About }
]
})
vue
<RouterLink to="/home" active-class="active">首页</RouterLink>
<RouterView></RouterView> <!-- 路由出口 -->
- 路由组件通常存放在
pages或views文件夹,一般组件通常存放在components文件夹。 - 通过点击导航,视觉效果上「消失」了的路由组件,默认是被卸载掉的,需要的时候再去挂载(可用
KeepAlive缓存)。
4.2 路由工作模式
| 模式 | 创建函数 | URL | 注意点 |
|---|---|---|---|
| history | createWebHistory() |
不带 # |
后期项目上线需要服务端配合处理路径,否则刷新 404 |
| hash | createWebHashHistory() |
带 # |
无需服务端配合,但在 SEO 优化方面较差 |
4.3 命名路由与 to 的两种写法
命名路由可以简化路由跳转及传参。
vue
<!-- 第一种:to 的字符串写法 -->
<router-link active-class="active" to="/home">主页</router-link>
<!-- 第二种:to 的对象写法,可以传 path,也可以传 name -->
<router-link active-class="active" :to="{ path: '/home' }">Home</router-link>
<router-link :to="{ name: 'xiang' }">详情</router-link>
4.4 路由传参:query 参数
1. 传递参数
vue
<!-- 跳转并携带 query 参数(to 的字符串写法) -->
<router-link to="/news/detail?a=1&b=2&content=欢迎你">
跳转
</router-link>
<!-- 跳转并携带 query 参数(to 的对象写法) -->
<RouterLink
:to="{
// name:'xiang', // 用 name 也可以跳转
path: '/news/detail',
query: {
id: news.id,
title: news.title,
content: news.content
}
}"
>
{{ news.title }}
</RouterLink>
2. 接收参数
ts
import { useRoute } from 'vue-router'
const route = useRoute()
// 打印 query 参数
console.log(route.query)
4.5 路由传参:params 参数
1. 传递参数
vue
<!-- 跳转并携带 params 参数(to 的字符串写法) -->
<RouterLink :to="`/news/detail/001/新闻001/内容001`">{{ news.title }}</RouterLink>
<!-- 跳转并携带 params 参数(to 的对象写法) -->
<RouterLink
:to="{
name: 'xiang', // 用 name 跳转
params: {
id: news.id,
title: news.title,
content: news.content
}
}"
>
{{ news.title }}
</RouterLink>
2. 接收参数
ts
import { useRoute } from 'vue-router'
const route = useRoute()
// 打印 params 参数
console.log(route.params)
备注 1:传递
params参数时,若使用to的对象写法,必须使用name配置项,不能用path。备注 2:传递
params参数时,需要提前在路由规则中占位(如detail/:id/:title/:content)。Vue Router 4 中,未在规则里声明的 params 会被忽略。
4.6 路由的 props 配置
js
{
name: 'xiang',
path: 'detail/:id/:title/:content',
component: Detail,
// props 的对象写法,作用:把对象中的每一组 key-value 作为 props 传给 Detail 组件
// props: { a: 1, b: 2, c: 3 },
// props 的布尔值写法,作用:把收到的每一组 params 参数,作为 props 传给 Detail 组件
// props: true,
// props 的函数写法,作用:把返回的对象中每一组 key-value 作为 props 传给 Detail 组件
props(route) {
return route.query
}
}
4.7 编程式导航
ts
import { useRoute, useRouter } from 'vue-router'
const route = useRoute() // 当前路由信息
const router = useRouter() // 路由器实例
console.log(route.query)
console.log(route.params)
console.log(router.push)
console.log(router.replace)
route只能看不能改,用来获取当前页面信息。router可以进行路由导航,修改路由地址进行页面跳转。
4.8 重定向
重定向就是将特定的路径重新定位到已有路由,用 redirect 配置。例如检测到用户没登录,将用户导航到登录页。
js
{ path: '/', redirect: '/home' }
5. Pinia
管理全局数据的仓库。
5.1 三个核心概念
Store是一个保存状态 、业务逻辑 的实体,每个组件都可以读取 、写入它。- 它有三个概念:
state、getter、action,相当于组件中的data、computed和methods。 - 定义时给
defineStore传一个唯一 id(如'count'),并导出useXxxxxStore。
5.2 定义 store
具体编码:src/store/count.ts
ts
// 引入 defineStore 用于创建 store
import { defineStore } from 'pinia'
// 定义并暴露一个 store
export const useCountStore = defineStore('count', {
// 动作
actions: {},
// 状态
state() {
return {
sum: 6
}
},
// 计算
getters: {}
})
具体编码:src/store/talk.ts
ts
// 引入 defineStore 用于创建 store
import { defineStore } from 'pinia'
// 定义并暴露一个 store
export const useTalkStore = defineStore('talk', {
// 动作
actions: {},
// 状态
state() {
return {
talkList: [
{ id: 'yuysada01', content: '你今天有点怪,哪里怪?怪好看的!' },
{ id: 'yuysada02', content: '草莓、蓝莓、蔓越莓,你想我了没?' },
{ id: 'yuysada03', content: '心里给你留了一块地,我的死心塌地' }
]
}
},
// 计算
getters: {}
})
5.3 组件中使用 state 中的数据
vue
<template>
<h2>当前求和为:{{ sumStore.sum }}</h2>
</template>
<script setup lang="ts" name="Count">
// 引入对应的 useXxxxxStore
import { useSumStore } from '@/store/sum'
// 调用 useXxxxxStore 得到对应的 store
const sumStore = useSumStore()
</script>
vue
<template>
<ul>
<li v-for="talk in talkStore.talkList" :key="talk.id">
{{ talk.content }}
</li>
</ul>
</template>
<script setup lang="ts" name="Talk">
import { useTalkStore } from '@/store/talk'
const talkStore = useTalkStore()
</script>
注意:课程示例里
count.ts导出的是useCountStore,组件中却写useSumStore/@/store/sum,属于示例命名不一致。实际项目里文件名、store id、导出名、导入路径要保持一致。
5.4 修改数据(三种方式)
第一种:直接修改
ts
countStore.sum = 666
第二种:批量修改 $patch
ts
countStore.$patch({
sum: 999,
school: 'atguigu'
})
第三种:借助 action 修改 (action 中可以编写一些业务逻辑)
ts
import { defineStore } from 'pinia'
export const useCountStore = defineStore('count', {
/*************/
actions: {
// 加
increment(value: number) {
if (this.sum < 10) {
// 操作 countStore 中的 sum
this.sum += value
}
},
// 减
decrement(value: number) {
if (this.sum > 1) {
this.sum -= value
}
}
},
/*************/
})
组件中调用 action 即可:
ts
// 使用 countStore
const countStore = useCountStore()
// 调用对应 action
countStore.increment(n.value)
5.5 常用配套 API
storeToRefs:将 store 中的数据转为ref对象,解构后不丢失响应式,方便在模板中使用。$subscribe():通过 store 的该方法侦听state及其变化。nanoid():轻量的唯一 ID 生成库(不是组件库),常用来给列表项生成 id。- 使用 setup 语法定义 store 时,只有
return出去的东西,组件里才能使用;选项式defineStore则由state/getters/actions自动暴露。
5.6 Pinia 与 mitt 的区别
| Pinia | mitt | |
|---|---|---|
| 适合 | 管理长期存在的全局状态(如用户登录信息、购物车) | 处理瞬时的动作通知 |
| 是否保存数据 | 保存并管理状态,组件可读写 | 不保存状态,只转发事件 |
| 典型场景 | 多组件共享同一份数据 | 一次性的跨组件事件通知 |
6. 组件通信
6.1 props:父组件向子组件传递数据
父组件里,在引入子组件的地方写:
vue
<Person :list="persons" />
persons 就是要传的值,list 是在子组件里定义的属性名。
子组件的三种接收写法:
ts
// 第一种写法:仅接收
// list 是子组件内部通过 defineProps 声明好要接收的属性名
const props = defineProps(['list'])
// 第二种写法:接收 + 限制类型
defineProps<{ list: Persons }>()
// 第三种写法:接收 + 限制类型 + 指定默认值 + 限制必要性
let props = withDefaults(defineProps<{ list?: Persons }>(), {
list: () => [{ id: 'asdasg01', name: '小猪佩奇', age: 18 }]
})
注意:HTML 原生属性对大小写不敏感。所以父组件传值时,如果子组件声明的是驼峰命名(比如
userList),父组件写的时候通常要改成短横线分隔(:user-list="users")。
6.2 emit:子传父
子组件里 defineEmits,然后再调用:
ts
const emit = defineEmits(['sendMsg'])
emit('sendMsg', msg.value)
父组件里监听 sendMsg 事件,触发时执行 getData 函数:
vue
<Son @send-msg="getData" />
6.3 mitt:任意组件通信
新建一个文件(比如 src/utils/bus.ts),把 mitt 实例化并导出去,这样所有组件都能共用这一个实例。
ts
// src/utils/bus.ts
import mitt from 'mitt'
// 定义事件类型(可选,但结合 TS 很好用)
type Events = {
sendMsg: string // 事件名:sendMsg,传参类型:字符串
updateUser: { id: number, name: string } // 事件名:updateUser,传参类型:对象
}
const bus = mitt<Events>()
export default bus
ComponentA.vue(发送)
vue
<template>
<button @click="sendMessage">给组件B发消息</button>
</template>
<script setup lang="ts">
import bus from '@/utils/bus'
function sendMessage() {
// 使用 bus.emit('事件名', 传递的数据)
bus.emit('sendMsg', '你好,我是组件A!')
}
</script>
ComponentB.vue(接收)
vue
<template>
<div>接收到的消息:{{ msg }}</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import bus from '@/utils/bus'
const msg = ref('暂无消息')
// 必须在组件挂载时开始监听
onMounted(() => {
bus.on('sendMsg', (data) => {
console.log('组件B收到了:', data)
msg.value = data
})
})
// 一定要在 onUnmounted 生命周期里关掉它,否则会内存泄漏
onUnmounted(() => {
// 关闭指定事件的监听
bus.off('sendMsg')
// 或者清空所有事件监听:
// bus.all.clear()
})
</script>
6.4 v-model:父子通信
组件标签上的 v-model 的本质::modelValue 加 update:modelValue 事件。
6.5 $attrs:祖 → 孙
$attrs 用于实现当前组件的父组件,向当前组件的子组件通信(祖 → 孙)。
$attrs 是一个对象,包含所有父组件传入、但未被当前组件 props 声明接收的标签属性。中间组件用 v-bind="$attrs" 一次性透传即可。
父组件:
vue
<template>
<div class="father">
<h3>父组件</h3>
<Child :a="a" :b="b" :c="c" :d="d" v-bind="{x:100,y:200}" :updateA="updateA" />
</div>
</template>
<script setup lang="ts" name="Father">
import Child from './Child.vue'
import { ref } from 'vue'
let a = ref(1)
let b = ref(2)
let c = ref(3)
let d = ref(4)
function updateA(value: number) {
a.value = value
}
</script>
子组件:
vue
<template>
<div class="child">
<h3>子组件</h3>
<GrandChild v-bind="$attrs" />
</div>
</template>
<script setup lang="ts" name="Child">
import GrandChild from './GrandChild.vue'
</script>
孙组件:
vue
<template>
<div class="grand-child">
<h3>孙组件</h3>
<h4>a:{{ a }}</h4>
<h4>b:{{ b }}</h4>
<h4>c:{{ c }}</h4>
<h4>d:{{ d }}</h4>
<h4>x:{{ x }}</h4>
<h4>y:{{ y }}</h4>
<button @click="updateA(666)">点我更新A</button>
</div>
</template>
<script setup lang="ts" name="GrandChild">
defineProps(['a', 'b', 'c', 'd', 'x', 'y', 'updateA'])
</script>
注意:中间的子组件中不用编写任何东西,是不受到任何打扰的。
6.6 refs / parent
$refs用于:父 → 子。$parent用于:子 → 父。
| 属性 | 说明 |
|---|---|
$refs |
值为对象,包含所有被 ref 属性标识的 DOM 元素或组件实例 |
$parent |
值为对象,当前组件的父组件实例对象 |
补充:在
<script setup>中这两个实例属性不会直接暴露,需要getCurrentInstance();实际项目更推荐用模板 ref、props / emit 或 provide / inject。
6.7 provide / inject
概述 :实现祖孙组件直接通信。
具体使用:
- 在祖先组件中通过
provide配置向后代组件提供数据。 - 在后代组件中通过
inject配置来声明接收数据。
【第一步】父组件中,使用 provide 提供数据
vue
<template>
<div class="father">
<h3>父组件</h3>
<h4>资产:{{ money }}</h4>
<h4>汽车:{{ car }}</h4>
<button @click="money += 1">资产+1</button>
<button @click="car.price += 1">汽车价格+1</button>
<Child />
</div>
</template>
<script setup lang="ts" name="Father">
import Child from './Child.vue'
import { ref, reactive, provide } from 'vue'
// 数据
let money = ref(100)
let car = reactive({
brand: '奔驰',
price: 100
})
// 用于更新 money 的方法
function updateMoney(value: number) {
money.value += value
}
// 提供数据
provide('moneyContext', { money, updateMoney })
provide('car', car)
</script>
注意:中间子组件中不用编写任何东西,是不受到任何打扰的。
【第二步】孙组件中使用 inject 接收数据
vue
<template>
<div class="grand-child">
<h3>我是孙组件</h3>
<h4>资产:{{ money }}</h4>
<h4>汽车:{{ car }}</h4>
<button @click="updateMoney(6)">点我</button>
</div>
</template>
<script setup lang="ts" name="GrandChild">
import { inject } from 'vue'
// 注入数据,第二个参数是找不到 provider 时的默认值
let { money, updateMoney } = inject('moneyContext', {
money: 0,
updateMoney: (x: number) => {}
})
let car = inject('car')
</script>
补充:在 TS 里
inject('car')不带默认值时类型会包含undefined,建议传入默认值或用类型参数明确类型。
6.8 插槽 slot
- 默认插槽 :
<slot></slot>。 - 具名插槽 :子组件写
<slot name="s1"></slot>,父组件在引用子组件时用<template v-slot:s1>(或#s1)填充。 - 作用域插槽:数据在组件自身,但根据数据生成的结构需要组件的使用者来决定。
具体编码:
父组件中:
vue
<Game v-slot="params">
<!-- <Game v-slot:default="params"> -->
<!-- <Game #default="params"> -->
<ul>
<li v-for="g in params.games" :key="g.id">{{ g.name }}</li>
</ul>
</Game>
子组件中:
vue
<template>
<div class="category">
<h2>今日游戏榜单</h2>
<slot :games="games" a="哈哈"></slot>
</div>
</template>
<script setup lang="ts" name="Category">
import { reactive } from 'vue'
let games = reactive([
{ id: 'asgdytsa01', name: '英雄联盟' },
{ id: 'asgdytsa02', name: '王者荣耀' },
{ id: 'asgdytsa03', name: '红色警戒' },
{ id: 'asgdytsa04', name: '斗罗大陆' }
])
</script>
7. 响应式进阶 API
7.1 总览
| API | 作用 |
|---|---|
shallowRef |
创建一个响应式数据,但只对顶层做响应式处理:只跟踪 .value 整体替换,不关心值内部的属性变化 |
shallowReactive |
创建一个浅层响应式对象,只让对象的最顶层属性变成响应式,嵌套属性不是 |
readonly |
用于创建一个对象的深只读副本 |
shallowReadonly |
只将对象的最顶层属性设置为只读,内部的嵌套属性仍然可以修改 |
toRaw |
获取一个响应式对象的原始对象;返回的对象不再是响应式的,不会触发视图更新 |
markRaw |
标记一个对象,使其永远不会变成响应式的 |
shallowRef/shallowReactive是用来做性能优化的:ref/reactive的深度监听在面对巨大数据时会有开销。toRaw在需要将响应式数据传给非 Vue 的库或者外部系统时使用,确保对方收到的是普通对象。
7.2 customRef
作用 :创建一个自定义的 ref,并对其依赖项跟踪(track)和更新触发(trigger)进行逻辑控制。
实现防抖效果(useSumRef.ts):
ts
import { customRef } from 'vue'
export default function (initValue: string, delay: number) {
let msg = customRef((track, trigger) => {
let timer: number
return {
get() {
track() // 告诉 Vue 数据 msg 很重要,要对 msg 持续关注,一旦变化就更新
return initValue
},
set(value) {
clearTimeout(timer)
timer = setTimeout(() => {
initValue = value
trigger() // 通知 Vue 数据 msg 变化了
}, delay)
}
}
})
return { msg }
}
8. Vue 3 新组件与全局 API
8.1 Teleport
Teleport 是一种能够将我们的组件 HTML 结构移动到指定位置的技术。
html
<teleport to='body'>
<div class="modal" v-show="isShow">
<h2>我是一个弹窗</h2>
<p>我是弹窗中的一些内容</p>
<button @click="isShow = false">关闭弹窗</button>
</div>
</teleport>
8.2 Suspense
等待异步组件时渲染一些额外内容,让应用有更好的用户体验。
使用步骤:
- 异步引入组件。
- 使用
Suspense包裹组件,并配置好default与fallback两个插槽。
ts
import { defineAsyncComponent } from 'vue'
const Child = defineAsyncComponent(() => import('./Child.vue'))
vue
<template>
<div class="app">
<h3>我是App组件</h3>
<Suspense>
<template v-slot:default>
<Child />
</template>
<template v-slot:fallback>
<h3>加载中.......</h3>
</template>
</Suspense>
</div>
</template>
Suspense是内置组件,不需要从vue导入。
8.3 全局 API 转移到应用对象
app.componentapp.configapp.directiveapp.mountapp.unmountapp.use