【v-】

注:这里只是简单列举出了这些指令 在后续笔记中 某些指令会有完整的介绍

Vue 提供了一系列以 v- 开头的指令(Directives),用于在模板中实现各种交互逻辑和渲染控制。以下是常用 v- 指令的系统总结:

一、核心渲染与数据绑定指令

1. v-text

  • 作用:设置元素的文本内容(替代 {{ }} 插值)
  • 特点:会覆盖元素原有的文本,不解析 HTML
1
2
<div v-text="message"></div>
<!-- 等价于 <div>{{ message }}</div> -->

2. v-html

  • 作用:设置元素的 HTML 内容(解析 HTML 标签)
  • 注意:有 XSS 风险,仅用于可信内容,不可用于用户输入
1
2
<div v-html="htmlContent"></div>
<!-- 若 htmlContent 是 "<strong>Hello</strong>",则渲染为粗体文本 -->

3. v-bind(简写 :

  • 作用:动态绑定 HTML 属性、组件 props、CSS 类、样式等
  • 用法v-bind:属性名="表达式":属性名="表达式"
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 绑定普通属性 -->
<img :src="imageUrl" :alt="imageDesc">

<!-- 绑定 class(对象/数组语法) -->
<div :class="{ active: isActive, 'text-danger': hasError }"></div>
<div :class="[baseClass, isActive ? 'active' : '']"></div>

<!-- 绑定 style(对象/数组语法) -->
<div :style="{ color: textColor, fontSize: '16px' }"></div>
<div :style="[baseStyle, customStyle]"></div>

<!-- 动态属性名 -->
<div :[dynamicAttr]="value"></div> <!-- dynamicAttr 是变量,决定属性名 -->

二、条件渲染指令

1. v-if

  • 作用:根据表达式真假,决定是否渲染元素(条件为 false 时不生成 DOM)
  • 搭配v-else-if(多条件判断)、v-else(兜底条件),三者需连续书写
1
2
3
<div v-if="score >= 90">优秀</div>
<div v-else-if="score >= 60">及格</div>
<div v-else>不及格</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
2
3
4
5
6
7
8
9
10
11
<!-- 遍历数组 -->
<ul>
<li v-for="(user, index) in users" :key="user.id">
{{ index + 1 }}. {{ user.name }}
</li>
</ul>

<!-- 遍历对象 -->
<div v-for="(value, key) in info" :key="key">
{{ key }}: {{ value }}
</div>

v-for 是 Vue 中用于列表渲染的核心指令,它可以基于源数据(数组、对象、字符串等)重复渲染元素或组件。以下是其详细用法和核心知识点:

一、基本语法(遍历数组)

最常用场景是遍历数组,语法为:
v-for="(item, index) in 数组"

  • item:数组中的当前元素(必填)
  • index:当前元素的索引(可选,从 0 开始)
  • in 可替换为 of(更符合 JavaScript 语法习惯)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<ul>
<!-- 仅使用元素 -->
<li v-for="user in users" :key="user.id">
{{ user.name }}
</li>

<!-- 同时使用元素和索引 -->
<li v-for="(user, index) of users" :key="index">
{{ index + 1 }}. {{ user.name }}
</li>
</ul>
</template>

<script setup>
const users = [
{ id: 1, name: "张三" },
{ id: 2, name: "李四" },
];
</script>

二、遍历对象

1
2
v-for` 也可遍历对象的键值对,语法为:
`v-for="(value, key, index) in 对象"
  • value:属性值(必填)
  • key:属性名(可选)
  • index:索引(可选,从 0 开始)
1
2
3
4
5
6
7
8
9
<template>
<div v-for="(value, key, index) in user" :key="key">
{{ index }}. {{ key }}: {{ value }}
</div>
</template>

<script setup>
const user = { name: "张三", age: 18, gender: "男" };
</script>

三、遍历字符串 / 数字

  • 遍历字符串:按字符拆分
  • 遍历数字:从 1 开始计数到该数字
1
2
3
4
5
<!-- 遍历字符串 -->
<div v-for="(char, index) in 'hello'" :key="index">{{ char }}</div>

<!-- 遍历数字(渲染 3 个元素) -->
<div v-for="num in 3" :key="num">{{ num }}</div>

四、必须的 key 属性

Vue 要求在 v-for 中为每个循环项绑定 :key,作用是:

  1. 标识唯一性:帮助 Vue 区分不同元素,提高列表更新时的渲染性能(避免不必要的 DOM 重新创建)。
  2. 避免状态混乱:确保组件 / 元素的状态(如表单输入值)在列表变化时正确保留。

注意

  • 优先使用唯一且稳定的标识(如数据的 id)作为 key,而非索引 index(索引会随数组排序 / 删除变化,可能导致性能问题)。
  • key 的值需为字符串或数字,且在当前循环中唯一。

所以在后续我们要对 v-for 的每个元素进行针对性的操作的时候 用的索引也是这个 key

五、注意事项

  1. **避免在 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>
  2. 遍历范围v-for 只能用于单个元素或 <template>,不能直接用于 <slot>v-text 等指令。

  3. 性能优化:对于大数据列表,可结合 v-memo 或虚拟滚动(如 vue-virtual-scroller)减少渲染压力。

四、事件处理指令

v-on(简写 @

  • 作用:绑定事件监听器(DOM 事件或自定义事件)
  • 用法v-on:事件名="处理函数"@事件名="处理函数"
  • 扩展:支持事件修饰符(如 .stop.prevent)、按键修饰符(如 .enter
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 基础用法 -->
<button @click="handleClick">点击我</button>

<!-- 传递参数 -->
<button @click="handleDelete(id)">删除</button>

<!-- 事件修饰符 -->
<form @submit.prevent="handleSubmit"> <!-- 阻止表单默认提交行为 -->
<input @keyup.enter="handleEnter"> <!-- 按 Enter 键触发 -->
<div @click.stop="handleDivClick"> <!-- 阻止事件冒泡 -->
<button @click="handleBtnClick">按钮</button>
</div>
</form>

五、表单输入绑定指令

v-model

  • 作用:在表单元素(input、select、textarea 等)上创建双向数据绑定
  • 特点:自动同步表单值与数据,适用于用户输入场景
  • 修饰符.trim(去除首尾空格)、.number(转为数字)、.lazy(失焦后同步)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- 文本输入 -->
<input v-model="username" placeholder="请输入用户名">

<!-- 复选框 -->
<input type="checkbox" v-model="isAgree"> 同意协议

<!-- 单选框 -->
<input type="radio" v-model="gender" value="male"> 男
<input type="radio" v-model="gender" value="female"> 女

<!-- 下拉选择 -->
<select v-model="city">
<option value="beijing">北京</option>
<option value="shanghai">上海</option>
</select>

<!-- 修饰符用法 -->
<input v-model.trim="username"> <!-- 自动去除首尾空格 -->
<input v-model.number="age" type="number"> <!-- 转为数字类型 -->

六、其他实用指令

1. v-pre

  • 作用:跳过元素及其子元素的编译过程(直接渲染原始内容,不解析插值和指令)
1
<div v-pre>{{ 这里的插值不会被解析,会原样显示 }}</div>

2. v-cloak

  • 作用:在 Vue 实例编译完成前隐藏元素,避免页面闪烁(配合 CSS 使用)
1
2
3
[v-cloak] {
display: none;
} /* 编译完成前隐藏 */
1
2
<div v-cloak>{{ message }}</div>
<!-- 编译完成后才显示内容 -->

3. v-once

  • 作用:元素和组件只渲染一次,后续数据变化不会重新渲染(优化静态内容性能)
1
2
<div v-once>{{ staticMessage }}</div>
<!-- 即使 staticMessage 变化,也不会重新渲染 -->

总结

Vue 的 v- 指令是模板语法的核心,覆盖了数据绑定、条件渲染、列表循环、事件处理、表单交互等常见场景。掌握这些指令的用法,能高效实现各种交互逻辑,其中:

  • v-bindv-on 是最基础的绑定指令(简写 :@ 需熟练使用);
  • v-if/v-showv-for 用于控制元素渲染;
  • v-model 是表单交互的核心;
  • 其他指令(v-prev-cloak 等)用于特定优化场景。

【响应式数据】

ref 能用来定义基本类型数据 和 对象类型数据(但底层上也是在 ref 中调用了 reactive)

reactive 用来定义对象类型数据

在模板中使用 ref 响应式数据的时候直接使用即可

1
2
3
4
5
6
7
8
<div>
{{ name }}
<div />

<script>
let name = ref("lisi");
</script>
</div>

但在 js 代码中需要对响应式数据进行操控的时候必须对 响应式数据 . value 进行操作

1
2
3
fuction changeName() {
name.value = "zhangsan"
}

注意同样地,当用 ref 定义对象类型的响应数据的时候一定不要忘了需要写 value

1
2
3
4
5
6
7
8
let games = [
{ id: "1", name: "zhang" },
{ id: "2", name: "san" },
];

function change() {
games.value[0].name = "li";
}

reactive 是深层次的 只要是被 reactive 包裹的对象 无论其中层级多深都会被改变

1
2
3
4
5
6
7
8
9
10
11
let moutain = reactive({
a: {
b{
c:666
}
}
})

function change() {
moutain.a.b.c = 999
}

宏观角度看:

  1. ref 用来定义:基本类型数据、对象类型数据;
  2. reactive 用来定义:对象类型数据。
  • 区别:
  1. ref 创建的变量必须使用 .value(可以使用 volar 插件自动添加 .value)。
  2. reactive 重新分配一个新对象,会失去响应式(可以使用 Object.assign 去整体替换)。

什么意思呢 即如果把原对象数据直接赋值成一个新的对象 则这个对象会失去响应式

解决这个问题的方法如下

1
2
3
4
5
let car = {brand:"benchi",price: 100}

fuction changeCar(){
Object.assign(car,{brand:'奥迪',price:100})
}

Object.assign

1
Object.assign(target, ...sources);

target:目标对象,也就是要将其他对象属性合并进去的对象,该对象会被修改。

...sources:一个或多个源对象,这些对象的可枚举自身属性(包括 Symbol 属性以外的可枚举属性)会被复制到目标对象。

​ 可以进行几个对象元素的合并 当源对象中有同名属性时,后面源对象的属性会覆盖前面源对象的同名属性。

Object.assign() 执行的是浅拷贝,也就是说,如果源对象的属性值是一个对象,那么复制的是该对象的引用,而不是创建一个新的对象。需要注意的是,由于 Object.assign() 是浅拷贝,对于包含嵌套对象的情况,如果需要深拷贝,就需要使用如 JSON.parse(JSON.stringify()) 或者第三方库(如 lodashcloneDeep 方法 )等其他方式来处理。

但是如果用的是 ref 包裹对象的话 就不会有这种烦恼 因为在改变的过程中 我们会直接写

1
2
3
function changeCar() {
car.value = { brand: "aodi", price: 100 };
}
  • 使用原则:
  1. 若需要一个基本类型的响应式数据,必须使用 ref。
  2. 若需要一个响应式对象,层级不深,ref、 reactive 都可以。
  3. 若需要一个响应式对象,且层级较深,推荐使用 reactive。

【toRefs 与 toRef】

  • 作用:将一个响应式对象中的每一个属性,转换为ref对象。
  • 备注:toRefstoRef功能一致,但toRefs可以批量转换。
  • 语法如下:
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
<template>
<div class="person">
<h2>姓名:{{ person.name }}</h2>
<h2>年龄:{{ person.age }}</h2>
<h2>性别:{{ person.gender }}</h2>
<button @click="changeName">修改名字</button>
<button @click="changeAge">修改年龄</button>
<button @click="changeGender">修改性别</button>
</div>
</template>

<script lang="ts" setup name="Person">
import { ref, reactive, toRefs, toRef } from "vue";

// 数据
let person = reactive({ name: "张三", age: 18, gender: "男" });

// 通过toRefs将person对象中的n个属性批量取出,且依然保持响应式的能力
let { name, gender } = toRefs(person);

// 通过toRef将person对象中的gender属性取出,且依然保持响应式的能力
let age = toRef(person, "age");

// 方法
function changeName() {
name.value += "~";
}
function changeAge() {
age.value += 1;
}
function changeGender() {
gender.value = "女";
}
</script>

【computed】

在 Vue 3 中,computed计算属性主要用于根据响应式数据(如refreactive、Pinia 状态等)派生新的响应式数据,它的核心作用是缓存计算结果并保持响应式依赖跟踪

在我们后面用 pinia 进行组件通信的时候是经常用到它这个特性的。

作用:根据已有数据计算出新数据(和Vue2中的computed作用一致)。

computed 的一大好处(特点)是 它当且仅当涉及到(依赖)的变量数据发生改变的时候才进行计算 即就算大量引用这个计算属性数据 只要依赖数据没发生改变 也只会进行一次计算(即计算属性是有缓存的)

注意在其中我们会写 get 和 set 方法 其中 get 方法用来返回值(即对读取值进行一些操作)

set 方法用来设置新值

看下面代码 fullName 是我们用计算属性 computed 来包裹的 它的数据类型是一个 Computed RefImpl 当我们对其 value 进行修改时 可以通过 set 方法拿到这个新设置的 value 并且对它进行操作

并且 像我们下面写的那个 changefullname 函数是当且仅当对 fullName 设置了 set 方法 让其不止可读的时候才能使用

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
<template>
<div class="person">
姓:<input type="text" v-model="firstName" /> <br />
名:<input type="text" v-model="lastName" /> <br />
全名:<span>{{ fullName }}</span> <br />
<button @click="changeFullName">全名改为:li-si</button>
</div>
</template>

<script setup lang="ts" name="App">
import { ref, computed } from "vue";

let firstName = ref("zhang");
let lastName = ref("san");

// 计算属性——只读取,不修改
/* let fullName = computed(()=>{
return firstName.value + '-' + lastName.value
}) */

// 计算属性——既读取又修改
let fullName = computed({
// 读取
get() {
return firstName.value + "-" + lastName.value;
},
// 修改
set(val) {
console.log("有人修改了fullName", val);
firstName.value = val.split("-")[0];
lastName.value = val.split("-")[1];
},
});

function changeFullName() {
fullName.value = "li-si";
}
</script>

【watch】

  • 作用:监视数据的变化(和Vue2中的watch作用一致)
  • 特点:Vue3中的watch只能监视以下四种数据
  1. ref定义的数据。注意:这里定义的数据是不用写.value 的 应该直接写这个数据本身
  2. reactive定义的数据。
  3. 函数返回一个值(getter函数)。
  4. 一个包含上述内容的数组。

watch 监视的基本语法

1
2
3
watch(被监视的目标数据, 回调函数);
//进一步地 一般回调函数的写法是
watch(sum, (newValue, oldValue) => {});

watch 有一个返回值 可以用来终止监视 基本的写法为

1
2
3
4
5
const stopWatch = watch(sum, (newValue, oldValue) => {
if (sum > 10) {
stopWatch();
}
});

我们在Vue3中使用watch的时候,通常会遇到以下几种情况:

* 情况一

监视ref定义的【基本类型】数据:直接写数据名即可,监视的是其value值的改变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<div class="person">
<h1>情况一:监视【ref】定义的【基本类型】数据</h1>
<h2>当前求和为:{{ sum }}</h2>
<button @click="changeSum">点我sum+1</button>
</div>
</template>

<script lang="ts" setup name="Person">
import { ref, watch } from "vue";
// 数据
let sum = ref(0);
// 方法
function changeSum() {
sum.value += 1;
}
// 监视,情况一:监视【ref】定义的【基本类型】数据
const stopWatch = watch(sum, (newValue, oldValue) => {
console.log("sum变化了", newValue, oldValue);
if (newValue >= 10) {
stopWatch();
}
});
</script>

* 情况二

监视ref定义的【对象类型】数据:直接写数据名,监视的是对象的【地址值】,若想监视对象内部的数据,要手动开启深度监视。

下面的这个注意可以类比为一个房子 改变属性相当于装修 不会改变 value(因为地址没有改变) 而改变整个对象才相当于换房子 会改变 value (地址改变了)

注意:

  • 若修改的是ref定义的对象中的属性,newValueoldValue 都是新值,因为它们是同一个对象。

  • 若修改整个ref定义的对象,newValue 是新值, oldValue 是旧值,因为不是同一个对象了。

  • 总结来说 只要对象地址没有改变 new 和 old 就是一个值 否则就是不同值

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
<template>
<div class="person">
<h1>情况二:监视【ref】定义的【对象类型】数据</h1>
<h2>姓名:{{ person.name }}</h2>
<h2>年龄:{{ person.age }}</h2>
<button @click="changeName">修改名字</button>
<button @click="changeAge">修改年龄</button>
<button @click="changePerson">修改整个人</button>
</div>
</template>

<script lang="ts" setup name="Person">
import { ref, watch } from "vue";
// 数据
let person = ref({
name: "张三",
age: 18,
});
// 方法
function changeName() {
person.value.name += "~";
}
function changeAge() {
person.value.age += 1;
}
function changePerson() {
person.value = { name: "李四", age: 90 };
}
/*
监视,情况一:监视【ref】定义的【对象类型】数据,监视的是对象的地址值,若想监视对象内部属性的变化,需要手动开启深度监视
watch的第一个参数是:被监视的数据
watch的第二个参数是:监视的回调
watch的第三个参数是:配置对象(deep、immediate等等.....)
*/
watch(
person,
(newValue, oldValue) => {
console.log("person变化了", newValue, oldValue);
},
{ deep: true }
);
</script>

* 情况三

监视reactive定义的【对象类型】数据,且默认开启了深度监视。

注意这里改变对象 new 和 old 也是一样的 因为我们之前说过 用 Object.assign 修改对象的时候 是把相同的元素值更改掉 并没有改变其地址值 只是改变了元素值 故而 new 和 old 是一样的

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
<template>
<div class="person">
<h1>情况三:监视【reactive】定义的【对象类型】数据</h1>
<h2>姓名:{{ person.name }}</h2>
<h2>年龄:{{ person.age }}</h2>
<button @click="changeName">修改名字</button>
<button @click="changeAge">修改年龄</button>
<button @click="changePerson">修改整个人</button>
<hr />
<h2>测试:{{ obj.a.b.c }}</h2>
<button @click="test">修改obj.a.b.c</button>
</div>
</template>

<script lang="ts" setup name="Person">
import { reactive, watch } from "vue";
// 数据
let person = reactive({
name: "张三",
age: 18,
});
let obj = reactive({
a: {
b: {
c: 666,
},
},
});
// 方法
function changeName() {
person.name += "~";
}
function changeAge() {
person.age += 1;
}
function changePerson() {
Object.assign(person, { name: "李四", age: 80 });
}
function test() {
obj.a.b.c = 888;
}

// 监视,情况三:监视【reactive】定义的【对象类型】数据,且默认是开启深度监视的
watch(person, (newValue, oldValue) => {
console.log("person变化了", newValue, oldValue);
});
watch(obj, (newValue, oldValue) => {
console.log("Obj变化了", newValue, oldValue);
});
</script>

* 情况四

监视refreactive定义的【对象类型】数据中的某个属性,注意点如下:

  1. 若该属性值不是【对象类型】,即基本类型,需要写成函数形式,直接写一个箭头函数即可。 为什么可以改成函数形式呢 其实就是我们之前所说的 watch 能够监视的其中一个类型 函数的返回值
  2. 若该属性值是依然是【对象类型】,可直接编,也可写成函数,建议写成函数。

结论:监视的要是对象里的属性,那么最好写函数式,注意点:若是对象监视的是地址值,需要关注对象内部,需要手动开启深度监视。

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
<template>
<div class="person">
<h1>情况四:监视【ref】或【reactive】定义的【对象类型】数据中的某个属性</h1>
<h2>姓名:{{ person.name }}</h2>
<h2>年龄:{{ person.age }}</h2>
<h2>汽车:{{ person.car.c1 }}、{{ person.car.c2 }}</h2>
<button @click="changeName">修改名字</button>
<button @click="changeAge">修改年龄</button>
<button @click="changeC1">修改第一台车</button>
<button @click="changeC2">修改第二台车</button>
<button @click="changeCar">修改整个车</button>
</div>
</template>

<script lang="ts" setup name="Person">
import { reactive, watch } from "vue";

// 数据
let person = reactive({
name: "张三",
age: 18,
car: {
c1: "奔驰",
c2: "宝马",
},
});
// 方法
function changeName() {
person.name += "~";
}
function changeAge() {
person.age += 1;
}
function changeC1() {
person.car.c1 = "奥迪";
}
function changeC2() {
person.car.c2 = "大众";
}
function changeCar() {
person.car = { c1: "雅迪", c2: "爱玛" };
}

// 监视,情况四:监视响应式对象中的某个属性,且该属性是基本类型的,要写成函数式
/* watch(()=> person.name,(newValue,oldValue)=>{
console.log('person.name变化了',newValue,oldValue)
}) */

// 监视,情况四:监视响应式对象中的某个属性,且该属性是对象类型的,可以直接写,也能写函数,更推荐写函数
watch(
() => person.car,
(newValue, oldValue) => {
console.log("person.car变化了", newValue, oldValue);
},
{ deep: true }
);
</script>

监视 对象类型的元素 最佳实践

1
2
3
4
// 监视,情况四:监视响应式对象中的某个属性,且该属性是对象类型的,可以直接写,也能写函数,更推荐写函数
watch(()=>person.car,(newValue,oldValue)=>{
console.log('person.car变化了',newValue,oldValue)
},{deep:true})

* 情况五

监视上述的多个数据

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>情况五:监视上述的多个数据</h1>
<h2>姓名:{{ person.name }}</h2>
<h2>年龄:{{ person.age }}</h2>
<h2>汽车:{{ person.car.c1 }}、{{ person.car.c2 }}</h2>
<button @click="changeName">修改名字</button>
<button @click="changeAge">修改年龄</button>
<button @click="changeC1">修改第一台车</button>
<button @click="changeC2">修改第二台车</button>
<button @click="changeCar">修改整个车</button>
</div>
</template>

<script lang="ts" setup name="Person">
import {reactive,watch} from 'vue'

// 数据
let person = reactive({
name:'张三',
age:18,
car:{
c1:'奔驰',
c2:'宝马'
}
})
// 方法
function changeName(){
person.name += '~'
}
function changeAge(){
person.age += 1
}
function changeC1(){
person.car.c1 = '奥迪'
}
function changeC2(){
person.car.c2 = '大众'
}
function changeCar(){
person.car = {c1:'雅迪',c2:'爱玛'}
}

// 监视,情况五:监视上述的多个数据
watch([()=>person.name,person.car],(newValue,oldValue)=>{
console.log('person.car变化了',n ewValue,oldValue)
},{deep:true})
</script>

【watchEffect】

  • 官网:立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行该函数。

  • watch对比watchEffect

    1. 都能监听响应式数据的变化,不同的是监听数据变化的方式不同

    2. watch:要明确指出监视的数据

    3. 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
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
<template>
<div class="person">
<h1 ref="title1">尚硅谷</h1>
<h2 ref="title2">前端</h2>
<h3 ref="title3">Vue</h3>
<input type="text" ref="inpt" /> <br /><br />
<button @click="showLog">点我打印内容</button>
</div>
</template>

<script lang="ts" setup name="Person">
import { ref } from "vue";

let title1 = ref();
let title2 = ref();
let title3 = ref();

function showLog() {
// 通过id获取元素
const t1 = document.getElementById("title1");
// 打印内容
console.log((t1 as HTMLElement).innerText);
console.log((<HTMLElement>t1).innerText);
console.log(t1?.innerText);

/************************************/

// 通过ref获取元素
console.log(title1.value);
console.log(title2.value);
console.log(title3.value);
}
</script>

用在组件标签上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 父组件App.vue -->
<template>
<Person ref="ren" />
<button @click="test">测试</button>
</template>

<script lang="ts" setup name="App">
import Person from "./components/Person.vue";
import { ref } from "vue";

let ren = ref();

function test() {
console.log(ren.value.name);
console.log(ren.value.age);
}
</script>

总结来说 当我们将 ref 标记在组件上后 就能获得这个组件实例

但问题是这个组件实例中的元素内容并不是开放的 我们需要自己去定义我们在这个组件中需要什么

1
2
3
4
5
6
7
8
9
10
11
<!-- 子组件Person.vue中要使用defineExpose暴露内容 -->
<script lang="ts" setup name="Person">
import { ref, defineExpose } from "vue";
// 数据
let name = ref("张三");
let age = ref(18);
/****************************/
/****************************/
// 使用defineExpose将组件中的数据交给外部
defineExpose({ name, age });
</script>

当我们进行完这个操作后就可以通过 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 代码,在组件渲染时动态计算值,并且会追踪表达式中依赖的响应式数据(如refreactive对象),当依赖变化时自动重新计算并更新属性值。

简单来说就是 带冒号:的属性会去动态读取冒号后面的内容

动态属性(v-bind

(1)绑定基本表达式

可以直接使用 JavaScript 表达式(运算、三元判断等):

1
2
3
4
5
6
7
8
<template>
<!-- 运算 -->
<div :a="10 + 20"></div>
<!-- 结果:a="30" -->

<!-- 三元表达式 -->
<div :class="isActive ? 'active' : 'inactive'"></div>
</template>

(2)绑定响应式数据

结合 Vue 的响应式数据(ref/reactive),实现动态更新:

1
2
3
4
5
6
7
8
9
<template>
<div :title="message"></div>
<!-- 当message变化时,title自动更新 -->
</template>

<script setup lang="ts">
import { ref } from "vue";
const message = ref(" Hello, Vue3"); // 响应式数据
</script>

(3)绑定对象 / 数组(用于 class/style)

动态绑定classstyle时,可直接传入对象或数组:

1
2
3
4
5
6
7
<template>
<!-- 绑定class对象 -->
<div :class="{ active: isActive, 'text-danger': hasError }"></div>

<!-- 绑定style数组 -->
<div :style="[baseStyles, activeStyles]"></div>
</template>

(4)绑定属性名(动态属性名)

属性名也可以通过表达式动态生成(使用[]包裹):

1
2
3
4
5
6
7
8
9
10
11
<template>
<div :[attrName]="value"></div>
<!-- attrName是变量,动态决定属性名 -->
</template>

<script setup lang="ts">
import { ref } from "vue";
const attrName = ref("data-id"); // 属性名动态为"data-id"
const value = ref("123"); // 属性值为"123"
// 渲染结果:<div data-id="123"></div>
</script>

(5)绑定布尔属性

对于布尔属性(如disabledchecked),值为false时会移除该属性:

1
2
3
4
5
<template>
<button :disabled="isDisabled">按钮</button>
<!-- isDisabled为true时:<button disabled>按钮</button> -->
<!-- isDisabled为false时:<button>按钮</button>(无disabled属性) -->
</template>

注意事项

  • 表达式限制v-bind后的表达式只能是单个表达式(不能是语句或流程控制,如iffor)。
    错误示例:<div :a="if (flag) { 1 } else { 2 }"></div>(应改用三元表达式)。
  • 避免副作用:表达式中不应包含副作用操作(如++--或函数调用修改数据),可能导致渲染异常。
  • 简写规则:所有 HTML 属性都可以用:简写,包括自定义属性(如data-*)。

总结

  • a="xxx":静态字符串,直接渲染,不参与动态计算。
  • :a="xxx":动态绑定,xxx作为 JavaScript 表达式计算,支持响应式数据,会随依赖更新。

【props】

为什么要使用 props 呢

在 Vue 组件化开发中,props是实现组件间通信的核心机制之一,主要用于父组件向子组件传递数据,它的作用和必要性可以从以下几个方面理解:

  1. 实现组件间的数据传递

组件化开发的核心思想是 “拆分复杂 UI 为独立可复用的组件”,而组件之间往往需要共享数据。props就是 Vue 为这种场景设计的标准数据传递通道:

  • 父组件通过props向子组件传递数据(如示例中App.vuePerson.vue传递persons数组)。
  • 子组件通过声明props接收数据,并在自身模板或逻辑中使用。

没有props,子组件无法直接获取父组件的数据,组件间就会变成孤立的 “信息孤岛”。

  1. 保证组件的独立性和复用性

props的设计遵循了 “单向数据流” 原则:父组件传递数据,子组件只能使用数据,不能直接修改(若需修改,需通过事件通知父组件)。这种机制带来两个好处:

  • 组件独立性:子组件的行为只依赖于接收的props,不直接操作父组件数据,避免组件间耦合过深。
  • 高复用性:同一个子组件(如Person.vue)可以通过接收不同的props数据,在不同场景下渲染不同内容(例如既可以展示 “张三、李四”,也可以展示 “小明、小红”)。
  1. 提供数据校验和类型约束

在 Vue 中,props支持对接收的数据进行校验(尤其是结合 TypeScript 时):

  • 可以限制数据类型(如示例中list必须是Persons类型的数组)。
  • 可以设置默认值(如示例中withDefaults指定的默认数组)。
  • 可以指定是否为必填项(通过?标识可选)。

这些校验能在开发阶段就暴露数据传递的错误(如父组件传了一个字符串而非数组),减少运行时 bug。

  1. 明确组件的接口规范

props相当于子组件对外暴露的 “数据接口”,其他开发者使用该组件时,只需查看props定义就知道:

  • 这个组件需要哪些数据才能工作?
  • 每个数据的格式、类型是什么?
  • 哪些数据是可选的(有默认值)?

例如看到Person.vueprops定义,就知道它需要一个list数组,数组元素必须包含idnameage,从而快速理解如何使用该组件。

总结

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的生命周期

    创建阶段:beforeCreatecreated

    挂载阶段:beforeMountmounted

    更新阶段:beforeUpdateupdated

    销毁阶段:beforeDestroydestroyed

  • Vue3的生命周期

    创建阶段:setup

    挂载阶段:onBeforeMountonMounted

    更新阶段:onBeforeUpdateonUpdated

    卸载阶段:onBeforeUnmountonUnmounted

  • 常用的钩子: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
    18
    import { 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
    28
    import {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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { createRouter, createWebHistory } from "vue-router"; // 引入
import Home from "@/pages/Home.vue";
import News from "@/pages/News.vue";
import About from "@/pages/About.vue";

const router = createRouter({
// 创建路由器
history: createWebHistory(), // 路由器工作模式(后面会写)
routes: [
// 一组route路由
{
path: "/home", // 路由路径 (会显示在地址后缀以及routerlink)
component: Home, // 一个route必须对应一个组件
},
{
path: "/about",
component: About,
},
],
});
export default router; // router路由器必须被暴露出去
  • main.ts代码如下:

    1
    2
    3
    4
    import 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.路由组件通常存放在pagesviews文件夹,一般组件通常存放在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.通过点击导航,视觉效果上“消失” 了的路由组件,默认是被卸载掉的,需要的时候再去挂载

【路由器工作模式】

  1. history模式

    优点:URL更加美观,不带有#,更接近传统的网站URL

    缺点:后期项目上线,需要服务端配合处理路径问题,否则刷新会有404错误。

    1
    2
    3
    4
    const router = createRouter({
    history: createWebHistory(), //history模式
    /******/
    });
  2. hash模式

    优点:兼容性更好,因为不需要服务器端处理路径。

    缺点:URL带有#不太美观,且在SEO优化方面相对较差。

    1
    2
    3
    4
    const router = createRouter({
    history: createWebHashHistory(), //hash模式
    /******/
    });

【to 的两种写法】

1
2
3
4
5
<!-- 第一种:to的字符串写法 -->
<router-link active-class="active" to="/home">主页</router-link>

<!-- 第二种:to的对象写法 -->
<router-link active-class="active" :to="{ path: '/home' }">Home</router-link>

【命名路由】

作用:可以简化路由跳转及传参(后面就讲)

注意这里的 name 不一定要和 path 或者 component 的名字一样 这个 name 是用来自定义的 主要是便于我们后期的路由跳转问题

给路由规则命名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
routes: [
{
name: "zhuye",
path: "/home",
component: Home,
},
{
name: "xinwen",
path: "/news",
component: News,
},
{
name: "guanyu",
path: "/about",
component: About,
},
];

跳转路由:

1
2
3
4
5
<!--简化前:需要写完整的路径(to的字符串写法) -->
<router-link to="/news/detail">跳转</router-link>

<!--简化后:直接通过名字跳转(to的对象写法配合name属性) 注意这种写法是to的对象写法 -->
<router-link :to="{ name: 'guanyu' }">跳转</router-link>

【嵌套路由】

  1. 编写News的子路由:Detail.vue

  2. 配置路由规则,使用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
    28
    const 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;
  3. 跳转路由(记得要加完整路径):(注意这里对象跳转的话使用名称也是可以实现的)

    1
    2
    3
    <router-link to="/news/detail">xxxx</router-link>
    <!-- 或 -->
    <router-link :to="{ path: '/news/detail' }">xxxx</router-link>
  4. 记得去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. 传递参数

    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里面写好这些数据
    这里就省去了(实际开发中是后端给的)
  2. 接收参数:

    我们用 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 是一致的 这里就不再写了 主要是写法不一样

  1. 定义路由

    在我们使用 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;
  2. 传递参数

    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>
  3. 接收参数:

    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
2
3
4
5
6
{
name:'xiang',
path:'detail/:id/:title/:content',
component:Detail,
props:true
}

相当于我们把 Detail 组件变成了一个一般组件 然后我们把 path 中占位的 id title content(即收到的 params 参数)写在一般组件中作为一般组件的 props

1
//等价于 <Detail id="??" title="??" content="??" />

那么对应的我们需要在接受参数的地方定义一下我们需要的 props

1
2
3
4
5
6
7
8
9
10
11
<template>
<ul>
<li>编号:{{ id }}</li>
<li>标题:{{ title }}</li>
<li>内容:{{ content }}</li>
</ul>
</template>

<script setup lang="ts">
defineProps(["id", "title", "content"]);
</script>

显然这样我们就做了极大的简化

这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
name:'xiang',
path:'detail/:id/:title/:content',
component:Detail,

// props的对象写法,作用:把对象中的每一组key-value作为props传给Detail组件 这种props是写死的 很少使用
// props:{a:1,b:2,c:3},

// props的布尔值写法,作用:把收到了每一组params参数,作为props传给Detail组件 注意这种写法只能传递params参数(提前占位)
// props:true

// props的函数写法,作用:把返回的对象中每一组key-value作为props传给Detail组件 可以用于传递query参数
props(route){
return route.query
}
}

【 replace 属性】

  1. 作用:控制路由跳转时操作浏览器历史记录的模式。

  2. 浏览器的历史记录有两种写入方式:分别为pushreplace

    • push是追加历史记录(默认值)。 (就是这些历史记录 or 页面都是放在栈里的 可以前进后退)
    • replace是替换当前记录 。 (直接替换掉当前内容 不支持前进后退)
  3. 开启replace模式:

    1
    <RouterLink replace .......>News</RouterLink>

【编程式导航】

什么是编程式导航呢 简单来说就是 脱离 通过 JavaScript 代码实现路由跳转

什么时候会用编程式导航呢 一般地,当路由跳转需要逻辑(如完成某些操作才跳转路由)的时候我们需要使用编程式导航

Vue3 中实现编程式导航主要依赖 vue-router 提供的 useRouter 组合式 API,以下是其核心用法:

基本用法

首先需要导入并创建路由实例:

1
2
3
import { useRouter } from "vue-router";

const router = useRouter();

我们会使用

1
2
3
4
5
6
//导航到特定地址
router.push({ 地址 });
router.replace({ 地址 });

//在历史记录中前进或后退指定步数
rouer.go(1); // 前进一页

这里的 push 和 replace 就对应着上面介绍的两种浏览器历史记录写入方式

push 或者 replace 后面地址的写法 跟 routerlink 中 to 参数的写法一模一样

【重定向】

  1. 作用:将特定的路径,重新定向到已有路由。

  2. 具体编码:

    1
    2
    3
    4
    {
    path:'/',
    redirect:'/about'
    }

可以用来处理页面初始地址的问题

【pinia】

pinia 用于集中式状态(数据)管理

我们需要把一些需要共享的数据交给集中式状态管理

【搭建 pinia 环境】

第一步:npm install pinia

第二步:操作src/main.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { createApp } from "vue";
import App from "./App.vue";

/* 引入createPinia,用于创建pinia */
import { createPinia } from "pinia";

/* 创建pinia */
const pinia = createPinia();
const app = createApp(App);

/* 使用插件 */ {
}
app.use(pinia);
app.mount("#app");

此时开发者工具中已经有了pinia选项

【存储+读取数据】

  1. Store是一个保存:状态业务逻辑 的实体,每个组件都可以读取写入它。 其实它就相当于 pinia 的具体实现部分 我们需要在 src 目录下创建一个 store 文件夹用于存放各个组件的 pinia 从另一个角度说 由于我们把这些数据放在了 store 里 那么我们就可以在其他组件中复用这些数据 这也就是我们所说的存放共享数据

  2. 它有三个概念:stategetteraction,相当于组件中的: datacomputedmethods

  3. 具体编码:src/store/count.ts

    1
    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: {},
    });
  4. 具体编码:src/store/talk.ts

    1
    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: {},
    });
  5. 组件中使用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
    12
    let 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. 第一种修改方式,直接修改

    1
    countStore.sum = 666;
  2. 第二种修改方式:批量修改

    1
    2
    3
    4
    countStore.$patch({
    sum: 999,
    school: "atguigu",
    });
  3. 第三种修改方式:借助action修改(action中可以编写一些业务逻辑)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    import { 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;
    }
    },
    },
    /*************/
    });
  4. 组件中调用action即可

    1
    2
    3
    4
    5
    // 使用countStore
    const countStore = useCountStore();

    // 调用对应action
    countStore.incrementOdd(n.value);

