vue3快速上手
一、 Vue3简介
2020年9月18日,
Vue.js发布版3.0版本,代号:One Piece(n截止2023年10月,最新的公开版本为:
3.3.4
``
1.1、性能的提升
打包大小减少
41%。初次渲染快
55%, 更新渲染快133%。内存减少
54%。
1.2、源码的升级
使用
Proxy代替defineProperty实现响应式。重写虚拟
DOM的实现和Tree-Shaking。
1.3、拥抱TypeScript
Vue3可以更好的支持TypeScript。
1.4、新的特性
Composition API(组合API):setupref与reactivecomputed与watch……
新的内置组件:
FragmentTeleportSuspense……
其他改变:
新的生命周期钩子
data选项应始终被声明为一个函数移除
keyCode支持作为v-on的修饰符……
二、创建Vue3工程
2.1、基于 vue-cli 创建
点击查看官方文档
备注:目前
vue-cli已处于维护模式,官方推荐基于Vite创建项目。
1 | ## 查看@vue/cli版本,确保@vue/cli版本在4.5.0以上 |
2.2、基于 vite 创建(推荐)
vite 是新一代前端构建工具,官网地址:https://vitejs.cn,vite的优势如下:
- 轻量快速的热重载(
HMR),能实现极速的服务启动。 - 对
TypeScript、JSX、CSS等支持开箱即用。 - 真正的按需编译,不再等待整个应用编译完成。
webpack构建 与vite构建对比图如下:

- 具体操作如下(点击查看官方文档)
1 | ## 1.创建命令 |
自己动手编写一个App组件
1 | <template> |
安装官方推荐的vscode插件:

总结:
Vite项目中,index.html是项目的入口文件,在项目最外层。- 加载
index.html后,Vite解析<script type="module" src="xxx">指向的JavaScript。 Vue3**中是通过 **createApp函数创建一个应用实例。
2.3、一个简单的效果
Vue3向下兼容Vue2语法,且Vue3中的模板中可以没有根标签
1 | <template> |
三、Vue3核心语法
3.1、OptionsAPI 与 CompositionAPI
Vue2的API设计是Options(配置)风格的。Vue3的API设计是Composition(组合)风格的。
1)、Options API 的弊端
Options类型的 API,数据、方法、计算属性等,是分散在:data、methods、computed中的,若想新增或者修改一个需求,就需要分别修改:data、methods、computed,不便于维护和复用。


2)、Composition API 的优势
可以用函数的方式,更加优雅的组织代码,让相关功能的代码更加有序的组织在一起。


说明:以上四张动图原创作者:大帅老猿
3.2、拉开序幕的 setup
1)、setup 概述
setup是Vue3中一个新的配置项,值是一个函数,它是 Composition API “表演的舞台”,组件中所用到的:数据、方法、计算属性、监视……等等,均配置在setup中。
特点如下:
setup函数返回的对象中的内容,可直接在模板中使用。setup中访问this是undefined。setup函数会在beforeCreate之前调用,它是“领先”所有钩子执行的。
1 | <template> |
2)、setup 的返回值
- 若返回一个对象:则对象中的:属性、方法等,在模板中均可以直接使用**(重点关注)。**
- 若返回一个函数:则可以自定义渲染内容将直接渲染在页面,代码如下:
1 | setup(){ |
3)、setup 与 Options API 的关系
Vue2的配置(data、methos……)中可以访问到setup中的属性、方法。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18<template>
<h2>姓名:{{ c }}</h2>
</template>
<script lang="ts">
export default {
name: 'Person',
setup() {
const name = '张三'
return { name }
},
data() {
return {
c: this.name,
}
},
}
</script>但在
setup中不能访问到Vue2的配置(data、methos……)。如果与
Vue2冲突,则setup优先。
4)、setup 语法糖
setup函数有一个语法糖,这个语法糖,可以让我们把setup独立出去(默认会把里面的数据return出去),代码如下:
1 | <template> |
**扩展:**上述代码,还需要编写一个不写setup的script标签,去指定组件名字,比较麻烦,我们可以借助vite中的插件简化
- 第一步:
npm i vite-plugin-vue-setup-extend -D - 第二步:
vite.config.ts
1 | import { defineConfig } from 'vite' |
- 第三步:
1 | <script setup lang="ts" name="Person"></script> |
3.3、ref 创建:基本类型的响应式数据
- **作用:**定义响应式变量。
- 语法:
let xxx = ref(初始值)。 - **返回值:**一个
RefImpl的实例对象,简称ref对象或ref,ref对象的value属性是响应式的。 - 注意点:
JS中操作数据需要:xxx.value,但模板中不需要.value,直接使用即可。- 对于
let name = ref('张三')来说,name不是响应式的,name.value是响应式的。
1 | <template> |
3.4、reactive 创建:对象类型的响应式数据
- 作用:定义一个响应式对象(基本类型不要用它,要用
ref,否则报错) - 语法:
let 响应式对象= reactive(源对象)。 - **返回值:**一个
Proxy的实例对象,简称:响应式对象。 - 注意点:
reactive定义的响应式数据是“深层次”的。
1 | <template> |
3.5、ref 创建:对象类型的响应式数据
- 其实
ref接收的数据可以是:基本类型、对象类型。 - 若
ref接收的是对象类型,内部其实也是调用了reactive函数。
1 | <template> |
3.6、ref 对比 reactive
宏观角度看:
ref用来定义:基本类型数据、对象类型数据;reactive用来定义:对象类型数据。
- 区别:
ref创建的变量必须使用.value(可以使用Vue - Official插件自动添加.value)。
reactive重新分配一个新对象,会失去响应式(可以使用Object.assign去整体替换)。
- 使用原则:
- 若需要一个基本类型的响应式数据,必须使用
ref。 - 若需要一个响应式对象,层级不深,
ref、reactive都可以。 - 若需要一个响应式对象,且层级较深,推荐使用
reactive。
3.7、toRefs 与 toRef
从一个响应式对象中解构对象,解构出来的属性必丢失响应,因此需要使用到toRefs 与 toRef
例如:let route = useRoute()
let { query } = route // 这里的query响应必定会丢失
修改:let { query } = toRefs(route)
- 作用:将一个响应式对象中的每一个属性,转换为ref对象(响应式)。
- 备注:
toRefs与toRef功能一致,但toRefs可以批量转换。 - 语法如下:
1 | <template> |
3.8、computed
作用:根据已有数据计算出新数据(和Vue2中的computed作用一致)。

