Vue3基础
【v-】
注:这里只是简单列举出了这些指令 在后续笔记中 某些指令会有完整的介绍
Vue 提供了一系列以 v- 开头的指令(Directives),用于在模板中实现各种交互逻辑和渲染控制。以下是常用 v- 指令的系统总结:
一、核心渲染与数据绑定指令
1. v-text
- 作用:设置元素的文本内容(替代
{{ }}插值) - 特点:会覆盖元素原有的文本,不解析 HTML
1 | <div v-text="message"></div> |
2. v-html
- 作用:设置元素的 HTML 内容(解析 HTML 标签)
- 注意:有 XSS 风险,仅用于可信内容,不可用于用户输入
1 | <div v-html="htmlContent"></div> |
3. v-bind(简写 :)
- 作用:动态绑定 HTML 属性、组件 props、CSS 类、样式等
- 用法:
v-bind:属性名="表达式"或:属性名="表达式"
1 | <!-- 绑定普通属性 --> |
二、条件渲染指令
1. v-if
- 作用:根据表达式真假,决定是否渲染元素(条件为
false时不生成 DOM) - 搭配:
v-else-if(多条件判断)、v-else(兜底条件),三者需连续书写
1 | <div v-if="score >= 90">优秀</div> |
2. v-show
- 作用:根据表达式真假,控制元素显示 / 隐藏(通过
display: none实现,始终生成 DOM) - 区别:
v-if适合切换频率低的场景,v-show适合切换频繁的场景
1 | <div v-show="isVisible">频繁切换显示/隐藏时用我</div> |
我们在这里区分一下 v-if 与 v-show
v-if 当不满足条件不再显示之后 执行的是对原组件的销毁
而 v-show 不满足条件不再显示之后 执行的是对原组件的隐藏 组件的结构是没有被改变的
三、列表渲染指令
v-for
- 作用:基于源数据(数组、对象、字符串、数字)循环渲染元素
- 语法:
v-for="(item, index) in 数据源"(数组)、v-for="(value, key, index) in 对象" - 必须:绑定
:key属性(唯一标识,优化渲染性能)
1 | <!-- 遍历数组 --> |
v-for 是 Vue 中用于列表渲染的核心指令,它可以基于源数据(数组、对象、字符串等)重复渲染元素或组件。以下是其详细用法和核心知识点:
一、基本语法(遍历数组)
最常用场景是遍历数组,语法为:v-for="(item, index) in 数组"
item:数组中的当前元素(必填)index:当前元素的索引(可选,从 0 开始)in可替换为of(更符合 JavaScript 语法习惯)
1 | <template> |
二、遍历对象
1 | v-for` 也可遍历对象的键值对,语法为: |
value:属性值(必填)key:属性名(可选)index:索引(可选,从 0 开始)
1 | <template> |
三、遍历字符串 / 数字
- 遍历字符串:按字符拆分
- 遍历数字:从 1 开始计数到该数字
1 | <!-- 遍历字符串 --> |
四、必须的 key 属性
Vue 要求在 v-for 中为每个循环项绑定 :key,作用是:
- 标识唯一性:帮助 Vue 区分不同元素,提高列表更新时的渲染性能(避免不必要的 DOM 重新创建)。
- 避免状态混乱:确保组件 / 元素的状态(如表单输入值)在列表变化时正确保留。
注意:
- 优先使用唯一且稳定的标识(如数据的
id)作为key,而非索引index(索引会随数组排序 / 删除变化,可能导致性能问题)。 key的值需为字符串或数字,且在当前循环中唯一。
所以在后续我们要对 v-for 的每个元素进行针对性的操作的时候 用的索引也是这个 key
五、注意事项
**避免在
v-for中使用v-if**:两者同时存在时,v-for优先级更高,可能导致不必要的循环执行。如需过滤数据,建议在逻辑层(computed)处理:1
2
3
4
5
6
7
8
9<template>
<li v-for="user in activeUsers" :key="user.id">
{{ user.name }}
</li>
</template>
<script setup>
import { computed } from "vue";
const activeUsers = computed(() => users.filter((u) => u.isActive));
</script>遍历范围:
v-for只能用于单个元素或<template>,不能直接用于<slot>、v-text等指令。性能优化:对于大数据列表,可结合
v-memo或虚拟滚动(如vue-virtual-scroller)减少渲染压力。
四、事件处理指令
v-on(简写 @)
- 作用:绑定事件监听器(DOM 事件或自定义事件)
- 用法:
v-on:事件名="处理函数"或@事件名="处理函数" - 扩展:支持事件修饰符(如
.stop、.prevent)、按键修饰符(如.enter)
1 | <!-- 基础用法 --> |
五、表单输入绑定指令
v-model
- 作用:在表单元素(input、select、textarea 等)上创建双向数据绑定
- 特点:自动同步表单值与数据,适用于用户输入场景
- 修饰符:
.trim(去除首尾空格)、.number(转为数字)、.lazy(失焦后同步)
1 | <!-- 文本输入 --> |
六、其他实用指令
1. v-pre
- 作用:跳过元素及其子元素的编译过程(直接渲染原始内容,不解析插值和指令)
1 | <div v-pre>{{ 这里的插值不会被解析,会原样显示 }}</div> |
2. v-cloak
- 作用:在 Vue 实例编译完成前隐藏元素,避免页面闪烁(配合 CSS 使用)
1 | [v-cloak] { |
1 | <div v-cloak>{{ message }}</div> |
3. v-once
- 作用:元素和组件只渲染一次,后续数据变化不会重新渲染(优化静态内容性能)
1 | <div v-once>{{ staticMessage }}</div> |
总结
Vue 的 v- 指令是模板语法的核心,覆盖了数据绑定、条件渲染、列表循环、事件处理、表单交互等常见场景。掌握这些指令的用法,能高效实现各种交互逻辑,其中:
v-bind和v-on是最基础的绑定指令(简写:和@需熟练使用);v-if/v-show、v-for用于控制元素渲染;v-model是表单交互的核心;- 其他指令(
v-pre、v-cloak等)用于特定优化场景。
【响应式数据】
ref 能用来定义基本类型数据 和 对象类型数据(但底层上也是在 ref 中调用了 reactive)
reactive 用来定义对象类型数据
在模板中使用 ref 响应式数据的时候直接使用即可
1 | <div> |
但在 js 代码中需要对响应式数据进行操控的时候必须对 响应式数据 . value 进行操作
1 | fuction changeName() { |
注意同样地,当用 ref 定义对象类型的响应数据的时候一定不要忘了需要写 value
1 | let games = [ |
reactive 是深层次的 只要是被 reactive 包裹的对象 无论其中层级多深都会被改变
1 | let moutain = reactive({ |
宏观角度看:
- ref 用来定义:基本类型数据、对象类型数据;
- reactive 用来定义:对象类型数据。
- 区别:
- ref 创建的变量必须使用 .value(可以使用 volar 插件自动添加 .value)。
- reactive 重新分配一个新对象,会失去响应式(可以使用 Object.assign 去整体替换)。
什么意思呢 即如果把原对象数据直接赋值成一个新的对象 则这个对象会失去响应式
解决这个问题的方法如下
1 | let car = {brand:"benchi",price: 100} |
Object.assign
1 | Object.assign(target, ...sources); |
target:目标对象,也就是要将其他对象属性合并进去的对象,该对象会被修改。
...sources:一个或多个源对象,这些对象的可枚举自身属性(包括 Symbol 属性以外的可枚举属性)会被复制到目标对象。
可以进行几个对象元素的合并 当源对象中有同名属性时,后面源对象的属性会覆盖前面源对象的同名属性。
Object.assign() 执行的是浅拷贝,也就是说,如果源对象的属性值是一个对象,那么复制的是该对象的引用,而不是创建一个新的对象。需要注意的是,由于 Object.assign() 是浅拷贝,对于包含嵌套对象的情况,如果需要深拷贝,就需要使用如 JSON.parse(JSON.stringify()) 或者第三方库(如 lodash 的 cloneDeep 方法 )等其他方式来处理。
但是如果用的是 ref 包裹对象的话 就不会有这种烦恼 因为在改变的过程中 我们会直接写
1 | function changeCar() { |
- 使用原则:
- 若需要一个基本类型的响应式数据,必须使用 ref。
- 若需要一个响应式对象,层级不深,ref、 reactive 都可以。
- 若需要一个响应式对象,且层级较深,推荐使用 reactive。
【toRefs 与 toRef】
- 作用:将一个响应式对象中的每一个属性,转换为
ref对象。 - 备注:
toRefs与toRef功能一致,但toRefs可以批量转换。 - 语法如下:
1 | <template> |
【computed】
在 Vue 3 中,computed计算属性主要用于根据响应式数据(如ref、reactive、Pinia 状态等)派生新的响应式数据,它的核心作用是缓存计算结果并保持响应式依赖跟踪。
在我们后面用 pinia 进行组件通信的时候是经常用到它这个特性的。
作用:根据已有数据计算出新数据(和Vue2中的computed作用一致)。
computed 的一大好处(特点)是 它当且仅当涉及到(依赖)的变量数据发生改变的时候才进行计算 即就算大量引用这个计算属性数据 只要依赖数据没发生改变 也只会进行一次计算(即计算属性是有缓存的)
注意在其中我们会写 get 和 set 方法 其中 get 方法用来返回值(即对读取值进行一些操作)
set 方法用来设置新值
看下面代码 fullName 是我们用计算属性 computed 来包裹的 它的数据类型是一个 Computed RefImpl 当我们对其 value 进行修改时 可以通过 set 方法拿到这个新设置的 value 并且对它进行操作
并且 像我们下面写的那个 changefullname 函数是当且仅当对 fullName 设置了 set 方法 让其不止可读的时候才能使用
1 | <template> |
【watch】
- 作用:监视数据的变化(和
Vue2中的watch作用一致) - 特点:
Vue3中的watch只能监视以下四种数据:
ref定义的数据。注意:这里定义的数据是不用写.value 的 应该直接写这个数据本身reactive定义的数据。- 函数返回一个值(
getter函数)。- 一个包含上述内容的数组。
watch 监视的基本语法
1 | watch(被监视的目标数据, 回调函数); |
watch 有一个返回值 可以用来终止监视 基本的写法为
1 | const stopWatch = watch(sum, (newValue, oldValue) => { |
我们在Vue3中使用watch的时候,通常会遇到以下几种情况:
* 情况一
监视ref定义的【基本类型】数据:直接写数据名即可,监视的是其value值的改变。
1 | <template> |
* 情况二
监视ref定义的【对象类型】数据:直接写数据名,监视的是对象的【地址值】,若想监视对象内部的数据,要手动开启深度监视。
下面的这个注意可以类比为一个房子 改变属性相当于装修 不会改变 value(因为地址没有改变) 而改变整个对象才相当于换房子 会改变 value (地址改变了)
注意:
若修改的是
ref定义的对象中的属性,newValue和oldValue都是新值,因为它们是同一个对象。若修改整个
ref定义的对象,newValue是新值,oldValue是旧值,因为不是同一个对象了。总结来说 只要对象地址没有改变 new 和 old 就是一个值 否则就是不同值
1 | <template> |
* 情况三
监视reactive定义的【对象类型】数据,且默认开启了深度监视。
注意这里改变对象 new 和 old 也是一样的 因为我们之前说过 用 Object.assign 修改对象的时候 是把相同的元素值更改掉 并没有改变其地址值 只是改变了元素值 故而 new 和 old 是一样的
1 | <template> |
* 情况四
监视ref或reactive定义的【对象类型】数据中的某个属性,注意点如下:
- 若该属性值不是【对象类型】,即基本类型,需要写成函数形式,直接写一个箭头函数即可。 为什么可以改成函数形式呢 其实就是我们之前所说的 watch 能够监视的其中一个类型 函数的返回值
- 若该属性值是依然是【对象类型】,可直接编,也可写成函数,建议写成函数。
结论:监视的要是对象里的属性,那么最好写函数式,注意点:若是对象监视的是地址值,需要关注对象内部,需要手动开启深度监视。
1 | <template> |
监视 对象类型的元素 最佳实践
1 | // 监视,情况四:监视响应式对象中的某个属性,且该属性是对象类型的,可以直接写,也能写函数,更推荐写函数 |
* 情况五
监视上述的多个数据
1 | <template> |
【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<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";
// 数据
let temp = ref(0);
let height = ref(0);
// 方法
function changePrice() {
temp.value += 10;
}
function changeSum() {
height.value += 1;
}
// 用watch实现,需要明确的指出要监视:temp、height
watch([temp, height], (value) => {
// 从value中获取最新的temp值、height值
const [newTemp, newHeight] = value;
// 室温达到50℃,或水位达到20cm,立刻联系服务器
if (newTemp >= 50 || newHeight >= 20) {
console.log("联系服务器");
}
});
// 用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>
【标签的 ref 属性】
作用:用于注册模板引用。
用在普通
DOM标签上,获取的是DOM节点。用在组件标签上,获取的是组件实例对象。
用在普通DOM标签上:
1 | <template> |
用在组件标签上:
1 | <!-- 父组件App.vue --> |
总结来说 当我们将 ref 标记在组件上后 就能获得这个组件实例
但问题是这个组件实例中的元素内容并不是开放的 我们需要自己去定义我们在这个组件中需要什么
1 | <!-- 子组件Person.vue中要使用defineExpose暴露内容 --> |
当我们进行完这个操作后就可以通过 ren 来调用其中的元素了
【props】
1
2
3
4
5
6
7
8
9 // 定义一个接口,限制每个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 <template>
<Person :list="persons" />
</template>
<script lang="ts" setup name="App">
import Person from "./components/Person.vue";
import { reactive } from "vue";
import { type Persons } from "./types";
let 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 <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'
import {type PersonInter} from '@/types' //注意在这里引入类型规范的时候必须在前面加type关键字
// 第一种写法:仅接收
// const props = defineProps(['list'])
// 第二种写法:接收+限制类型
// defineProps<{list:Persons}>()
// 第三种写法:接收+限制类型+指定默认值+限制必要性
let props = withDefaults(defineProps<{list?:Persons}>(),{
list:()=>[{id:'asdasg01',name:'小猪佩奇',age:18}]
})
console.log(props)
</script>
接口(Interface):
PersonInter接口定义了 Person 对象的结构规范,强制要求包含id(string 类型)、name(string 类型)和age(number 类型)三个属性,确保数据格式一致性。类型别名(Type Alias):
Persons类型通过Array<PersonInter>定义了一个由PersonInter对象组成的数组类型,简化了数组类型的使用。代码中
Array<PersonInter>使用了泛型语法,Array是 TypeScript 内置的泛型接口,<PersonInter>指定了数组元素的类型。Vue 3 的
reactive函数是泛型函数,reactive<Persons>(...)通过泛型参数指定了响应式对象的类型,确保只能存储符合Persons类型的数据。
【模板静态属性与动态属性】
Vue 在编译模板时,会根据是否有v-bind(或:)来区分处理方式:
- 不带
:的属性:直接作为静态字符串写入 DOM,不参与 Vue 的响应式系统。 - 带
:的属性:将表达式解析为 JavaScript 代码,在组件渲染时动态计算值,并且会追踪表达式中依赖的响应式数据(如ref、reactive对象),当依赖变化时自动重新计算并更新属性值。
简单来说就是 带冒号:的属性会去动态读取冒号后面的内容
动态属性(v-bind)
(1)绑定基本表达式
可以直接使用 JavaScript 表达式(运算、三元判断等):
1 | <template> |
(2)绑定响应式数据
结合 Vue 的响应式数据(ref/reactive),实现动态更新:
1 | <template> |
(3)绑定对象 / 数组(用于 class/style)
动态绑定class或style时,可直接传入对象或数组:
1 | <template> |
(4)绑定属性名(动态属性名)
属性名也可以通过表达式动态生成(使用[]包裹):
1 | <template> |
(5)绑定布尔属性
对于布尔属性(如disabled、checked),值为false时会移除该属性:
1 | <template> |
注意事项
- 表达式限制:
v-bind后的表达式只能是单个表达式(不能是语句或流程控制,如if、for)。
错误示例:<div :a="if (flag) { 1 } else { 2 }"></div>(应改用三元表达式)。 - 避免副作用:表达式中不应包含副作用操作(如
++、--或函数调用修改数据),可能导致渲染异常。 - 简写规则:所有 HTML 属性都可以用
:简写,包括自定义属性(如data-*)。
总结
a="xxx":静态字符串,直接渲染,不参与动态计算。:a="xxx":动态绑定,xxx作为 JavaScript 表达式计算,支持响应式数据,会随依赖更新。
【props】
为什么要使用 props 呢
在 Vue 组件化开发中,props是实现组件间通信的核心机制之一,主要用于父组件向子组件传递数据,它的作用和必要性可以从以下几个方面理解:
- 实现组件间的数据传递
组件化开发的核心思想是 “拆分复杂 UI 为独立可复用的组件”,而组件之间往往需要共享数据。props就是 Vue 为这种场景设计的标准数据传递通道:
- 父组件通过
props向子组件传递数据(如示例中App.vue向Person.vue传递persons数组)。 - 子组件通过声明
props接收数据,并在自身模板或逻辑中使用。
没有props,子组件无法直接获取父组件的数据,组件间就会变成孤立的 “信息孤岛”。
- 保证组件的独立性和复用性
props的设计遵循了 “单向数据流” 原则:父组件传递数据,子组件只能使用数据,不能直接修改(若需修改,需通过事件通知父组件)。这种机制带来两个好处:
- 组件独立性:子组件的行为只依赖于接收的
props,不直接操作父组件数据,避免组件间耦合过深。 - 高复用性:同一个子组件(如
Person.vue)可以通过接收不同的props数据,在不同场景下渲染不同内容(例如既可以展示 “张三、李四”,也可以展示 “小明、小红”)。
- 提供数据校验和类型约束
在 Vue 中,props支持对接收的数据进行校验(尤其是结合 TypeScript 时):
- 可以限制数据类型(如示例中
list必须是Persons类型的数组)。 - 可以设置默认值(如示例中
withDefaults指定的默认数组)。 - 可以指定是否为必填项(通过
?标识可选)。
这些校验能在开发阶段就暴露数据传递的错误(如父组件传了一个字符串而非数组),减少运行时 bug。
- 明确组件的接口规范
props相当于子组件对外暴露的 “数据接口”,其他开发者使用该组件时,只需查看props定义就知道:
- 这个组件需要哪些数据才能工作?
- 每个数据的格式、类型是什么?
- 哪些数据是可选的(有默认值)?
例如看到Person.vue的props定义,就知道它需要一个list数组,数组元素必须包含id、name、age,从而快速理解如何使用该组件。
总结
props的核心价值是在保证组件独立性的前提下,安全、规范地实现父向子的数据传递
1
2
3
4
5
6
7
8
9 // 定义一个接口,限制每个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 <template>
<Person :list="persons" />
</template>
<script lang="ts" setup name="App">
import Person from "./components/Person.vue";
import { reactive } from "vue";
import { type Persons } from "./types";
let 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'
import {type PersonInter} from '@/types'
// 第一种写法:仅接收
// const props = defineProps(['list'])
// 第二种写法:接收+限制类型
// defineProps<{list:Persons}>()
// 第三种写法:接收+限制类型+指定默认值+限制必要性
//这里的问号的含义是不一定需要 没有传递的时候 就不会进行遍历
let props = withDefaults(defineProps<{list?:Persons}>(),{
list:()=>[{id:'asdasg01',name:'小猪佩奇',age:18}]
//withDefaults的默认值必须是一个函数返回值 即这里我们写的这个箭头函数
})
console.log(props)
</script>
【生命周期】
直观地说 什么是生命周期呢? 其实就是一个组件的一生
什么是挂载? 其实就是把组件显示在浏览器上
父子生命周期的核心规律 用一句话概括所有场景的共性: “父先开始,子先完成”
- 挂载 / 更新 / 卸载的 “准备阶段”(如
onBeforeMount/onBeforeUpdate/onBeforeUnmount):父组件先执行; - 挂载 / 更新 / 卸载的 “完成阶段”(如
onMounted/onUpdated/onUnmounted):子组件先执行,父组件后执行。
概念:
Vue组件实例在创建时要经历一系列的初始化步骤,在此过程中Vue会在合适的时机,调用特定的函数,从而让开发者有机会在特定阶段运行自己的代码,这些特定的函数统称为:生命周期钩子规律:
生命周期整体分为四个阶段,分别是:创建、挂载、更新、销毁,每个阶段都有两个钩子,一前一后。
Vue2的生命周期创建阶段:
beforeCreate、created挂载阶段:
beforeMount、mounted更新阶段:
beforeUpdate、updated销毁阶段:
beforeDestroy、destroyedVue3的生命周期创建阶段:
setup挂载阶段:
onBeforeMount、onMounted更新阶段:
onBeforeUpdate、onUpdated卸载阶段:
onBeforeUnmount、onUnmounted常用的钩子:
onMounted(挂载完毕)、onUpdated(更新完毕)、onBeforeUnmount(卸载之前)示例代码:
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<template>
<div class="person">
<h2>当前求和为:{{ sum }}</h2>
<button @click="changeSum">点我sum+1</button>
</div>
</template>
<!-- vue3写法 -->
<script lang="ts" setup name="Person">
import {
ref,
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
} from "vue";
// 数据
let 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>
【自定义 hook】
什么是
hook?—— 本质是一个函数,把
setup函数中使用的Composition API进行了封装,类似于vue2.x中的mixin。更加详细地说 我们为什么要定义一个 hook 就是因为我们需要把不同的 api(不同的功能)分隔开 即某个功能的方法和数据我们定义在一个 hook 里 我们就可以复用这个 api 方便后续地维护 简洁地组合各个 api 其实这也是我们为什么说 vue3 使用的是组合式 api
自定义
hook的优势:复用代码, 让setup中的逻辑更清楚易懂。
在写自定义 hook 的时候的一些注意事项:
1.需要把整个函数给 export 出去
(export default 后面直接加值 不能给这个函数命名 如果直接写 export 就应该给函数命名)
2.最后在函数中我们需要把数据和方法给 return 出去 才能拿到这些方法
3.在 hook 里我们也可以写关于这个功能的生命周期钩子函数
示例代码:
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
26
27
28import {reactive,onMounted} from 'vue'
import axios,{AxiosError} from 'axios'
export default function(){
let dogList = reactive<string[]>([])
// 方法
async function getDog(){
try {
// 发请求
let {data} = await axios.get('https://dog.ceo/api/breed/pembroke/images/random')
// 维护数据
dogList.push(data.message)
} catch (error) {
// 处理错误
const err = <AxiosError>error
console.log(err.message)
}
}
// 挂载钩子
onMounted(()=>{
getDog()
})
//向外部暴露数据
return {dogList,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<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 useSum from "./hooks/useSum";
import useDog from "./hooks/useDog";
let { sum, increment, decrement } = useSum();
let { dogList, getDog } = useDog();
</script>
【路由】
通俗地说什么是路由
路由其实就是一组 key-value 的对应关系
多个路由 需要经过路由器的管理
那么进一步地 如果我们想要实现路由的基本实现 需要准备哪些东西呢?
1.一个基本的导航区(用于点击跳转路由) 一个基本的展示区(用于显示路由对应组件内容)
2.创建路由器
3.制定路由的具体规则(什么路径 path? 该路径对应什么组件 component?)
4.完成我们对应需要的这些组件(**???.vue**)
【路由切换的基本实现】
Vue3中要使用vue-router的最新版本,目前是4版本。
1 | npm i vue-router |
- 我们想要实现对多个路由的切换首先需要实现路由的管理 那么像上面说的路由的管理需要路由器 router 所以首先我们需要创建一个 router 出来
1 | import { createRouter, createWebHistory } from "vue-router"; // 引入 |
main.ts代码如下:1
2
3
4import router from "./router/index";
app.use(router); // router的挂载
app.mount("#app");
这个时候 我们让路由和组件一一对应了 相当于我们已经将组件引导到了正确的门前 但问题是 此时这个门并没有打开 我们在展示区并不能看到其内容 这是因为我们 需要设置路由出口 RouterView
<!-- 展示区 --> <div class="main-content"> <RouterView></RouterView> </div> <script lang="ts" setup name="App"> import { RouterView } from "vue-router"; </script>这样我们就可以实现点击导航实现路由的跳转啦1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
此时我们就可以通过改变地址后的 path (例如/home)来在显示区显示对应组件内容
* 此时我们还想要实现一个点击跳转的效果 即点击其导航标题就能跳转到对应的组件内容 显然我们需要一个像 a 超链接一样的东西 本质是实现 **路由的跳转(进一步地说 应该是 path 的跳转)** 为了实现这个功能 我们用到 vue-router 中自带的 **RouterLink** 其标签是 **to** 后面是跳转到的 path
* ```html
<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>
<script lang="ts" setup name="App">
import { RouterLink } from "vue-router";
</script>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>
【两个注意点】
1.路由组件通常存放在
pages或views文件夹,一般组件通常存放在components文件夹。如何区分一般组件和路由组件呢?
路由组件:按照路由的规则渲染出来的
1
2
3
4 routes:[
{path:'/demo',component:Demo}
]
//组件为Demo.vue对于这个组件 我们一般需要用路由出口 orRouterlink 来渲染这个组件 像这样的组件就称之为路由组件
路由组件我们放在 pages 或者 vies 文件夹中 为了实现一个页面中的一些小组件 我们会在每一个 pages 下面再单独写一个 components 文件夹用于存放页面内一般组件
一般组件:亲手写标签渲染出来的组件
1
2
3
4
5
6
7
8
9 //假设我们现在有一个组件为Header.vue 如果像现在这么写的话 这个组件就是一般组件
<script>
import Header form './components/Header.vue'
</script>
<template>
<Header />
<template></template>
</template>像这样 我们需要自己写标签渲染出来的组件我们称之为一般组件
一般组件我们放在 components 文件夹中
一般在我们的实际开发中 我们一般会在路由组件下面写一般组件 (也就是拆组件)
2.通过点击导航,视觉效果上“消失” 了的路由组件,默认是被卸载掉的,需要的时候再去挂载。
【路由器工作模式】
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模式
/******/
});
【to 的两种写法】
1 | <!-- 第一种:to的字符串写法 --> |
【命名路由】
作用:可以简化路由跳转及传参(后面就讲)
注意这里的 name 不一定要和 path 或者 component 的名字一样 这个 name 是用来自定义的 主要是便于我们后期的路由跳转问题
给路由规则命名:
1 | routes: [ |
跳转路由:
1 | <!--简化前:需要写完整的路径(to的字符串写法) --> |
【嵌套路由】
编写
News的子路由:Detail.vue配置路由规则,使用
children配置项: 需要注意 子组件中的 path 不用写 /对于子路由的最简单的一种应用场景就是比如说我们需要在一个页面 article 里跳转到这个 article 的详情页 detail
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
28const 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;跳转路由(记得要加完整路径):(注意这里对象跳转的话使用名称也是可以实现的)
1
2
3<router-link to="/news/detail">xxxx</router-link>
<!-- 或 -->
<router-link :to="{ path: '/news/detail' }">xxxx</router-link>记得去
Home组件中预留一个<router-view>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<template>
<div class="news">
<nav class="news-list">
<RouterLink
v-for="news in newsList"
:key="news.id"
:to="{ path: '/news/detail' }"
>
{{ news.name }}
</RouterLink>
</nav>
<div class="news-detail">
<RouterView />
</div>
</div>
</template>
【路由传参·query 参数】
query 参数可以用来解决什么问题呢
我们还是以之前的 article 和 detail 为例 显然我们想要实现的效果是我们希望一个 detail 能够显示出来对应文章的细节 想要实现效果我们至少需要一个响应式的东西 在这种情况下我们使用 query 接受参数 通过接受的参数我们就可以把这个响应式的东西放进我们的 detail 里
传递参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24<!-- 跳转并携带query参数(to的字符串写法) -->
<router-link to="/news/detail?a=1&b=2&content=欢迎你">
跳转
</router-link>
<!-- 跳转并携带query参数(to的对象写法) -->
<li v-for="news in newsList" :key="news.id">
<RouterLink
:to="{
//name:'xiang', //用name也可以跳转
path:'/news/detail',
//在v-for里 我们可以直接调用这个views
query:{
id:news.id,
title:news.title,
content:news.content
} // 这里就是我们传入的三个参数
}"
>
{{news.title}}
</RouterLink>
</li>
//当然我们需要在newsList里面写好这些数据
这里就省去了(实际开发中是后端给的)接收参数:
我们用 route.query.具体参数 的方式来接受这些参数 我们在下面简单地写一个
1
2
3
4
5
6
7
8
9
10
11
12
13
14<template>
<ul>
<li>编号:{{ route.query.id }}</li>
<li>标题:{{ route.query.title }}</li>
<li>内容:{{ route.query.content }}</li>
</ul>
</template>
<script setup lang="ts">
import { useRoute } from "vue-router";
const route = useRoute();
// 打印query参数
//console.log(route.query)
</script>
这样我们就可以实现 detail 和文章的一一对应了!
【路由传参·params 参数】
params 参数实现的功能基本上跟 query 是一致的 这里就不再写了 主要是写法不一样
定义路由
在我们使用 params 参数定义路由的时候必须用:来占位所需的参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// 路由配置文件(如router/index.js)
import { createRouter, createWebHistory } from "vue-router";
import User from "../views/User.vue";
const routes = [
{
path: "/user/:id/:title/:content", // 定义params参数 必须提前占位
name: "User",
component: User,
},
];
const router = createRouter({
history: createWebHistory(),
routes,
});
export default router;传递参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18<!-- 跳转并携带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.title,
},
}"
>
{{news.title}}
</RouterLink>接收参数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<template>
<ul>
<li>编号:{{route.params.id}} </li>
<li>标题:{{route.params.title}}</li>
<li>内容:{{route.params.content}} </li>
</ul>
</template>
<script setup lang = "ts">
import {useRoute} from 'vue-router'
const route = useRoute()
</script>
必须配合路由的 name 属性使用:传递
params时,to对象中必须使用name来指定路由,不能使用path显示在 URL 中:参数会作为 URL 的一部分,例如
/user/123/张三刷新页面参数不丢失:因为参数存储在 URL 中,刷新页面后仍然可以获取
必填性:如果路由定义了
params参数,导航时必须传递,否则会导致路由匹配失败如果我们希望 params 参数是可传可不传的 直接在定义路由的时候再参数后面加?即可
1
2
3
4
5
6 // 路由配置
{
path: '/user/:id/:name?', // name参数可选
name: 'User',
component: User
}没有默认值:不能像
query参数那样设置默认值,需要在组件中处理未传递的情况
【路由的 props 配置】
作用:让路由组件更方便的收到参数(可以将路由参数作为props传给组件) 也就是能简化 detail 部分的写法
我们知道一般组件也是有 props 参数的 也就是该组件接受的数据
其实路由的 props 配置跟组件的 props 有很大关系
简单来说 当我们设置了 props 后 会把这些参数作为组件的 props
以布尔值写法为例
1 | { |
相当于我们把 Detail 组件变成了一个一般组件 然后我们把 path 中占位的 id title content(即收到的 params 参数)写在一般组件中作为一般组件的 props
1 | //等价于 <Detail id="??" title="??" content="??" /> |
那么对应的我们需要在接受参数的地方定义一下我们需要的 props
1 | <template> |
显然这样我们就做了极大的简化
这里
1 | { |
【 replace 属性】
作用:控制路由跳转时操作浏览器历史记录的模式。
浏览器的历史记录有两种写入方式:分别为
push和replace:push是追加历史记录(默认值)。 (就是这些历史记录 or 页面都是放在栈里的 可以前进后退)replace是替换当前记录 。 (直接替换掉当前内容 不支持前进后退)
开启
replace模式:1
<RouterLink replace .......>News</RouterLink>
【编程式导航】
什么是编程式导航呢 简单来说就是 脱离
什么时候会用编程式导航呢 一般地,当路由跳转需要逻辑(如完成某些操作才跳转路由)的时候我们需要使用编程式导航
Vue3 中实现编程式导航主要依赖 vue-router 提供的 useRouter 组合式 API,以下是其核心用法:
基本用法
首先需要导入并创建路由实例:
1 | import { useRouter } from "vue-router"; |
我们会使用
1 | //导航到特定地址 |
这里的 push 和 replace 就对应着上面介绍的两种浏览器历史记录写入方式
push 或者 replace 后面地址的写法 跟 routerlink 中 to 参数的写法一模一样
【重定向】
作用:将特定的路径,重新定向到已有路由。
具体编码:
1
2
3
4{
path:'/',
redirect:'/about'
}
可以用来处理页面初始地址的问题
【pinia】
pinia 用于集中式状态(数据)管理
我们需要把一些需要共享的数据交给集中式状态管理
【搭建 pinia 环境】
第一步:npm install pinia
第二步:操作src/main.ts
1 | import { createApp } from "vue"; |
此时开发者工具中已经有了pinia选项
【存储+读取数据】
Store是一个保存:状态、业务逻辑 的实体,每个组件都可以读取、写入它。 其实它就相当于 pinia 的具体实现部分 我们需要在 src 目录下创建一个 store 文件夹用于存放各个组件的 pinia 从另一个角度说 由于我们把这些数据放在了 store 里 那么我们就可以在其他组件中复用这些数据 这也就是我们所说的存放共享数据它有三个概念:
state、getter、action,相当于组件中的:data、computed和methods。具体编码:
src/store/count.ts1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// 引入defineStore用于创建store
import { defineStore } from "pinia";
// 定义并暴露一个store
export const useCountStore = defineStore("count", {
// 动作
actions: {},
// 状态 必须是一个函数然后返回某些值
state() {
return {
sum: 6,
};
},
// 计算
getters: {},
});具体编码:
src/store/talk.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 useTalkStore = defineStore("talk", {
// 动作
actions: {},
// 状态
state() {
return {
talkList: [
{ id: "yuysada01", content: "你今天有点怪,哪里怪?怪好看的!" },
{ id: "yuysada02", content: "草莓、蓝莓、蔓越莓,你想我了没?" },
{ id: "yuysada03", content: "心里给你留了一块地,我的死心塌地" },
],
};
},
// 计算
getters: {},
});组件中使用
state中的数据1
2
3
4
5
6
7
8
9
10
11<template>
<h2>当前求和为:{{ sumStore.sum }}</h2>
</template>
<script setup lang="ts" name="Count">
// 引入对应的useXxxxxStore
import { useSumStore } from "@/store/sum";
// 调用useXxxxxStore得到对应的store
const sumStore = useSumStore();
</script>1
2
3
4
5
6
7
8
9
10
11
12
13
14<template>
<ul>
<li v-for="talk in talkStore.talkList" :key="talk.id">
{{ talk.content }}
</li>
</ul>
</template>
<script setup lang="ts" name="Count">
import axios from "axios";
import { useTalkStore } from "@/store/talk";
const talkStore = useTalkStore();
</script>这里还有一个小小的注意点是 当我们在组件中使用这些数据的时候 虽然像 sum 这样的数据是 ref 但是我们并不需要.value 因为它是放在响应式对象之中的 会自动拆包不需要额外.value 了
总结:响应式对象中的响应式数据会被自动拆包 间接地使用都不要加.value
我们在这里写一小段代码
1
2
3
4
5
6
7
8
9
10
11
12let x = ref(6);
let ovj = reactive({
a: 1,
b: 2,
c: ref(3),
});
console.log(obj.a);
console.log(obj.b);
console.log(obj.c); // 在reactive中这个ref已经被拆包了
console.log(x.value); // 直接定义的ref当我们想获得值的时候当然需要.value
【修改数据的三种方式】
第一种修改方式,直接修改
1
countStore.sum = 666;
第二种修改方式:批量修改
1
2
3
4countStore.$patch({
sum: 999,
school: "atguigu",
});第三种修改方式:借助
action修改(action中可以编写一些业务逻辑)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21import { defineStore } from "pinia";
export const useCountStore = defineStore("count", {
/*************/
actions: {
//加
increment(value: number) {
if (this.sum < 10) {
//操作countStore中的sum 注意 这里的this指向的是当前的store
this.sum += value;
}
},
//减
decrement(value: number) {
if (this.sum > 1) {
this.sum -= value;
}
},
},
/*************/
});组件中调用
action即可1
2
3
4
5// 使用countStore
const countStore = useCountStore();
// 调用对应action
countStore.incrementOdd(n.value);
【storeToRefs】
- 借助
storeToRefs将store中的数据转为ref对象,方便在模板中使用。 - 注意:
pinia提供的storeToRefs只会将数据做转换,而Vue的toRefs会将store中所有东西都转换成 ref(显然是我们不想看到的)。
1 | <template> |
【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// 引入defineStore用于创建store
import { defineStore } from "pinia";
// 定义并暴露一个store
export const useCountStore = defineStore("count", {
// 动作
actions: {
/************/
},
// 状态
state() {
return {
sum: 1,
school: "atguigu",
};
},
// 计算
getters: {
bigSum: (state): number => state.sum * 10,
upperSchool(): string {
return this.school.toUpperCase();
},
},
});组件中读取数据:
1
2const { increment, decrement } = countStore;
let { sum, school, bigSum, upperSchool } = storeToRefs(countStore);
【$subscribe 订阅】
通过 store 的 $subscribe() 方法侦听 state 及其变化
1 | talkStore.$subscribe((mutate, state) => { |
补充一小点 上面我们把数据储存到浏览器本地储存 这样就可以一直保留这个数据 并且对于数据的共享更加灵活 当然对应的 在我们的 store 里也应该对应做出一些改变
1 | state(){ |
这样我们就能拿到储存在浏览器本地的数据了
【store 组合式写法】
1 | import { defineStore } from "pinia"; |
【组件通信】
【props】
概述:props是使用频率最高的一种通信方式,常用与 :父 ↔ 子。
- 若 父传子:属性值是非函数。
- 若 子传父:属性值是函数。
父组件:
父组件传递 Props
父组件通过属性绑定的方式向子组件传递数据,与 HTML 属性写法类似。
1 | <template> |
子组件
子组件定义与接收 Props
在 Vue 3 中,子组件通过 defineProps() 宏来声明接收的 props(无需导入,Vue 会自动处理)。
1 | <template> |
我们简单在这里写一点东西来解释一下上面的父传子和子传父 我们说父传子传非函数 如父组件中的 car 我们只需要暴露出来此参数 然后直接用插值语法 子传父传函数 但我们实际上需要在父组件中写一个函数来接受这个传递的数据 这也就是我们为什么说子传父传函数 并且这个函数当我们 defineprops 后是可以直接使用的
【自定义事件】
在 Vue 3 中,自定义事件是子组件向父组件传递数据或触发父组件逻辑的重要方式,与 props(父传子)形成互补,共同实现组件间的双向通信。
基本概念
自定义事件允许子组件主动触发一个事件,并传递数据给父组件,父组件通过监听该事件来执行相应的处理逻辑。
基本用法
- 子组件触发自定义事件
子组件通过 defineEmits() 宏声明可以触发的事件(无需导入),然后使用 emit 函数触发事件并传递数据。
1 | <!-- 子组件 Child.vue --> |
- 父组件监听自定义事件
父组件通过 v-on(简写 @)监听子组件触发的事件,并定义处理函数接收数据。
1 | <!-- 父组件 Parent.vue --> |
- 概述:自定义事件常用于:子 => 父。
- 注意区分好:原生事件、自定义事件。
- 原生事件:
- 事件名是特定的(
click、mosueenter等等) - 事件对象
$event: 是包含事件相关信息的对象(pageX、pageY、target、keyCode)
- 事件名是特定的(
- 自定义事件:
- 事件名是任意名称
- 事件对象
$event: 是调用emit时所提供的数据,可以是任意类型!!!
示例:
1
2
3
4
5<!--在父组件中,给子组件绑定自定义事件:-->
<Child @send-toy="toy = $event" />
<!--注意区分原生事件与自定义事件中的$event-->
<button @click="toy = $event">测试</button>1
2//子组件中,触发事件:
this.$emit("send-toy", 具体数据);
【mitt】
概述:与消息订阅与发布(pubsub)功能类似,可以实现任意组件间通信。
简单地比喻一下 mitt 的作用 其实就是一个中间人的角色 能够脱离父子关系 进行任意组件间的通信
安装mitt
1 | npm i mitt |
新建文件:src\utils\emitter.ts
1 | // 引入mitt |
接收数据的组件中:绑定事件、同时在销毁前解绑事件:
1 | import emitter from "@/utils/emitter"; |
【第三步】:提供数据的组件,在合适的时候触发事件
1 | import emitter from "@/utils/emitter"; |
注意这个重要的内置关系,总线依赖着这个内置关系
【v-model】
v-model 双向绑定 所谓双向就是 能够实现从数据到页面 再从页面到数据 同时既能子传父又能父传子
概述:实现 父 ↔ 子 之间相互通信。
前序知识 ——
v-model的本质1
2
3
4
5
6
7
8
9<!-- 使用v-model指令 -->
<input type="text" v-model="userName">
<!-- v-model的本质是下面这行代码 -->
<input
type="text"
:value="userName"
@input="userName =(<HTMLInputElement>$event.target).value"
>组件标签上的
v-model的本质::moldeValue+update:modelValue事件。1
2
3
4
5
6
7
8<!-- 组件标签上使用v-model指令 -->
<AtguiguInput v-model="userName" />
<!-- 组件标签上v-model的本质 -->
<AtguiguInput
:modelValue="userName"
@update:model-value="userName = $event"
/>AtguiguInput组件中:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18<template>
<div class="box">
<!--将接收的value值赋给input元素的value属性,目的是:为了呈现数据 -->
<!--给input元素绑定原生input事件,触发input事件时,进而触发update:model-value事件-->
<input
type="text"
:value="modelValue"
@input="emit('update:model-value', $event.target.value)"
/>
</div>
</template>
<script setup lang="ts" name="AtguiguInput">
// 接收props
defineProps(["modelValue"]);
// 声明事件
const emit = defineEmits(["update:model-value"]);
</script>也可以更换
value,例如改成abc1
2
3
4
5<!-- 也可以更换value,例如改成abc-->
<AtguiguInput v-model:abc="userName" />
<!-- 上面代码的本质如下 -->
<AtguiguInput :abc="userName" @update:abc="userName = $event" />AtguiguInput组件中:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<template>
<div class="box">
<input
type="text"
:value="abc"
@input="emit('update:abc', $event.target.value)"
/>
</div>
</template>
<script setup lang="ts" name="AtguiguInput">
// 接收props
defineProps(["abc"]);
// 声明事件
const emit = defineEmits(["update:abc"]);
</script>如果
value可以更换,那么就可以在组件标签上多次使用v-model1
<AtguiguInput v-model:abc="userName" v-model:xyz="password" />
这里我们简单辨析一下上述代码中写到过的$event
$event 到底是啥 啥时候能.target 呢?
对于原生事件 $event 就是事件对象 所以可以使用.target
对于自定义事件,$event就是触发事件时所传递的数据 (即上面写的$event.target.value) 所以不能.target
【$attrs 】
概述:
$attrs用于实现当前组件的父组件,向当前组件的子组件通信(祖 → 孙)。具体说明:
$attrs是一个对象,包含所有父组件传入的标签属性。注意:
$attrs会自动排除props中声明的属性(可以认为声明过的props被子组件自己“消费”了)
父组件:
1 | <template> |
子组件:
1 | <template> |
孙组件:
1 | <template> |
【$refs、$parent】
概述:
$refs用于 :父 → 子。$parent用于:子 → 父。
原理如下:
属性 说明 $refs值为对象,包含所有被 ref属性标识的DOM元素或组件实例。$parent值为对象,当前组件的父组件实例对象。
【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<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配置项接受数据。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18<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";
// 注入数据
let { money, updateMoney } = inject("moneyContext", {
money: 0,
updateMoney: (x: number) => {},
});
let car = inject("car");
</script>
【pinia】
参考之前pinia部分的讲解
【slot】
1. 默认插槽
默认插槽一般用在下面这种场景 即几个组件的基本格式相同 不同的只是里面的内容 使用插槽能够实现该组件样式的使用

实际上插槽解决的就是 当我们写组件的时候想直接在标签里面写内容是无法显示的 我们需要再该组件的源码中写下插槽 这样我们在组件标签内写的内容就会自动插入这个插槽所在的位置
所以其实插槽很像 routerview 那样的占位符 传递过来的数据内容会显示在那里
1 | 父组件中: |
2. 具名插槽
1 | 父组件中: |
具名插槽实际上就是给插槽加一个名字 让内容和插槽位置是一一对应的 可以实现乱序插入数据 但显示正确顺序
注意事项是 当我们在父组件组件标签中选择某个插槽时 v-slot 的选择必须放在 template 里面 所以我们给插入的数据都放在 template 里面
v-slot:xxx 可以简写为#xxx
3.作用域插槽
理解:数据在组件的自身,但根据数据生成的结构需要组件的使用者来决定。(新闻数据在
News组件中,但使用数据所遍历出来的结构由App组件决定) 所谓作用域的问题其实就是因为数据的调用是跨域的 所以我们用作用域插槽去实现 作用域插槽(Scoped Slots)是一种特殊的插槽机制,允许子组件向父组件传递数据,从而实现父子组件之间更灵活的数据交互。具体编码:
父组件中:
一 直接引入所有 slot 传递的配置项
1
2
3
4
5
6
7<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>二 通过解构赋值获得特定的数据
1
2
3
4
5
6
7<Game v-slot="{ games }">
<!-- <Game v-slot:default="params"> -->
<!-- <Game #default="params"> --> //这是两种别样的写法
<ul>
<li v-for="g in games" :key="g.id">{{ g.name }}</li>
</ul>
</Game>子组件中 通过 slot 传递数据
1 | <template> |
【shallowRef 与 shallowReactive 】
shallowRef
作用:创建一个响应式数据,但只对顶层属性进行响应式处理。 只有.value(.xxx)是响应式的 再多点一个就不行了
用法:
1
let myVar = shallowRef(initialValue);
特点:只跟踪引用值的变化,不关心值内部的属性变化。
shallowReactive
作用:创建一个浅层响应式对象,只会使对象的最顶层属性变成响应式的,对象内部的嵌套属性则不会变成响应式的 只有.xxx 是响应式的 再多点一个就不行了
用法:
1
const myObj = shallowReactive({ ... });
特点:对象的顶层属性是响应式的,但嵌套对象的属性不是。
总结
通过使用
shallowRef()和shallowReactive()来绕开深度响应。浅层式API创建的状态只在其顶层是响应式的,对所有深层的对象不会做任何处理,避免了对每一个内部属性做响应式所带来的性能成本,这使得属性的访问变得更快,可提升性能。
【readonly 与 shallowReadonly】
readonly
作用:用于创建一个对象的深只读副本。
用法:
1
2const original = reactive({ ... });
const readOnlyCopy = readonly(original);特点:
- 对象的所有嵌套属性都将变为只读。
- 任何尝试修改这个对象的操作都会被阻止(在开发模式下,还会在控制台中发出警告)。
应用场景:
- 创建不可变的状态快照。
- 保护全局状态或配置不被修改。
shallowReadonly
作用:与
readonly类似,但只作用于对象的顶层属性。用法:
1
2const original = reactive({ ... });
const shallowReadOnlyCopy = shallowReadonly(original);特点:
只将对象的顶层属性设置为只读,对象内部的嵌套属性仍然是可变的。
适用于只需保护对象顶层属性的场景。
【toRaw 与 markRaw】
toRaw
作用:用于获取一个响应式对象的原始对象,
toRaw返回的对象不再是响应式的,不会触发视图更新。官网描述:这是一个可以用于临时读取而不引起代理访问/跟踪开销,或是写入而不触发更改的特殊方法。不建议保存对原始对象的持久引用,请谨慎使用。
何时使用? —— 在需要将响应式对象传递给非
Vue的库或外部系统时,使用toRaw可以确保它们收到的是普通对象具体编码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21import { reactive, toRaw, markRaw, isReactive } from "vue";
/* toRaw */
// 响应式对象
let person = reactive({ name: "tony", age: 18 });
// 原始对象
let rawPerson = toRaw(person);
/* markRaw */
let citysd = markRaw([
{ id: "asdda01", name: "北京" },
{ id: "asdda02", name: "上海" },
{ id: "asdda03", name: "天津" },
{ id: "asdda04", name: "重庆" },
]);
// 根据原始对象citys去创建响应式对象citys2 —— 创建失败,因为citys被markRaw标记了
let citys2 = reactive(citys);
console.log(isReactive(person));
console.log(isReactive(rawPerson));
console.log(isReactive(citys));
console.log(isReactive(citys2));
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);
【customRef】
作用:创建一个自定义的ref,并对其依赖项跟踪和更新触发进行逻辑控制。
实现防抖效果(useSumRef.ts):
1 | import { customRef } from "vue"; |
组件中使用:
1 | let { msg } = useCustomRef("nnn", 2000); |
【Teleport】
- 什么是 Teleport?—— Teleport 是一种能够将我们的组件 html 结构移动到指定位置的技术。 注意 to 后的位置只能是我们 css 中能选择的部分
1 | <teleport to="body"> |
【Suspense】
- 等待异步组件时渲染一些额外内容,让应用有更好的用户体验
- 使用步骤:
- 异步引入组件
- 使用
Suspense包裹组件,并配置好default与fallback相当于一个插槽 有两个槽可以供你插入
1 | import { defineAsyncComponent, Suspense } from "vue"; |
1 | <template> |
【全局 API 转移到应用对象】
详情看官方文档
app.componentapp.configapp.directiveapp.mountapp.unmountapp.use
【其他】
过渡类名
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。……