【storeToRefs】

  • 借助storeToRefsstore中的数据转为ref对象,方便在模板中使用。
  • 注意:pinia提供的storeToRefs只会将数据做转换,而VuetoRefs会将store中所有东西都转换成 ref(显然是我们不想看到的)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div class="count">
<h2>当前求和为:{{ sum }}</h2>
</div>
</template>

<script setup lang="ts" name="Count">
import { useCountStore } from "@/store/count";
/* 引入storeToRefs */
import { storeToRefs } from "pinia";

/* 得到countStore */
const countStore = useCountStore();
/* 使用storeToRefs转换countStore,随后解构 */
const { sum } = storeToRefs(countStore);
</script>

【getters】

  1. 概念:当state中的数据,需要经过处理后再使用时,可以使用getters配置。

  2. 追加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();
    },
    },
    });
  3. 组件中读取数据:

    1
    2
    const { increment, decrement } = countStore;
    let { sum, school, bigSum, upperSchool } = storeToRefs(countStore);

【$subscribe 订阅】

通过 store 的 $subscribe() 方法侦听 state 及其变化

1
2
3
4
talkStore.$subscribe((mutate, state) => {
console.log("LoveTalk", mutate, state);
localStorage.setItem("talk", JSON.stringify(talkList.value)); //首先需要转化成字符串才能正常显示
});