1 | <template> |
3.9、watch
- 作用:监视数据的变化(和
Vue2中的watch作用一致) - 特点:
Vue3中的watch只能监视以下四种数据:ref定义的数据。reactive定义的数据。- 函数返回一个值(
getter函数)。 - 一个包含上述内容的数组。
我们在Vue3中使用watch的时候,通常会遇到以下几种情况:
1)、情况一:监视ref定义的基本类型数据
监视ref定义的【基本类型】数据:直接写数据名即可,监视的是其value值的改变。
1 | <template> |
2)、情况二:监视ref定义的对象类型数据
监视ref定义的【对象类型】数据:直接写数据名,监视的是对象的【地址值】,若想监视对象内部的数据,要手动开启深度监视。
注意:
若修改的是
ref定义的对象中的属性,newValue和oldValue都是新值,因为它们是同一个对象。若修改整个
ref定义的对象,newValue是新值,oldValue是旧值,因为不是同一个对象了。
1 | <template> |
3)、情况三:监视reactive定义的对象类型数据
监视reactive定义的【对象类型】数据,且默认开启了深度监视。
1 | <template> |
4)、情况四:监视ref/reactive定义的对象类型数据中的某个属性
监视ref或reactive定义的【对象类型】数据中的某个属性,注意点如下:
- 若该属性值不是【对象类型】,需要写成函数形式。
- 若该属性值是依然是【对象类型】,可直接编,也可写成函数,建议写成函数。
结论:监视的要是对象里的属性,那么最好写函数式
注意点:若是对象监视的是地址值,需要关注对象内部,需要手动开启深度监视。
1 | <template> |
5)、情况五:监视上述的多个数据
监视上述的多个数据
1 | <template> |
3.10、watchEffect
官网:立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行该函数。
watch对比watchEffect都能监听响应式数据的变化,不同的是监听数据变化的方式不同
watch:要明确指出监视的数据watchEffect:不用明确指出监视的数据(函数中用到哪些属性,那就监视哪些属性),初始化的时候会执行一次。
示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60<template>
<div class="person">
<h1>需求:水温达到50℃,或水位达到20cm,则联系服务器</h1>
<h2 id="demo">
水温:{{ temp }}
</h2>
<h2>水位:{{ height }}</h2>
<button @click="changePrice">
水温+1
</button>
<button @click="changeSum">
水位+10
</button>
</div>
</template>
<script lang="ts" setup name="Person">
import { ref, watch, watchEffect } from 'vue'
// 数据
const temp = ref(0)
const height = ref(0)
// 方法
function changePrice() {
temp.value += 10
}
function changeSum() {
height.value += 1
}
// 用watch实现,需要明确的指出要监视:temp、height
const stopWtach1 = watch([temp, height], (value) => {
// 从value中获取最新的temp值、height值
const [newTemp, newHeight] = value
// 室温达到50℃,或水位达到20cm,立刻联系服务器
if (newTemp >= 50 || newHeight >= 20) {
console.log(document.getElementById('demo')?.innerText)
console.log('联系服务器')
}
if (newTemp >= 100 || newHeight >= 50) {
console.log('清理了')
stopWtach1()
}
})
// 用watchEffect实现,不用
const stopWtach = watchEffect(() => {
// 室温达到50℃,或水位达到20cm,立刻联系服务器
if (temp.value >= 50 || height.value >= 20) {
console.log(document.getElementById('demo')?.innerText)
console.log('联系服务器')
}
// 水温达到100,或水位达到50,取消监视
if (temp.value === 100 || height.value === 50) {
console.log('清理了')
stopWtach()
}
})
</script>
3.11、标签的 ref 属性
作用:用于注册模板引用。
用在普通
DOM标签上,获取的是DOM节点。用在组件标签上,获取的是组件实例对象。
用在普通DOM标签上:
1 | <template> |
用在组件标签上:
1 | <!-- 父组件father.vue --> |
3.12、组件的$refs 属性
获取到页面所有组件的ref实例对象

父组件(father.vue)
1 | <template> |
子组件(Person.vue)
1 | <template> |
子组件(Person1.vue)
1 | <template> |
3.13、props
1
2
3
4
5
6
7
8
9
10 / types/index.ts
// 定义一个接口,限制每个Person对象的格式
export interface PersonInter {
id:string,
name:string,
age:number
}
// 定义一个自定义类型Persons
export type Persons = Array<PersonInter>
App.vue中代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 <template>
<Person :list="persons" />
</template>
<script lang="ts" setup name="App">
import type { Persons } from './types'
import { reactive } from 'vue'
import Person from './components/Person.vue'
// let persons:Persons = reactive([
const persons = reactive<Persons>([
{ id: 'e98219e12', name: '张三', age: 18 },
{ id: 'e98219e13', name: '李四', age: 19 },
{ id: 'e98219e14', name: '王五', age: 20 },
])
</script>
Person.vue中代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 <template>
<div class="person">
<ul>
<li v-for="item in list" :key="item.id">
{{ item.name }}--{{ item.age }}
</li>
</ul>
</div>
</template>
<script lang="ts" setup name="Person">
import { defineProps } from 'vue'
// 第一种写法:仅接收
// let props = defineProps(['list'])
// 第二种写法:接收+限制类型,因为传来的可能不止一个数据,因此在泛型中用一个对象包裹
// let props = defineProps<{ list: Persons; aa?: string }>();
// 第三种写法:接收+限制类型+指定默认值+限制必要性
// .....withDefaults 指定默认值
// .....Persons 限制类型
// .....list?: 限制必要性 :父组件必须要 ?: 父组件可不传 ts语法
const props = withDefaults(defineProps<{ list?: Persons }>(), {
list: () => [{ id: 'asdasg01', name: '小猪佩奇', age: 18 }],
})
console.log(props)
</script>
易懂
在 Vue 3 中使用 TypeScript 时,你可以通过 withDefaults 函数来为 defineProps 定义的 props 设置默认值。withDefaults 函数允许你指定默认值,同时保持 TypeScript 的类型检查。
以下是如何在 TypeScript 中为 defineProps 设置默认值的示例:
1 | 假设你有一个子组件 PasswordDialog.vue,你可以这样定义和接收 lianlianMercNo,并为其设置默认值: |
1 | 在父组件中,你可以这样使用这个子组件并传递 lianlianMercNo: |
在这个示例中:
子组件 PasswordDialog.vue 中定义了一个 Props 接口,其中 lianlianMercNo 属性是可选的(使用 ? 标记)。
使用 withDefaults 函数将 defineProps
如果父组件没有传递 lianlianMercNo,则 PasswordDialog 组件会使用默认值 1234。
这样,你就可以在 TypeScript 中为 defineProps 设置默认值,同时保持类型检查。
3.14、生命周期
概念:
Vue组件实例在创建时要经历一系列的初始化步骤,在此过程中Vue会在合适的时机,调用特定的函数,从而让开发者有机会在特定阶段运行自己的代码,这些特定的函数统称为:生命周期钩子规律:
生命周期整体分为四个阶段,分别是:创建、挂载、更新、销毁,每个阶段都有两个钩子,一前一后。
Vue2的生命周期创建阶段:
beforeCreate、created挂载阶段:
beforeMount、mounted更新阶段:
beforeUpdate、updated销毁阶段:
beforeDestroy、destroyedVue3的生命周期创建阶段:
setup挂载阶段:
onBeforeMount、onMounted更新阶段:
onBeforeUpdate、onUpdated卸载阶段:
onBeforeUnmount、onUnmounted常用的钩子:
onMounted(挂载完毕)、onUpdated(更新完毕)、onBeforeUnmount(卸载之前)注意项:(重点)
到底是子组件挂载(onMounted)先执行还是父组件的挂载(onMounted)先执行?
答:先执行子组件的挂载再执行父组件的挂载,vue项目中app.vue是所有组件的顶级组件,因此它的挂载是最后执行
示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49<template>
<div class="person">
<h2>当前求和为:{{ sum }}</h2>
<button @click="changeSum">
点我sum+1
</button>
</div>
</template>
<!-- vue3写法 -->
<script lang="ts" setup name="Person">
import {
onBeforeMount,
onBeforeUnmount,
onBeforeUpdate,
onMounted,
onUnmounted,
onUpdated,
ref,
} from 'vue'
// 数据
const sum = ref(0)
// 方法
function changeSum() {
sum.value += 1
}
console.log('setup')
// 生命周期钩子
onBeforeMount(() => {
console.log('挂载之前')
})
onMounted(() => {
console.log('挂载完毕')
})
onBeforeUpdate(() => {
console.log('更新之前')
})
onUpdated(() => {
console.log('更新完毕')
})
onBeforeUnmount(() => {
console.log('卸载之前')
})
onUnmounted(() => {
console.log('卸载完毕')
})
</script>
3.15、自定义hook
接口地址:https://dog.ceo/api/breed/pembroke/images/random 随机获取一张狗的图片(有的时候须要梯子)
什么是
hook?—— 本质是一个函数,把setup函数中使用的Composition API进行了封装,类似于vue2.x中的mixin。自定义
hook的优势:复用代码, 让setup中的逻辑更清楚易懂。useSum.ts和useDog.ts中的可以分别写onMounted钩子
示例代码:
useSum.ts中内容如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import {ref,onMounted} from 'vue'
export default function(){
let sum = ref(0)
const increment = ()=>{
sum.value += 1
}
const decrement = ()=>{
sum.value -= 1
}
onMounted(()=>{
increment()
})
//向外部暴露数据
return {sum,increment,decrement}
}useDog.ts中内容如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26import axios from 'axios'
import { onMounted, reactive, ref } from 'vue'
export default function () {
const dogList = reactive<string[]>([])
const loading = ref(false)
const getDog = async () => {
loading.value = true
try {
const res = await axios.get('https://dog.ceo/api/breed/pembroke/images/random')
dogList.push(res.data.message)
} catch (error) {
console.log(error)
} finally {
loading.value = false
}
}
onMounted(() => {
getDog()
})
// 向外暴露数据
return { dogList, loading, getDog }
}组件中具体使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31<template>
<h2>当前求和为:{{ sum }}</h2>
<button @click="increment">
点我+1
</button>
<button @click="decrement">
点我-1
</button>
<hr>
<img v-for="(u, index) in dogList.urlList" :key="index" :src="(u as string)">
<span v-show="dogList.isLoading">加载中......</span><br>
<button @click="getDog">
再来一只狗
</button>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'App',
})
</script>
<script setup lang="ts">
import useDog from './hooks/useDog'
import useSum from './hooks/useSum'
const { sum, increment, decrement } = useSum()
const { dogList, getDog } = useDog()
</script>
四、 路由
4.1、对路由的理解