补充一小点 上面我们把数据储存到浏览器本地储存 这样就可以一直保留这个数据 并且对于数据的共享更加灵活 当然对应的 在我们的 store 里也应该对应做出一些改变

1
2
3
4
5
state(){
return {
talkList:JSON.parse(localStorage.getItem('talkList') as string) || [] // 这里我们把字符串反序列化成对象 as是typescript中的断言 用于去掉警告 并且我们放一个空数组作为选项
}
},

这样我们就能拿到储存在浏览器本地的数据了

【store 组合式写法】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { defineStore } from "pinia";
import axios from "axios";
import { nanoid } from "nanoid";
import { reactive } from "vue";

export const useTalkStore = defineStore("talk", () => {
// talkList就是state
const talkList = reactive(
JSON.parse(localStorage.getItem("talkList") as string) || []
);

// getATalk函数相当于action
async function getATalk() {
// 发请求,下面这行的写法是:连续解构赋值+重命名
let {
data: { content: title },
} = await axios.get("https://api.uomg.com/api/rand.qinghua?format=json");
// 把请求回来的字符串,包装成一个对象
let obj = { id: nanoid(), title };
// 放到数组中
talkList.unshift(obj);
}
return { talkList, getATalk };
});

【组件通信】

【props】

概述:props是使用频率最高的一种通信方式,常用与 :父 ↔ 子

  • 父传子:属性值是非函数
  • 子传父:属性值是函数