4.2、基本切换效果
Vue3中要使用vue-router的最新版本,目前是4版本。路由配置文件代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31// 创建一个路由器,并暴露出去
import About from '@/components/About.vue'
// 引入一个一个可能要呈现组件
import Home from '@/components/Home.vue'
import News from '@/components/News.vue'
// 第一步:引入createRouter
import { createRouter, createWebHistory } from 'vue-router'
// 第二步:创建路由器
const router = createRouter({
history: createWebHistory(), // 路由器的工作模式(稍后讲解)
routes: [
// 一个一个的路由规则
{
path: '/home',
component: Home,
},
{
path: '/news',
component: News,
},
{
path: '/about',
component: About,
},
],
})
// 暴露出去router
export default router
main.ts代码如下:1
2
3
4import router from './router/index'
app.use(router)
app.mount('#app')
App.vue代码如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19<template>
<div class="app">
<h2 class="title">Vue路由测试</h2>
<!-- 导航区 -->
<div class="navigate">
<RouterLink to="/home" active-class="active">首页</RouterLink>
<RouterLink to="/news" active-class="active">新闻</RouterLink>
<RouterLink to="/about" active-class="active">关于</RouterLink>
</div>
<!-- 展示区 -->
<div class="main-content">
<RouterView></RouterView>
</div>
</div>
</template>
<script lang="ts" setup name="App">
import {RouterLink,RouterView} from 'vue-router'
</script>
4.3、 两个注意点
路由组件通常存放在
pages或views文件夹,一般组件通常存放在components文件夹。通过点击导航,视觉效果上“消失” 了的路由组件,默认是被卸载掉的,需要的时候再去挂载。
4.4、路由器工作模式
history模式优点:
URL更加美观,不带有#,更接近传统的网站URL。缺点:后期项目上线,需要服务端配合处理路径问题,否则刷新会有
404错误。1
2
3
4const router = createRouter({
history:createWebHistory(), //history模式
/******/
})hash模式优点:兼容性更好,因为不需要服务器端处理路径。
缺点:
URL带有#不太美观,且在SEO优化方面相对较差。1
2
3
4const router = createRouter({
history:createWebHashHistory(), //hash模式
/******/
})
4.5、to的两种写法
1 | <!-- 第一种:to的字符串写法 --> |
4.6、命名路由
作用:可以简化路由跳转及传参(后面就讲)。
给路由规则命名:
1 | routes:[ |
跳转路由:
1 | <!--简化前:需要写完整的路径(to的字符串写法) --> |
4.7、嵌套路由
编写
News的子路由:Detail.vue1
2
3
4
5
6
7
8
9
10
11
12
13<template>
<div class="detail">
{{ query.content }}
</div>
</template>
<script setup lang="ts">
import { toRefs } from 'vue'
import { useRoute } from 'vue-router'
const { query } = toRefs(useRoute())
</script>配置路由规则,使用
children配置项:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29const router = createRouter({
history: createWebHistory(),
routes: [
{
name: 'zhuye',
path: '/home',
component: Home,
},
{
name: 'xinwen',
path: '/news',
component: News,
children: [
{
name: 'xiang',
path: 'detail', // 子路由的路径不需要加斜杠/
component: Detail,
},
],
},
{
name: 'guanyu',
path: '/about',
component: About,
},
],
})
export default router记得去
News.vue组件中预留一个<router-view>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44// News.vue
<template>
<div class="news">
<ul>
<li v-for="(item, index) in list" :key="index">
<RouterLink
:to="{
name: 'Detail',
query: {
id: item.id,
content: item.content,
},
}"
>
{{ item.title }}
</RouterLink>
</li>
</ul>
<RouterView />
</div>
</template>
<script setup lang="ts" name="News">
import { reactive } from 'vue'
const list = reactive([
{
id: 1,
title: '新闻1',
content: '新闻1的详情',
},
{
id: 2,
title: '新闻2',
content: '新闻2的详情',
},
{
id: 3,
title: '新闻3',
content: '新闻3的详情',
},
])
</script>

4.8、路由传参
1)、query参数
传递参数
1 | // News.vue |
接收参数:
1 | // Detail.vue |
2)、params参数
需在路由中占位
传递参数
1 | <!-- 跳转并携带params参数(to的对象写法) --> |
接收参数:
1 | <template> |
在路由中占位:
1 | import { createRouter, createWebHistory } from 'vue-router' |
备注1:传递
params参数时,若使用to的对象写法,必须使用name配置项,不能用path。备注2:传递
params参数时,需要提前在路由中占位。
4.9、路由的props配置
上述路由接收参数的时候、需要写大量的代码
1
2
3 import { toRefs } from 'vue';
import { useRoute } from 'vue-router';
let { params, query } = toRefs(useRoute());
props作用:让路由组件更方便的收到参数(可以将路由参数作为props传给组件),在组件中直接用defineProps便可接收到其他路由组件传递过来的参数
这种写法其实就是类似于自定义组件的传参
1 | 类似于<Detail a=“1” b="2" c="3"><Detail/> |
1)、props的布尔值写法(只适用于params传参)
作用:将路由收到的所有params参数作为props传给当前组件
1 | <!-- orderList.vue --> |
2)、props的函数写法
作用:把返回的对象中每一组key-value作为props传给Detail组件(params、query可使用,更推荐query使用)
1 | <!-- orderList.vue --> |
3)、props的对象写法
作用:把对象中的每一组key-value作为props传给Detail组件(不常用)
1 | <!-- orderList.vue --> |
**注意:**这里都是应用在路由组件中,而不是自定义组件中。所以当组件中有使用defineProps接收参数的时候,首先要确认这是路由组件还是自定义组件。
4.10、 replace属性
作用:控制路由跳转时操作浏览器历史记录的模式。
浏览器的历史记录有两种写入方式:分别为
push和replace:push是追加历史记录(默认值)。replace是替换当前记录。
开启
replace模式:1
<RouterLink replace .......>News</RouterLink>
4.11、 编程式导航
路由组件的两个重要的属性:$route和$router变成了两个hooks
1 | import {useRoute,useRouter} from 'vue-router' |
4.12、 重定向
作用:将特定的路径,重新定向到已有路由。
具体编码:
1
2
3
4{
path:'/',
redirect:'/about'
}
五、pinia

5.1、 搭建 pinia 环境
第一步:npm install pinia
第二步:操作src/main.ts
1 | import { createApp } from 'vue' |
此时开发者工具中已经有了pinia选项

5.2、 选项式 Stores
1
2 import {defineStore} from 'pinia'
export const useCountStore = defineStore('count',{}) // 第二个参数是个对象
1)、存储+读取数据
Store是一个保存:状态、业务逻辑 的实体,每个组件都可以读取、写入它。它有三个概念:
state、getter、action,相当于组件中的:data、computed和methods。具体编码:
src/store/countStore.ts1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// 引入defineStore用于创建store
import {defineStore} from 'pinia'
// 定义并暴露一个store
export const useCountStore = defineStore('count',{
// 动作
actions:{},
// 状态
state(){
return {
sum:6,
school: '尚硅谷'
}
},
// 计算
getters:{}
})具体编码:
src/store/loveTalkStore.ts1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// 引入defineStore用于创建store
import {defineStore} from 'pinia'
// 定义并暴露一个store
export const useLoveTalkStore = defineStore('talk',{
// 动作
actions:{},
// 状态
state(){
return {
loveTalkList:[
{id:'yuysada01',content:'你今天有点怪,哪里怪?怪好看的!'},
{id:'yuysada02',content:'草莓、蓝莓、蔓越莓,你想我了没?'},
{id:'yuysada03',content:'心里给你留了一块地,我的死心塌地'}
]
}
},
// 计算
getters:{}
})组件中使用
state中的数据Count.vue1
2
3
4
5
6
7
8
9
10
11<template>
<h2>当前求和为:{{ countStore.sum }}</h2>
</template>
<script setup lang="ts" name="Count">
// 引入对应的useXxxxxStore
import {useCountStore} from '@/store/countStore'
``
// 调用useXxxxxStore得到对应的store
const countStore = useCountStore()
</script>LoveTalk.vue1
2
3
4
5
6
7
8
9
10
11
12
13
14<template>
<ul>
<li v-for="talk in loveTalkStore.loveTalkList" :key="talk.id">
{{ talk.content }}
</li>
</ul>
</template>
<script setup lang="ts" name="Count">
import axios from 'axios'
import { useLoveTalkStore } from '@/store/loveTalkStore'
const loveTalkStore = useLoveTalkStore()
</script>
2)、 修改数据 (三种方式)
第一种修改方式,直接修改
1
2// 在vue组件中修改
countStore.sum = 666第二种修改方式:批量修改
1
2
3
4
5
6
7
8
9
10
11
12// 在vue组件中修改
// 1)对象形式
countStore.$patch({
sum:999,
school:'atguigu'
})
// 2)函数形式
countStore.$patch((state) => {
state.sum=999,
state.school='atguigu'
})第三种修改方式:借助
action修改(action中可以编写一些业务逻辑)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// 在pinia中修改
import { defineStore } from 'pinia'
export const useCountStore = defineStore ('count', {
actions: {
//加
increment(value:number) {
if (this.sum < 10) {
//操作countStore中的sum
this.sum += value
}
}
}
})
// 在组件中调用action
let optVal = ref(1);
const countStore = useCountStore()
//在函数中调用
countStore.increment(optVal.value)
3)、 storeToRefs
- 借助
storeToRefs将store中的数据(state,getters)转为ref响应对象,方便在模板中使用。 - 注意:
pinia提供的storeToRefs只会将数据做转换,而Vue的toRefs会转换store中所有的数据(actions state等等)。 - action 的 increment 可以直接解构,无需使用 storeToRefs
1 | <template> |