父组件:

父组件传递 Props

父组件通过属性绑定的方式向子组件传递数据,与 HTML 属性写法类似。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div class="father">
<h3>父组件,</h3>
<h4>我的车:{{ car }}</h4>
<h4>儿子给的玩具:{{ toy }}</h4>
<Child :car="car" :getToy="getToy" />
</div>
</template>

<script setup lang="ts" name="Father">
import Child from "./Child.vue";
import { ref } from "vue";
// 数据
const car = ref("奔驰");
const toy = ref();
// 方法
function getToy(value: string) {
toy.value = value;
}
</script>

子组件

子组件定义与接收 Props

在 Vue 3 中,子组件通过 defineProps() 宏来声明接收的 props(无需导入,Vue 会自动处理)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div class="child">
<h3>子组件</h3>
<h4>我的玩具:{{ toy }}</h4>
<h4>父给我的车:{{ car }}</h4>
<button @click="getToy(toy)">玩具给父亲</button>
</div>
</template>

<script setup lang="ts" name="Child">
import { ref } from "vue";
const toy = ref("奥特曼");

defineProps(["car", "getToy"]);
</script>

我们简单在这里写一点东西来解释一下上面的父传子和子传父 我们说父传子传非函数 如父组件中的 car 我们只需要暴露出来此参数 然后直接用插值语法 子传父传函数 但我们实际上需要在父组件中写一个函数来接受这个传递的数据 这也就是我们为什么说子传父传函数 并且这个函数当我们 defineprops 后是可以直接使用的

【自定义事件】

在 Vue 3 中,自定义事件是子组件向父组件传递数据或触发父组件逻辑的重要方式,与 props(父传子)形成互补,共同实现组件间的双向通信。

基本概念

自定义事件允许子组件主动触发一个事件,并传递数据给父组件,父组件通过监听该事件来执行相应的处理逻辑。

基本用法

  1. 子组件触发自定义事件

子组件通过 defineEmits() 宏声明可以触发的事件(无需导入),然后使用 emit 函数触发事件并传递数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- 子组件 Child.vue -->
<template>
<button @click="handleClick">点击传递数据</button>
</template>

<script setup>
// 声明可以触发的事件(数组形式,指定事件名称)
const emit = defineEmits(["submit", "update"]);

const handleClick = () => {
// 触发事件,并传递数据(可传多个参数)
emit("submit", "这是子组件传递的数据", 123);
};
</script>
  1. 父组件监听自定义事件

父组件通过 v-on(简写 @)监听子组件触发的事件,并定义处理函数接收数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- 父组件 Parent.vue -->
<template>
<div>
<!-- 监听子组件的 submit 事件 -->
<Child @submit="handleSubmit" />
</div>
</template>

<script setup>
import Child from "./Child.vue";

// 处理子组件触发的事件,接收传递的数据
const handleSubmit = (msg, num) => {
console.log("收到子组件数据:", msg, num); // 输出:收到子组件数据:这是子组件传递的数据 123
};
</script>
  1. 概述:自定义事件常用于:子 => 父。
  2. 注意区分好:原生事件、自定义事件。
  • 原生事件:
    • 事件名是特定的(clickmosueenter等等)
    • 事件对象$event: 是包含事件相关信息的对象(pageXpageYtargetkeyCode
  • 自定义事件:
    • 事件名是任意名称
    • 事件对象$event: 是调用emit时所提供的数据,可以是任意类型!!!
  1. 示例:

    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
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
// 引入mitt
import mitt from "mitt";

// 创建emitter
const emitter = mitt();

/*
// 绑定事件
emitter.on('abc',(value)=>{
console.log('abc事件被触发',value)
})
emitter.on('xyz',(value)=>{
console.log('xyz事件被触发',value)
})

setInterval(() => {
// 触发事件
emitter.emit('abc',666)
emitter.emit('xyz',777)
}, 1000);

setTimeout(() => {
// 清理事件
emitter.all.clear()
}, 3000);
*/

// 创建并暴露mitt
export default emitter;

接收数据的组件中:绑定事件、同时在销毁前解绑事件:

1
2
3
4
5
6
7
8
9
10
11
12
import emitter from "@/utils/emitter";
import { onUnmounted } from "vue";

// 绑定事件
emitter.on("send-toy", (value) => {
console.log("send-toy事件被触发", value);
});

onUnmounted(() => {
// 解绑事件
emitter.off("send-toy");
});

【第三步】:提供数据的组件,在合适的时候触发事件

1
2
3
4
5
6
import emitter from "@/utils/emitter";

function sendToy() {
// 触发事件
emitter.emit("send-toy", toy.value);
}

注意这个重要的内置关系,总线依赖着这个内置关系

【v-model】

v-model 双向绑定 所谓双向就是 能够实现从数据到页面 再从页面到数据 同时既能子传父又能父传子

  1. 概述:实现 父 ↔ 子 之间相互通信。

  2. 前序知识 —— 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"
    >
  3. 组件标签上的v-model的本质::moldeValueupdate: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>
  4. 也可以更换value,例如改成abc

    1
    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>
  5. 如果value可以更换,那么就可以在组件标签上多次使用v-model

    1
    <AtguiguInput v-model:abc="userName" v-model:xyz="password" />
  6. 这里我们简单辨析一下上述代码中写到过的$event

    $event 到底是啥 啥时候能.target 呢?

    对于原生事件 $event 就是事件对象 所以可以使用.target

    对于自定义事件,$event就是触发事件时所传递的数据 (即上面写的$event.target.value) 所以不能.target

【$attrs 】

  1. 概述:$attrs用于实现当前组件的父组件,向当前组件的子组件通信(祖 → 孙)。

  2. 具体说明:$attrs是一个对象,包含所有父组件传入的标签属性。

    注意:$attrs会自动排除props中声明的属性(可以认为声明过的 props 被子组件自己“消费”了)

父组件:

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="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) {
a.value = value;
}
</script>