4)、 getters
概念:当
state中的数据,需要经过处理后再使用时,可以使用getters配置。追加
getters配置。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26// 引入defineStore用于创建store
import {defineStore} from 'pinia'
// 定义并暴露一个store
export const useCountStore = defineStore('count',{
// 动作
actions:{
/************/
},
// 状态
state(){
return {
sum:1,
school:'atguigu'
}
},
// 计算
getters: {
// 箭头函数
doubleSum: (state): number => state.sum * 2,
// 普通函数
upperSchool(): string {
return (this.school = '希望小学');
}
}
})组件中读取数据:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<template>
<div class="count">
<h2>当前求和为:{{ sum }}, 当前sun的两倍{{ doubleSum }}。 小明的多大了? {{ age }}岁</h2>
<h2>当前学校为:{{ upperSchool }}</h2>
</div>
</template>
<script setup lang="ts">
import { useCountStore } from '@/store/countStore'
import { storeToRefs } from 'pinia'
const countStore = useCountStore()
const { sum, age, doubleSum, upperSchool } = storeToRefs(countStore)
</script>
5)、 $subscribe
通过 store 的 $subscribe() 方法侦听 state 及其变化
1 | // loveTalkStore.ts |
1 | // 在组件中使用 LoveTalk.vue |
5.3、 store组合式写法
和选项式写法唯一不同的是pinia中的defineStor()中的写法不一样,其他都一样(在组件中修改数据,使用一些api)
1
2
3
4
5 import { defineStore } from 'pinia'
export const useCountStore = defineStore('count', () => { // 第二个参数是个函数
return {} // 需要把定义的数据/方法return出去
})
loveTalkStore.ts
1 | import axios from 'axios' |
countStore.ts
1 | import { defineStore } from 'pinia' |
六、组件通信
Vue3组件通信和Vue2的区别:
- 移出事件总线,使用
mitt代替。
vuex换成了pinia。- 把
.sync优化到了v-model里面了。 - 把
$listeners所有的东西,合并到$attrs中了。 $children被砍掉了。
常见搭配形式:

6.1、 props
概述:props是使用频率最高的一种通信方式,常用与 :父 ↔ 子。
- 若 父传子:属性值是非函数。
- 若 子传父:属性值是函数。
父组件:Father.vue
1 | <template> |
子组件:Child.vue
1 | <template> |
6.2、 自定义事件defineEmits
- 概述:自定义事件常用于:子 => 父。
- 注意区分好:原生事件、自定义事件。
- 原生事件:
- 事件名是特定的(
click、mosueenter等等) - 事件对象
$event: 是包含事件相关信息的对象(pageX、pageY、target、keyCode)
- 事件名是特定的(
- 自定义事件:
- 事件名是任意名称
- 事件对象
$event: 是调用emit时所提供的数据,可以是任意类型!!!
父组件:Father.vue
1 | <template> |
子组件:Child.vue
1 | <template> |
扩展:当有多个参数的时候,获取事件对象就需要使用$event去占位
1
2
3
4
5
6
7
8
9
10
11
12
13
14 ><template>
<div class="child">
<button @click="test(5, 6, $event)">
测试
</button>
</div>
></template>
><script setup lang="ts" name="Child">
>function test(a, b, e) {
console.log(e)
>}
></script>
6.3、 mitt
概述:与消息订阅与发布(pubsub)功能类似,可以实现任意组件间通信。
【第一步】:安装mitt
1 | pnpm add mitt |
【第二步】:新建文件:src\utils\emitter.ts
1 | // 引入mitt |
【第三步】:接收数据的组件中:绑定事件、同时在销毁前解绑事件:
1 | import emitter from "@/utils/emitter"; |
【第四步】:提供数据的组件,在合适的时候触发事件
1 | import emitter from "@/utils/emitter"; |
注意这个重要的内置关系,总线依赖着这个内置关系
6.4、 v-model
概述:实现 父↔子 之间相互通信。
1)、v-model用在html标签上(前序知识)
1 | <!-- html标签上使用v-model指令 --> |
使用:
1 | <template> |
2)、v-model用在组件标签上(前序知识)
1 | <!-- 组件标签上使用v-model指令 --> |
使用:
Father.vue
1 | <AtguiguInput v-model="userName"/> |
AtguiguInput.vue组件中:
1 | <template> |
案例:
1 | // 父组件 |
1 | // 子组件 |
Vue 3.4+ 简写
1 | // 父组件 |
2-1、修改传递参数的默认值
更换modelValue,例如改成abc
1 | <!-- 也可以更换value,例如改成abc--> |
AtguiguInput组件中:
1 | <template> |
2-2、组件标签上多次使用v-model
如果value可以更换,那么就可以在组件标签上多次使用v-model
1 | <AtguiguInput v-model:abc="userName" v-model:xyz="password"/> |
AtguiguInput组件中:
1 | <template> |
2-3、v-model二次封装组件
1 | // 子组件 |
1 | 父组件 |
6.5、 $attrs
概述:
$attrs用于实现当前组件的父组件,向当前组件的子组件通信(祖→孙)。具体说明:
$attrs是一个对象,包含所有父组件传入的标签属性。注意:
$attrs会自动排除props中声明的属性(可以认为声明过的props被子组件自己“消费”了),即$attrs中可以取到组件传递过来的参数,如果传值过来的参数在props中声明了,则不会在$attrs中展示

知识扩展
1 | <Child v-bind="{x:100,y:200}"/> |
父组件:
1 | <template> |
子组件:
1 | <template> |
孙组件:
1 | <template> |
6.6、 $refs $parent
概述:
$refs用于 :父→子。$parent用于:子→父。
原理如下:
属性 说明 $refs值为对象,包含所有被 ref属性标识的DOM元素或组件实例。$parent值为对象,当前组件的父组件实例对象。
父组件:
1 | <template> |
子组件:Child1.vue
1 | <template> |
子组件:Child2.vue
1 | <template> |