子组件:

1
2
3
4
5
6
7
8
9
10
<template>
<div class="child">
<h3>子组件</h3>
<GrandChild v-bind="$attrs" />
</div>
</template>

<script setup lang="ts" name="Child">
import GrandChild from "./GrandChild.vue";
</script>

孙组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<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>

【$refs、$parent】

  1. 概述:

    • $refs用于 :父 → 子。
    • $parent用于:子 → 父。
  2. 原理如下:

    属性 说明
    $refs 值为对象,包含所有被ref属性标识的DOM元素或组件实例。
    $parent 值为对象,当前组件的父组件实例对象。

【provide、inject】

  1. 概述:实现祖孙组件直接通信

  2. 具体使用:

    • 在祖先组件中通过provide配置向后代组件提供数据
    • 在后代组件中通过inject配置来声明接收数据
  3. 具体编码:

    【第一步】父组件中,使用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. 默认插槽

默认插槽一般用在下面这种场景 即几个组件的基本格式相同 不同的只是里面的内容 使用插槽能够实现该组件样式的使用

img

实际上插槽解决的就是 当我们写组件的时候想直接在标签里面写内容是无法显示的 我们需要再该组件的源码中写下插槽 这样我们在组件标签内写的内容就会自动插入这个插槽所在的位置

所以其实插槽很像 routerview 那样的占位符 传递过来的数据内容会显示在那里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
父组件中:
<Category title="今日热门游戏">
<ul>
<li v-for="g in games" :key="g.id">{{ g.name }}</li>
</ul>
</Category>
子组件中:
<template>
<div class="item">
<h3>{{ title }}</h3>
<!-- 默认插槽 -->
<slot></slot>
</div>
</template>

2. 具名插槽

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
父组件中:
<Category title="今日热门游戏">
<template v-slot:s1>
<ul>
<li v-for="g in games" :key="g.id">{{ g.name }}</li>
</ul>
</template>
<template #s2>
<a href="">更多</a>
</template>
</Category>
子组件中:
<template>
<div class="item">
<h3>{{ title }}</h3>
<slot name="s1"></slot>
<slot name="s2"></slot>
</div>
</template>

具名插槽实际上就是给插槽加一个名字 让内容和插槽位置是一一对应的 可以实现乱序插入数据 但显示正确顺序

注意事项是 当我们在父组件组件标签中选择某个插槽时 v-slot 的选择必须放在 template 里面 所以我们给插入的数据都放在 template 里面

v-slot:xxx 可以简写为#xxx

3.作用域插槽

  1. 理解:数据在组件的自身,但根据数据生成的结构需要组件的使用者来决定。(新闻数据在News组件中,但使用数据所遍历出来的结构由App组件决定) 所谓作用域的问题其实就是因为数据的调用是跨域的 所以我们用作用域插槽去实现 作用域插槽(Scoped Slots)是一种特殊的插槽机制,允许子组件向父组件传递数据,从而实现父子组件之间更灵活的数据交互。

  2. 具体编码:

    父组件中:

    一 直接引入所有 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div class="category">