6.7、 provide、inject
概述:实现祖孙组件直接通信
具体使用:
- 在祖先组件中通过
provide配置向后代组件提供数据 - 在后代组件中通过
inject配置来声明接收数据
- 在祖先组件中通过
具体编码:
【第一步】父组件中,使用
provide提供数据1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36<template>
<div class="father">
<h3>父组件</h3>
<h4>银子:{{ money }}万元</h4>
<h4>车子:一辆{{ car.brand }}车,价值{{ car.price }}万元</h4>
<Child />
</div>
</template>
<script setup lang="ts" name="Father">
import { provide, reactive, ref } from 'vue'
import Child from './Child.vue'
const money = ref(100)
const car = reactive({
brand: '奔驰',
price: 100,
})
function updateMoney(value: number) {
money.value -= value
}
// 向后代提供数据
// 语法:provide(命名, 传参);
provide('moneyContext', { money, updateMoney }) // 注意money不能用money.value,用.vue就是传递的一个数字 而不是一个响应式数据
provide('car', car)
</script>
<style scoped>
.father {
padding: 20px;
background-color: rgb(165 164 164);
border-radius: 10px;
}
</style>注意:子组件中不用编写任何东西,是不受到任何打扰的
【第二步】孙组件中使用
inject配置项接受数据。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28<template>
<div class="grand-child">
<h3>我是孙组件</h3>
<h4>银子:{{ money }}</h4>
<h4>车子:一辆{{ car.brand }}车,价值{{ car.price }}万元</h4>
<button @click="updateMoney(6)">
花爷爷的钱
</button>
</div>
</template>
<script setup lang="ts" name="GrandChild">
import { inject } from 'vue'
// inject(命名, 默认参数);
const { money, updateMoney } = inject('moneyContext', { money: 0, updateMoney: (param: number) => {} })
const car = inject('car', { brand: '未知', price: 0 })
</script>
<style scoped>
.grand-child {
padding: 20px;
background-color: orange;
border-radius: 10px;
box-shadow: 0 0 10px black;
}
</style>
6.8、 pinia
参考之前pinia部分的讲解
6.9、 slot
1)、 默认插槽