<h2>今日游戏榜单</h2>
<slot :games="games" a="哈哈"></slot> // 这里可以传入多个props
都会放进params中 (:games这个名字games是任取的)
</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>

【shallowRef 与 shallowReactive 】

shallowRef

  1. 作用:创建一个响应式数据,但只对顶层属性进行响应式处理。 只有.value(.xxx)是响应式的 再多点一个就不行了

  2. 用法:

    1
    let myVar = shallowRef(initialValue);
  3. 特点:只跟踪引用值的变化,不关心值内部的属性变化。

shallowReactive

  1. 作用:创建一个浅层响应式对象,只会使对象的最顶层属性变成响应式的,对象内部的嵌套属性则不会变成响应式的 只有.xxx 是响应式的 再多点一个就不行了

  2. 用法:

    1
    const myObj = shallowReactive({ ... });
  3. 特点:对象的顶层属性是响应式的,但嵌套对象的属性不是。

总结

通过使用 shallowRef()shallowReactive() 来绕开深度响应。浅层式 API 创建的状态只在其顶层是响应式的,对所有深层的对象不会做任何处理,避免了对每一个内部属性做响应式所带来的性能成本,这使得属性的访问变得更快,可提升性能。

【readonly 与 shallowReadonly】

readonly

  1. 作用:用于创建一个对象的深只读副本。

  2. 用法:

    1
    2
    const original = reactive({ ... });
    const readOnlyCopy = readonly(original);
  3. 特点:

    • 对象的所有嵌套属性都将变为只读。
    • 任何尝试修改这个对象的操作都会被阻止(在开发模式下,还会在控制台中发出警告)。
  4. 应用场景:

    • 创建不可变的状态快照。
    • 保护全局状态或配置不被修改。

shallowReadonly

  1. 作用:与 readonly 类似,但只作用于对象的顶层属性。

  2. 用法:

    1
    2
    const original = reactive({ ... });
    const shallowReadOnlyCopy = shallowReadonly(original);
  3. 特点:

    • 只将对象的顶层属性设置为只读,对象内部的嵌套属性仍然是可变的。

    • 适用于只需保护对象顶层属性的场景。

【toRaw 与 markRaw】

toRaw

  1. 作用:用于获取一个响应式对象的原始对象, toRaw 返回的对象不再是响应式的,不会触发视图更新。

    官网描述:这是一个可以用于临时读取而不引起代理访问/跟踪开销,或是写入而不触发更改的特殊方法。不建议保存对原始对象的持久引用,请谨慎使用。

    何时使用? —— 在需要将响应式对象传递给非 Vue 的库或外部系统时,使用 toRaw 可以确保它们收到的是普通对象

  2. 具体编码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    import { 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

  1. 作用:标记一个对象,使其永远不会变成响应式的。

    例如使用mockjs时,为了防止误把mockjs变为响应式对象,可以使用 markRaw 去标记mockjs

  2. 编码:

    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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 };
}

组件中使用:

1
let { msg } = useCustomRef("nnn", 2000);

【Teleport】

  • 什么是 Teleport?—— Teleport 是一种能够将我们的组件 html 结构移动到指定位置的技术。 注意 to 后的位置只能是我们 css 中能选择的部分
1
2
3
4
5
6
7
<teleport to="body">
<div class="modal" v-show="isShow">
<h2>我是一个弹窗</h2>
<p>我是弹窗中的一些内容</p>
<button @click="isShow = false">关闭弹窗</button>
</div>
</teleport>

【Suspense】

  • 等待异步组件时渲染一些额外内容,让应用有更好的用户体验
  • 使用步骤:
    • 异步引入组件
    • 使用Suspense包裹组件,并配置好defaultfallback 相当于一个插槽 有两个槽可以供你插入
1
2
import { defineAsyncComponent, Suspense } from "vue";
const Child = defineAsyncComponent(() => import("./Child.vue"));
1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<div class="app">
<h3>我是App组件</h3>
<Suspense>
<template v-slot:default>
<Child />
</template>
<template v-slot:fallback>
<h3>加载中.......</h3>
</template>
</Suspense>
</div>
</template>

【全局 API 转移到应用对象】

详情看官方文档

  • app.component
  • app.config
  • app.directive
  • app.mount
  • app.unmount
  • app.use

【其他】

  • 过渡类名 v-enter 修改为 v-enter-from、过渡类名 v-leave 修改为 v-leave-from

  • keyCode 作为 v-on 修饰符的支持。

  • v-model 指令在组件上的使用已经被重新设计,替换掉了 v-bind.sync。

  • v-ifv-for 在同一个元素身上使用时的优先级发生了变化。

  • 移除了$on$off$once 实例方法。

  • 移除了过滤器 filter

  • 移除了$children 实例 propert

    ……