1 | 父组件中: |
2)、 具名插槽
父组件:v-slot: xxx
子组件:name= “xxx”
其中v-slot: xxx 可以简写成 #xxx
1 | 父组件中: |
3)、 作用域插槽
理解:插槽的内容无法访问到子组件的状态,然而在某些场景下插槽的内容可能想要同时使用父组件域内和子组件域内的数据。要做到这一点,我们需要一种方法来让子组件在渲染时将一部分数据提供给插槽。
具体编码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28父组件中:
<Game v-slot="slotProps"> // 这几种写法都一样
<!-- <Game v-slot:default="slotProps"> -->
<!-- <Game #default="slotProps"> -->
<!-- <Game #="params"> -->
<ul>
<li v-for="g in slotProps.games" :key="g.id">{{ g.name }}</li>
</ul>
</Game>
子组件中:
<template>
<div class="category">
<h2>今日游戏榜单</h2>
<slot :games="games" a="哈哈" />
</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>
七、 directive自定义指令
TIP
只有当所需功能只能通过直接的 DOM 操作来实现时,才应该使用自定义指令。其他情况下应该尽可能地使用
v-bind这样的内置指令来声明式地使用模板,这样更高效,也对服务端渲染更友好。
自定义指令会有指令钩子,当需要在指令中用到多个钩子则需要使用对象的形式来完成。
1 | // 局部 |
如果只需要需要在 mounted 和 updated 钩子上上实现相同的行为,除此之外并不需要其他钩子。这种情况下我们可以直接用一个函数来定义指令
1 | // 局部 |
7.1、 局部自定义指令
1)、在使用语法糖
在使用<script setup>的情况下
1 | <template> |
1)、在未使用语法糖
在没有使用 <script setup> 的情况下,自定义指令需要通过 directives 选项注册:
1 | <template> |
7.2、 全局自定义指令
将一个自定义指令全局注册到应用层级也是一种常见的做法:
1 | const app = createApp({}) |
7.3、 指令钩子
一个指令的定义对象可以提供几种钩子函数 (都是可选的):
1 |
|
钩子参数
指令的钩子会传递以下几种参数:
el:指令绑定到的元素。这可以用于直接操作 DOM。binding:一个对象,包含以下属性。value:传递给指令的值。例如在v-my-directive="1 + 1"中,值是2。oldValue:之前的值,仅在beforeUpdate和updated中可用。无论值是否更改,它都可用。arg:传递给指令的参数 (如果有的话)。例如在v-my-directive:foo中,参数是"foo"。modifiers:一个包含修饰符的对象 (如果有的话)。例如在v-my-directive.foo.bar中,修饰符对象是{ foo: true, bar: true }。instance:使用该指令的组件实例。dir:指令的定义对象。
vnode:代表绑定元素的底层 VNode。prevVnode:代表之前的渲染中指令所绑定元素的 VNode。仅在beforeUpdate和updated钩子中可用。
举例来说,像下面这样使用指令:
1 | <div v-example:foo.bar="baz"> |
binding 参数会是一个这样的对象:
1 | { |
和内置指令类似,自定义指令的参数也可以是动态的。举例来说:
1 | <div v-example:[arg]="value"></div> |
7.4、 简化形式
对于自定义指令来说,一个很常见的情况是仅仅需要在 mounted 和 updated 上实现相同的行为,除此之外并不需要其他钩子。这种情况下我们可以直接用一个函数来定义指令,如下所示:
1 | <div v-color="color"></div> |
1 | app.directive('color', (el, binding) => { |
八、 其它 API
8.1、 shallowRef 与 shallowReactiv
1)、 shallowRef
作用:创建一个浅层响应式数据,但只对顶层属性进行响应式处理。
用法:
1
let myVar = shallowRef(initialValue);
特点:只跟踪引用值的变化,不关心值内部的属性变化。简单来说只对 XXX.value = ‘111’的生效 对 obj.value.xxx = ‘111’不生效。
1 | <template> |
2)、 shallowReactive
作用:创建一个浅层响应式对象,只会使对象的最顶层属性变成响应式的,对象内部的嵌套属性则不会变成响应式的
用法:
1
const myObj = shallowReactive({ ... });
特点:对象的顶层属性是响应式的,但嵌套对象的属性不是。简单来说只对第一层的属性值生效
1 | <template> |
3)、 总结
通过使用
shallowRef()和shallowReactive()来绕开深度响应。浅层式API创建的状态只在其顶层是响应式的,对所有深层的对象不会做任何处理,避免了对每一个内部属性做响应式所带来的性能成本,这使得属性的访问变得更快,可提升性能。
8.2、 readonly 与 shallowReadonly
1)、 readonly
作用:用于创建一个对象的深只读副本。
用法:
1
2const original = reactive({ ... });
const readOnlyCopy = readonly(original); // readonly的参数必须是一个响应式数据特点:
- 对象的所有嵌套属性都将变为只读。
- 任何尝试修改这个对象的操作都会被阻止(在开发模式下,还会在控制台中发出警告)。
应用场景:
- 创建不可变的状态快照。
- 保护全局状态或配置不被修改。
2)、 shallowReadonly
作用:与
readonly类似,但只作用于对象的顶层属性。用法:
1
2const original = reactive({ ... });
const shallowReadOnlyCopy = shallowReadonly(original);特点:
只将对象的顶层属性设置为只读,对象内部的嵌套属性仍然是可变的。
适用于只需保护对象顶层属性的场景。
8.3、toRaw 与 markRaw
1)、 toRaw
作用:用于获取一个响应式对象的原始对象,
toRaw返回的对象不再是响应式的,不会触发视图更新。官网描述:这是一个可以用于临时读取而不引起代理访问/跟踪开销,或是写入而不触发更改的特殊方法。不建议保存对原始对象的持久引用,请谨慎使用。
何时使用? —— 在需要将响应式对象传递给非
Vue的库或外部系统时,使用toRaw可以确保它们收到的是普通对象具体编码:
1
2
3
4
5
6
7import { reactive,toRaw,markRaw,isReactive } from "vue";
/* toRaw */
// 响应式对象
let person = reactive({name:'tony',age:18})
// 原始对象
let rawPerson = toRaw(person)
2)、 markRaw
作用:标记一个对象,使其永远不会变成响应式的。
例如使用
mockjs时,为了防止误把mockjs变为响应式对象,可以使用markRaw去标记mockjs编码:
1
2
3
4
5
6
7
8
9/* markRaw */
let citys = markRaw([
{id:'asdda01',name:'北京'},
{id:'asdda02',name:'上海'},
{id:'asdda03',name:'天津'},
{id:'asdda04',name:'重庆'}
])
// 根据原始对象citys去创建响应式对象citys2 —— 创建失败,因为citys被markRaw标记了
let citys2 = reactive(citys)
8.4、customRef(自定义ref)
作用:创建一个自定义的ref,并对其依赖项跟踪和更新触发进行逻辑控制。
实现防抖效果hooks(useSumRef.ts):
1 | import { customRef } from 'vue' |
组件中使用:
1 | <template> |
九、Vue3新组件
9.1、 Teleport
- 什么是Teleport?—— Teleport 是一种能够将我们的组件html结构移动到指定位置的技术。
app.vue
1 | <template> |
Modal.vue
1 | <template> |
9.2、 Suspense(实验性功能)
- 等待异步组件时渲染一些额外内容,让应用有更好的用户体验
- 使用步骤:
- 异步引入组件
- 使用
Suspense包裹组件,并配置好default与fallback
1 | import { defineAsyncComponent,Suspense } from "vue"; |
1 | <template> |
9.3、 全局API转移到应用对象
app.component:注册全局组件1
2
3
4
5
6
7
8
9
10
11
12
13<!-- hello.vue -->
<template>
<p>helloWord</p>
</template>
<!-- main.ts -->
import Child from './components/Child.vue';
app.component('Child', Child);
<!-- app.vue -->
<template>
<Child />
</template>app.config.globalProperties:用于注册能够被应用内所有组件实例访问到的全局属性的对象1
2
3
4
5
6
7
8
9
10
11
12
13<!-- main.ts -->
app.config.globalProperties.msg = 'hello'
declare module 'vue' { // 解决msg在其他页面使用时ts报错,可单独写一个文件中,不推荐过度使用
interface ComponentCustomProperties {
msg: number
}
}
<!-- app.vue -->
<template>
<p>{{ msg }}</p>
</template>app.mountapp.unmountapp.use
9.4、其他
过渡类名
v-enter修改为v-enter-from、过渡类名v-leave修改为v-leave-from。keyCode作为v-on修饰符的支持。v-model指令在组件上的使用已经被重新设计,替换掉了v-bind.sync。v-if和v-for在同一个元素身上使用时的优先级发生了变化。移除了
$on、$off和$once实例方法。移除了过滤器
filter。移除了
$children实例propert。……``
