Skip to content

Vue3

一、Vue3 简介

  • 2020年9月18日,Vue.js发布版3.0版本,代号:One Piece

1.1 性能提升

  • 打包大小减少41%
  • 初次渲染快55%, 更新渲染快133%
  • 内存减少54%

1.2 源码的升级

  • 使用Proxy代替defineProperty实现响应式。

  • 重写虚拟DOM的实现和Tree-Shaking

1.3 拥抱TypeScript

  • Vue3可以更好的支持TypeScript

1.4 新特性

  1. Composition API(组合API):

    • setup

    • refreactive

    • computedwatch

      ......

  2. 新的内置组件:

    • Fragment

    • Teleport

    • Suspense

      ......

  3. 其他改变:

    • 新的生命周期钩子

    • data 选项应始终被声明为一个函数

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

      ......

二、创建Vue3工程

2.1 基于 vue-cli 创建

点击查看官方文档

备注:目前vue-cli已处于维护模式,官方推荐基于 Vite 创建项目。

powershell
## 查看@vue/cli版本,确保@vue/cli版本在4.5.0以上
vue --version

## 安装或者升级你的@vue/cli 
npm install -g @vue/cli

## 执行创建命令
vue create vue_test

##  随后选择3.x
##  Choose a version of Vue.js that you want to start the project with (Use arrow keys)
##  > 3.x
##    2.x

## 启动
cd vue_test
npm run serve

2.2 基于 vite 创建(推荐)

vite 是新一代前端构建工具,官网地址:https://vitejs.cnvite的优势如下:

  • 轻量快速的热重载(HMR),能实现极速的服务启动。
  • TypeScriptJSXCSS 等支持开箱即用。
  • 真正的按需编译,不再等待整个应用编译完成。
  • webpack构建 与 vite构建对比图如下:
image-20240408100243590image-20240408100303641
powershell
## 1.创建命令
npm create vue@latest

## 2.具体配置
## 配置项目名称
√ Project name: vue3_test
## 是否添加TypeScript支持
√ Add TypeScript?  Yes
## 是否添加JSX支持
√ Add JSX Support?  No
## 是否添加路由环境
√ Add Vue Router for Single Page Application development?  No
## 是否添加pinia环境
√ Add Pinia for state management?  No
## 是否添加单元测试
√ Add Vitest for Unit Testing?  No
## 是否添加端到端测试方案
√ Add an End-to-End Testing Solution? » No
## 是否添加ESLint语法检查
√ Add ESLint for code quality?  Yes
## 是否添加Prettiert代码格式化
√ Add Prettier for code formatting?  No
2.2.1 自己动手编写一个App组件

mian.ts

ts
// 引入createApp用于创建应用
import { createApp } from "vue";
// 引入App根组件
import App from "./App.vue";

createApp(App).mount("#app");

App.vue

vue
<template>
  <div><h1 class="app">你好啊</h1></div>
</template>

<script lang="ts">
export default {
  // 组件名
  name: "App",
};
</script>

<style>
/* 样式 */
.app {
  background-color: #ddd;
  box-shadow: 0, 0, 10px;
  border-radius: 10px;
  padding: 20px;
}
</style>
2.2.2 总结
  • Vite项目中,index.html 是项目的入口文件,在项目最外层;vue-cli创建的项目入口文件:main.js
  • 加载 index.html 后,Vite解析 <script type="module" src="/src/main.ts"></script>指向的TS文件
  • Vue3 中是通过 createApp 函数创建一个应用实例

2.3 一个简单效果

Vue3向下兼容 Vue2 语法,且 Vue3 中的模板中可以没有根标签

vue
<template>
  <div class="person">
    <h2>姓名:{{ name }}</h2>
    <h2>年龄:{{ age }}</h2>
    <button @click="changename">修改名字</button>
    <button @click="changeage">修改年龄</button>
    <button @click="showTel">查看联系方式</button>
  </div>
</template>

<script lang="ts">
export default {
  name: "Person",
  data() {
    return {
      name: "张三",
      age: 18,
      tel: "124742759548",
    };
  },
  methods: {
    showTel() {
      alert(this.tel);
    },
    changename() {
      this.name = "zhang-san";
    },
    changeage() {
      this.age += 1;
    },
  },
};
</script>

<style scoped>
.person {
  background-color: skyblue;
  box-shadow: 0 0 10px;
  border-radius: 10px;
  padding: 20px;
}
button {
  margin: 0 5px;
}
</style>

Vue3的一个升级:在 <template>标签内可以不写根标签,可以有多个根标签

三、Vue3核心语法

3.1 OptionsAPI 与 CompositionAPI

  • Vue2API 设计是 Options(配置/选项)风格的
  • Vue3API 设计是 Composition (组合)风格的
3.1.1 Options API 的弊端

Options类型的 API,数据,方法,计算属性等,是分散在:datamethodscomputed中的,若想新增或者修改一个需求,就需要分别修改:datamethodscomputed、不便于维护和复用。

1696662197101-55d2b251-f6e5-47f4-b3f1-d8531bbf92791696662200734-1bad8249-d7a2-423e-a3c3-ab4c110628be

3.1.2 Composition API 的优势

可以用函数的方式,更加优雅的组织代码,让相关功能的代码更加有序的组织在一起

1696662249851-db6403a1-acb5-481a-88e0-e1e34d2ef53a1696662256560-7239b9f9-a770-43c1-9386-6cc12ef1e9c0

3.2 setup

3.2.1 setup 概述

setupVue3 中的一个新的配置项,值是一个函数,它是 Composition API “表演的舞台”,组件中所用到的:数据、方法、计算属性、监视.....等等,均配置在 setup 中。

特点如下:

  • setup 函数返回的对象中的内容,可以直接在模板中使用。
  • setup中访问 thisundefinedVue3 中已经弱化了 this
  • setup 函数是组合式API的第一个钩子函数
vue
<template>
  <div class="person">
    <h2>姓名:{{ name }}</h2>
    <h2>年龄:{{ age }}</h2>
    <button @click="changeName">修改名字</button>
    <button @click="changeAge">修改年龄</button>
    <button @click="showTel">查看联系方式</button>
  </div>
</template>

<script lang="ts">
export default {
  name: "Person",
  beforeCreate() {
    console.log("beforeCreate");
  },
  // setup在beforeCreate之前执行
  setup() {
    console.log("setup");
    // console.log(this); setup函数中的this是undefined,Vue3中已经弱化this了
    // 数据,原来是写在data中的 注意:此时的name,age,tel都不是响应式的数据
    let name = "张三";
    let age = 18;
    let tel = 121398493;
    // 方法
    function changeName() {
      name = "zhang-san"; // 注意:这样修改name,页面是没有变化的
      console.log(name); // name被修改了,但是页面没有变化
    }
    function changeAge() {
      age += 1; //注意:这样修改name,页面是没有变化的
      console.log(age); // name被修改了,但是页面没有变化
    }
    function showTel() {
      alert(tel);
    }
    // 将数据和方法交出去,模板中才能直接使用
    return { name, age, changeName, changeAge, showTel };
  },
};
</script>

<style scoped>
.person {
  background-color: skyblue;
  box-shadow: 0 0 10px;
  border-radius: 10px;
  padding: 20px;
}
button {
  margin: 0 5px;
}
</style>
3.2.2 setup 的返回值
  • 返回一个对象

    则返回对象中的:属性、方法等、在模板中均可以直接使用(常用

js
setup(){
    .....
	// 将数据和方法交出去,模板中才能直接使用
	return { name, age, changeName, changeAge, showTel };
}
  • 返回一个函数

    则可以在函数中自定义渲染内容

js
setup(){
	return ()=> {
 	 // 返回的内容会被直接渲染到页面中
  	return "haha";
	}; 
}
3.2.3 setup 与 Options API 的关系
  • setup (Vue3写法)可以和 data、methods..(Vue2写法)同时存在(不建议
  • Vue2 的配置(datamethods....)中可以通过this访问setup 中的属性、方法
  • setup不能访问Vue2 的配置(datamethods...)
  • 如果与 Vue2 冲突,则 setup 优先
js
export default {
  name: "Person",
  data() {
    return {
      a: 100,
// 2.data中可以通过this读取到setup中的数据
      c:this.name,
      d:900
    };
  },
  methods: {
    b() {
      console.log("b");
    },
  },
// 1.data和methods可以和setup同时存在(不建议) 
  setup() {
    // console.log(this); this:undefined
    // 数据,原来是写在data中的 注意:此时的name,age,tel都不是响应式的数据
    let name = "张三";
    let age = 18;
    let tel = 121398493;
// 3.在setup中读取不到data中的数据
    // let x = d; d未定义
    // 方法
    function changeName() {
      name = "zhang-san"; // 注意:这样修改name,页面是没有变化的
      console.log(name); // name被修改了,但是页面没有变化
    }
      ....
    // 将数据和方法交出去,模板中才能直接使用
    return { name, age, changeName, changeAge, showTel };
  },
3.2.3 setup 语法糖

setup 函数有一个语法糖,这个语法糖,可以让我们把 setup 独立出去,代码如下:

vue
<template>
  ....
</template>
// 用于指定组件在开发者工具中的名称,如果不指定默认为组件文件名
<script lang="ts">
  export default {
    name:'Person',
  }
</script>

<!-- 下面的写法是setup语法糖 -->
<script setup lang="ts">
  // 数据(注意:此时的name、age、tel都不是响应式数据)
  let name = '张三'
  let age = 18
  let tel = '13888888888'

  // 方法
  function changName(){
    name = '李四'//注意:此时这么修改name页面是不变化的
  }
    ...
   // 不需要使用return语句
</script>

使用 <script setup lang=ts></script> 包裹原本setup函数中的内容,可简化setup函数,且无需返回值

3.2.4 vite-plugin-vue-setup-extend 插件

按照上面写法,需要写两个 script 标签,一个不写 setup 用于指定组件名,一个写 setup 用于组合式API,较为麻烦。

可以借助 vite 中的插件 vite-plugin-vue-setup-extend来简化为一个 script 标签,并可以指定组件名

  1. 第一步:npm i vite-plugin-vue-setup-extend -D

  2. 第二步:配置 vite.config.ts

ts
.....
// 1.引入
import VueSetupExtend from "vite-plugin-vue-setup-extend"

export default defineConfig({
  plugins: [
    vue(),
    // 2.使用
    VueSetupExtend(),
  ],
.....
  1. 第三步:在 script标签中指定组件名

    vue
    <script setup lang="ts" name="Person">..</script>

3.3 ref 创建:基本类型的响应式数据

  • 作用:定义基本类型响应式变量
  • 语法:
vue
<template>
<!-- 4.使用响应式数据不需要.value -->
<h2>{{ xxx }}}</h2>
</template>
<script setup lang="ts" name="Person">
// 1.引入ref
import { ref } from "vue"; 
// 2.使用ref函数包裹响应式数据
let xxx = ref(初始值); 
// 3.使用xxx.value修改响应式数据
xxx.value = xx; // JS中操作ref对象时候需要.value
</script>
  • 返回值:一个 RefImpl 的实例对象,简称 ref响应式对象refref对象的 value 属性是响应式的。
vue
<template>
  <div class="person">
    <h2>姓名:{{name}}</h2>
    <h2>年龄:{{age}}</h2>
    <button @click="changeName">修改名字</button>
    <button @click="changeAge">年龄+1</button>
    <button @click="showTel">点我查看联系方式</button>
  </div>
</template>

<script setup lang="ts" name="Person">
  import {ref} from 'vue'
  // name和age是一个RefImpl的实例对象,简称ref对象,它们的value属性是响应式的。
  let name = ref('张三')
  let age = ref(18)
  // tel就是一个普通的字符串,不是响应式的
  let tel = '13888888888'

  function changeName(){
    // JS中操作ref对象时候需要.value
    name.value = '李四'
    console.log(name.value)

    // 注意:name不是响应式的,name.value是响应式的,所以如下代码并不会引起页面的更新。
    // name = ref('zhang-san')
  }
  function changeAge(){
    // JS中操作ref对象时候需要.value
    age.value += 1 
    console.log(age.value)
  }
  function showTel(){
    alert(tel)
  }
</script>

注意:

  1. JS中操作数据需要:xxx.value ,但模板中不需要 .value、直接使用即可
  2. 对应 let name = ref("张三")来说、name 不是响应式的、name.value是响应式的

3.4 reactive 创建:对象类型的响应式数据

  • 作用:定义一个响应式对象(object)(基本类型必须使用ref,否则报错)
  • 语法:
vue
<template>
<!-- 4.使用响应式对象数据 -->
<h2>obj.name</h2>
</template>
<script setup lang="ts" name="Person">
// 1.引入reactive
import { reactive } from "vue";
// 2.定义响应式对象 
let 响应式对象 = reactive(源对象);
let obj = reactive({name:"张三"})
// 3.修改响应式数据
obj.name = "李四"
</script>
  • 返回值:一个 Proxy的实例对象,简称:响应式对象
vue
<template>
  <div class="person">
    <h2>汽车信息</h2>
    <h2>一辆{{ car.brand }}车价值:{{ car.price }}w</h2>
    <button @click="changePrice">修改汽车的价格</button>
    <br />
    <h2>游戏列表</h2>
    <ul>
      <li v-for="game of games" :key="game.id">{{ game.name }}</li>
    </ul>
    <button @click="changeFirstGame">修改第一个游戏的名字</button>
    <hr />
    <h2>深层次响应 {{ obj.a.b.c }}</h2>
    <button @click="changeObj">测试</button>
  </div>
</template>

<script setup lang="ts" name="Person">
// 引入 reactive 实现对象类型的响应式数据
import { reactive } from "vue";

// 数据  使用reactive()包裹响应式对象类型的数据
let car = reactive({ brand: "奔驰", price: 100 });
let games = reactive([
  { id: "dshj01", name: "原神" },
  { id: "dshj01", name: "和平精英" },
  { id: "dshj01", name: "王者荣耀" },
]);
// 深层次响应数据
let obj = reactive({
  a: {
    b: {
      c: 666,
    },
  },
});
console.log(car); // Proxy
console.log(games);// Proxy

// 方法
function changePrice() {
  car.price += 10;
}
function changeFirstGame() {
  games[0].name = "绝地求生";
}
function changeObj() {
  obj.a.b.c = 888;
}
</script>

注意:

  1. reactive 定义的响应式数据是“深层次的”。(即对象可以多层嵌套)
  2. reactive 只能定义对象类型的响应式数据
  3. 对象类型的数据可以是:对象、数组、方法....

3.5 ref创建:对象类型的响应式数据

  • 其实 ref 接收的数据可以是(any):基本类型对象类型
  • ref 接收的是对象类型,内部其实也是调用了 reactive函数
  • ref接收的是对象类型,.valueProxy对象
  • ref接收的是基本类型,.value为响应式数据
vue
<template>
  <div class="person">
    <h2>汽车信息</h2>
    <h2>一辆{{ car.brand }}车价值:{{ car.price }}w</h2>
    <button @click="changePrice">修改汽车的价格</button>
    <br />
    <h2>游戏列表</h2>
    <ul>
      <li v-for="game of games" :key="game.id">{{ game.name }}</li>
    </ul>
    <button @click="changeFirstGame">修改第一个游戏的名字</button>
  </div>
</template>

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

// 数据 
let car = ref({ brand: "奔驰", price: 100 });
let games = ref([
  { id: "dshj01", name: "原神" },
  { id: "dshj01", name: "和平精英" },
  { id: "dshj01", name: "王者荣耀" },
]);
    
console.log(games); // Ref
console.log(games.value); // Proxy
    
// 方法
function changePrice() {
  // ref需要.value才能实现响应式修改
  car.value.price += 10;
  console.log(car.value.price);
}
function changeFirstGame() {
  // 对象名.value.属性名 = 新值
  games.value[0].name = "绝地求生";
}
</script>

注意:

ref 定义对象类型的响应式数据,在修改时需要使用 对象名.value.属性名 = 新值 因为 ref 对象的 value属性才能获取到值,但在模板中使用时不要 .value 模板会自动添加

3.6 ref 对比 reactive

定义数据类型

  1. ref 用来定义:基本类型数据对象类型数据(any)
  2. reactive用来定义:对象类型数据(Object)

区别

  1. ref创建的变量必须使用 .value(可以使用 volar插件自动添加 .value

    image-20240414101555636

  2. reactive 定义的响应式对象不可整体被修改,否则会失去响应式(可以使用 Object.assign合并)

js
let car = reactive({ brand: "奔驰", price: 100 });

function changeCar() {
  car = { brand: "奥运", price: 20 }; // car失去响应式页面不更新
  car = reactive({ brand: "奥运", price: 20 }); // 不是响应式
  // 原因:car原本是一个Proxy对象,被赋值为一个普通对象,因此失去响应式
    
  Object.assign(car, { brand: "奥运", price: 20 }); 
  // 解决办法:对象合并不改变原对象
}

使用原则

  1. 若需要一个基本类型的响应式数据,必须使用 ref
  2. 若需要一个响应式对象,层级不深,refreactive都可以
  3. 若需要一个响应式对象,且层级较深,推荐使用 reactive
  4. 若需要一个响应式对象,且需要频繁进行整体替换,推荐使用 ref

总结:定义响应式数据使用 ref 即可,不必使用 reactive

3.7 toRefs 与 toRef

  • 作用:将一个reactive响应式对象中的每一个属性,转换为 ref对象

  • 使用场景:将一个 reactive 定义的响应式对象解构赋值时,配合 toRefs 使用,解构出的变量均为 ref 响应式对象,且解构出来的响应式属性等同于源对象中的属性。(简化响应对象中属性的修改)(普通解构后,修改解构后的数据不会影响源数据)

vue
<template>
  <div class="person">
    <h2>姓名:{{ person.name }}</h2>
    <h2>年龄:{{ age }}</h2>
    <button @click="changeName">修改名字</button>
    <button @click="changeAge">修改年龄</button>
  </div>
</template>

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

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

// 通过toRefs将person对象中的n个属性批量取出,依然保持响应式的能力
console.log(toRefs(person)); // {name: ObjectRefImpl, age: ObjectRefImpl}
// 通过解构赋值,将toRefs生成的响应式属性赋值给变量
let { name, age } = toRefs(person);
console.log(name, age); // ref响应对象

let nl = toRef(person, "age");// 通过toRef将person对象的某个属性转换为响应式属性
console.log(nl, nl.value); // ref响应对象

// 方法
function changeName() {
  // person.name += "~"; // 不使用toRefs
  name.value += "~"; // 直接修改响应属性
  console.log(name.value === person.name); // 在模板中使用name/person.name均可
}
function changeAge() {
  age.value += 1;
}
</script>

tips:

  1. toRefstoRef 功能一致,但 toRefs可以批量转换
  2. 解构出来的响应式属性等同于对象中的属性
js
let { name, age } = toRefs(person); // person reactive相应式数据
console.log(name.value === person.name); // true 解构出来的属性为 ref响应式数据
  1. toRefs 转换的为 ref 响应式数据,需要使用 .value 修改响应数据

3.8 computed

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

返回值:ref 响应式对象

何时调用:读取计算属性的值时(有缓存只调用一次),依赖计算的属性发生改变时

计算属性值会基于其响应式依赖被缓存,一个计算属性仅会在其响应式依赖更新时才重新计算。(可以进行性能优化,用于计算一些计算量较大且不需要每次重新渲染时进行计算)

两种写法:

  1. 只可读
js
let c1 = computend(()=>{
    return ... // 返回计算属性的值
})
  1. 可读可修改
js
let c2 = computend({
    get(){ // 读取时调用
        return ...// 返回计算属性的值
    },
    set(val){ // 修改时调用
        ...// 修改计算属性依赖计算的属性重新调用get函数
    },
})

计算属性实现姓名案例:

vue
<template>
  <div class="person">
    姓:<input type="text" v-model.trim="firstName" /><br />
    名:<input type="text" v-model.trim="lastName" />
    <button @click="changeFullName">将全名改为 li-si</button><br />
    <div>姓名:{{ fullName }}</div>
    <div>姓名:{{ fullName }}</div>
    <div>姓名:{{ fullName2() }}</div>
    <div>姓名:{{ fullName2() }}</div>
  </div>
</template>

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

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

// 方法实现
function fullName2() {
  // 方法执行时没有缓存,调用一次就执行一次
  console.log("方法执行了");
  return (
    firstName.value.charAt(0).toUpperCase() +
    firstName.value.slice(1) +
    "-" +
    lastName.value
  );
}

// 计算属性实现 (这样定义的计算属性只可读,不可修改)
let fullName = computed(() => {
  // 计算属性存在缓存,只有当依赖计算的数据发生变化时才重新计算
  console.log("computed执行了");
  return (
    firstName.value.charAt(0).toUpperCase() +
    firstName.value.slice(1) +
    "-" +
    lastName.value
  );
});
console.log(fullName); // ComputedRefImpl 计算属性返回值为一个响应式ref对象
// fullName.value = "newName"; // 无法为value赋值,因为它是只读的

// 计算属性实现(这样定义的计算属性是可读可写的)
let fullName = computed({
  get() {
    return (
      firstName.value.charAt(0).toUpperCase() +
      firstName.value.slice(1) +
      "-" +
      lastName.value
    );
  },
  set(val) {
    console.log(val); // val:计算属性被修改为的值
    // 修改计算属性依赖计算的属性值,实现计算属性值的修改
    [firstName.value, lastName.value] = val.split("-");
  },
});

function changeFullName() {
  fullName.value = "li-si"; // 修改计算属性的值时,调用computed中的set方法
}
</script>

3.9 watch

作用:监视数据的变化(和 Vue2 中的 watch 作用一致)

特点:Vue3 中的 watch 只能监视以下四种数据

  1. ref 定义的数据(包括计算属性)
  2. reactive 定义的数据
  3. 函数返回一个值(getter函数)(常用)
  4. 一个包含上述内容的数组

注意:不能直接监听响应式对象的属性值,需要使用一个返回该属性的 getter 函数

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

3.9.1 情况一(常用):

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

返回值:用于停止监视的函数

语法:

js
const stopWatch = wacth(属性名,(newValue,oldValue)={
    ....
    stopWatch(); //停止监视
})

实例:

vue
<template>
  <div class="person">
    <h1>情况一:监视【ref】定义的【基本类型】数据</h1>
    <h2>当前求和值为:{{ sum }}</h2>
    <button @click="changeSum">点我sum加一</button>
  </div>
</template>

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

// 定义数据
let sum = ref(0);

// 定义方法
function changeSum() {
  sum.value += 1;
}
// 监视,情况一:监视【ref】定义的【基本类型】数据
const stopWatch = watch(sum, (newVlue, oldValue) => {
  console.log("sum变化了", newVlue, oldValue);
  if (newVlue >= 10) {
    stopWatch();  // 停止监视
  }
});
console.log(stopWatch);// watch返回一个函数,用于停止监视
</script>
3.9.2 情况二:

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

语法:

js
const stopWatch = watch(
	person, // 被监视对象
    (newValue,oldValue) => {
        .....
    },
    { deep:true, immediate:true }
    // 配置项:开启深度监视和初始化时执行一次
)

watch的三个参数:

  1. 第一个参数:被监视的对象
  2. 第二个参数:监视的回调
  3. 第三个参数:配置对象

watch的返回值:停止监视的函数

实例:

vue
<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 setup lang="ts" name="Person">
import { ref, watch } from "vue";

// 数据
let person = ref({
  name: "张三",
  age: 18,
});
// 方法 
// 修改对象中的属性,需要开启深度监视且oldValue和newValue相同
function changeName() {
  person.value.name += "~";
}
function changeAge() {
  person.value.age += 1;
} 
// 修改整个对象 
function changePerson() {
  person.value = { name: "李四", age: 20 };
}
// 监视,情况二:监视的是【ref】定义的【对象类型】数据,监视的是对象的地址值,
// 若想监视对象内部属性的变化,需要手动开启深度监视
watch(
  person, // 被监视的对象
  (newValue, oldValue) => {
    console.log("peroson变化了", newValue, oldValue); // proxy
  },
  { deep: true, immediate: true } // 开启深度监视,初始时调用一次
);
</script>

注意:

  1. 若修改的是 ref 定义的对象中的属性,newValueoldValue 都是新值,因为他们时同一个对象,指向同一个地址
  2. 若修改整个 ref 定义的对象,newValue是新值,oldValue 是旧值,因为不是同一个对象了
3.9.3 情况三:

监视 reactive 定义的【对象类型】数据,且默认开启了深度监视,且监视回调函数中的 newValue 和 oldValue 相同

js
import { reactive, watch } from "vue";

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

// 方法
function changeName() {
  person.name += "~";
}
function changeAge() {
  person.age += 1;
}
// 修改整个对象
function changePerson() {
  // person = { name: "李四", age: 20 };// reactive不可整体被修改
  Object.assign(person, { name: "李四", age: 20 }); // 解决办法
}

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

注意:

若使用 reactive 定义对象类型数据,是不可以直接修改整个对象的(obj={..} 失去响应式 ),要使用 Object.assign(obj,{...})来进行合并(地址不发生变化)

3.9.4 情况四(常用):

监视 refreactive 定义的【对象类型】数据中的某个属性 分以下两种情况:

  1. 若要监视的属性值是基本类型,需要写成函数式,函数返回要监视的属性
js
let person = reactive({
  name: "张三",
  age: 18,
  car: {
    c1: "奔驰",
    c2: "宝马",
  },
});
....
// 监视对象属性为基本类型使用函数式
watch(
  () => person.name, //使用函数返回要监视的值
  (newValue, oldValue) => {
    console.log("person变化了", newValue, oldValue);
  }
);
  1. 若该属性值是对象类型,可直接写,也可以写成函数返回要监视的属性,建议写成函数式+deep:true
js
let person = reactive({
  name: "张三",
  age: 18,
  car: {
    c1: "奔驰",
    c2: "宝马",
  },
});
....
// 监视的对象属性值为对象类型,最好使用函数式+deep:true
watch(
  () => person.car, // 使用函数返回要监视的值
  (newValue, oldValue) => {
    console.log("person.car变化了", newValue, oldValue);
  },
  { deep: true } // 开启深度监视
);

结论:要监视对象中的某个属性,那么最好写函数式,如果对象的属性仍是对象类型,那么监视的是地址值,需要在内部加上 deep:true

Tips:函数式也适用于 ref 定义的基本类型响应式数据,要加 .value

3.9.5 情况五:

监视上述多个数据

将(ref定义的数据,reactive定义的数据,getter函数)放入一个数组内

js
// 数据
let person = reactive({
  name: "张三",
  age: 18,
  car: {
    c1: "奔驰",
    c2: "宝马",
  },
});
....
// watch,监视多个数据
watch(
  [() => person.name, () => person.car.c1], // 要监视的多个数据
  (newValue, oldValue) => {
    // 监视回调
    console.log("person.name 或 person.car.c1 发生变化", newValue, oldValue);
    //  ['张三~', '奔驰']  ['张三', '奔驰']
  },
  { deep: true } // 深度监视
);

tips:newValue为一个包含被监视内容的数组

3.10 watchEffect

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

监视回调执行时机:最开始执行一次,监视函数中用到的数据改变时执行

watch 对比 watchEffect

  1. 都能监听响应式数据的变化,不同的是监听数据变化的方式不同
  2. watch :要明确指出监视的数据
  3. watchEffect:不用明确指出监视的数据(监视函数中用到哪些属性,就监视哪些属性)

示例:

vue
<template>
  <div class="person">
    <h2>当水温达到60℃或水位达到80cm时,给服务器发请求</h2>
    <h2>当前水温:{{ temp }}℃</h2>
    <h2>当前水位:{{ height }}cm</h2>
    <button @click="changeTemp">水温加10</button>
    <button @click="changeHeight">水位加10</button>
  </div>
</template>

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

// 数据
let temp = ref(10);
let height = ref(10);

// 方法
function changeTemp() {
  temp.value += 10;
}
function changeHeight() {
  height.value += 10;
}

// watch实现:需要指明监视的属性
watch([temp, height], ([newTemp, newHeigth]) => {
  if (newTemp >= 60 || newHeigth >= 80) {
    console.log("给服务器发请求");
  }
});

// watchEffect实现,不需要指定监视的属性,不根据监视函数中的属性自动监视
watchEffect(() => {
  if (temp.value >= 60 || height.value >= 80) {
    console.log("给服务器发请求");
  }
});
</script>

3.11 标签的 ref 属性

作用:获取DOM或组件的实例对象

用在普通 html 标签上,获取的是 DOM 节点

使用步骤:

  1. 为元素绑定 ref 属性

    html
    <h2 ref="h2">河南</h2>
  2. 创建变量用于存储 ref 标记的内容(变量名要和 ref 标记名一致)

    js
    let h2 = ref();
  3. 获取 ref 绑定元素的 DOM元素

    js
    h2.value
vue
<template>
  <div class="person">
<!-- 1.为元素绑定ref属性 -->
    <h2 ref="title2">河南</h2>
    <button @click="showLog">点我输出h2元素</button>
  </div>
</template>

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

// 2.创建一个title2用于存储ref标记的内容(变量名要和ref标记一致)
let title2 = ref();

function showLog() {
// 3.获取ref标记元素的DOM元素
  console.log(title2.value);
}
</script>

用在组件标签上,获取的是组件实例对象

使用步骤:

  1. 为子组件绑定 ref 属性

    html
    <Person ref="personRef" />
  2. 获取子组件的实例对象,创建变量保存 ref 标记的内容(要与ref标记名一致)

    js
    let personRef = ref()
  3. 在子组件中暴露数据供父组件中 ref 获取

    js
    defineExpose({x,x,x})
  4. 调用子组件暴露的属性/方法

    js
    personRef.value.x
vue
<template>
<!-- 1.为子组件绑定ref属性 -->
  <Person ref="ren" />
  <button @click="showLog">测试</button>
</template>

<script setup lang="ts" name="App">
import { ref } from "vue";
import Person from "./components/Person.vue";
// setup会自动将模板中的数据进行返回

// 2.创建变量保存ref标记内容
let ren = ref();
function showLog() {
  console.log(ren.value);
  // 在父组件中给子组件标签绑定ref,获取到的是子组件的实例对象, 只能访问到子组件通过defineExpose暴露出来的数据
}
</script>

注意:

  1. 用于存储 ref 标记内容的变量名必须与 ref 标记名一致
  2. 为组件标签添加 ref 标记,要想获取到组件实例对象上的数据,需要在子组件中使用 defineExpose 进行暴露

3.12 回顾接口、自定义类型,泛型

将TS的类型限制定义在 src/types/index.ts 文件

ts
// 定义一个接口用于限制person对象的具体属性
export interface PersonInter {
  id: string;
  name: string;
  age: number;
}

// 一个自定义类型限制一个数组内使用PersonInter的类型 
export type Persons = Array<PersonInter>
export type Persons = PersonInter[]

在vue文件中引入并使用

js
// 引入类型时,需要在加上 type
import type { PersonInter, Persons } from "@/types"; // @ = src
let person: PersonInter = { id: "hashfash2", name: "张三", age: 60 };

let personList: Persons = [
  { id: "hashfash2", name: "张三", age: 60 },
  { id: "hashfash3", name: "李四", age: 20 },
  { id: "hashfash4", name: "王五", age: 30 },
];

在组件中引入接口、自定义类型.....时需要加上 type

在路径中 @ === /src

若一个文件下存在 index.js/ts 引入时只需写到文件夹名,会自动寻找 index.js/ts

3.13 props

作用:父组件给子组件传递数据(父子组件间通信)

实现步骤:

  1. 定义接口、自定义类型用于限制数据类型
ts
// 定义一个接口,限制每一个Person对象的格式
export interface PersonInter{
	id:string,
    name:string,
    age:number
}
// 自定义类型限制数组Persons
export type Persons = PersonInter[]
  1. App.vue 为子组件传递数据
vue
<template>
	<!-- 使用props给子组件传递数据 -->
	<Person a="哈哈" :list="personList" />
</template>

<script setup lang="ts" name="App">
import { reactive } from "vue";
import Person from "./components/Person.vue";
// 引入定义的类型
import { type Persons } from "@/types";
// reactive定义数据时使用泛型限制数据类型
let PersonList = reactive<Persons>([
  { id: "fdhgjhg01", name: "张三", age: 18 },
  { id: "fdhgjhg02", name: "李四", age: 20 },
  { id: "fdhgjhg03", name: "王五", age: 22 },
])
</script>
  1. Person.vue 在子组件中接收数据
vue
<template>
<!-- 渲染接收到的数据 -->
  <div class="person">
    <h2>{{ a }}</h2>
    <ul>
      <li v-for="item of list" :key="item.id">
        {{ item.name }}---{{ item.age }}
      </li>
    </ul>
  </div>
</template>

<script setup lang="ts" name="Person">
import { type Persons } from "@/types";
    
// 四种接收数据的方式:
//1.只接收数据
defineProps(["a","list"]);
    
//2.接收数据+类型限制(泛型)
defineProps<{ list: Persons; a: string }>();
    
//3.接收数据+类型限制(泛型)+限制必要性(?)+指定默认值
defineDefault(defineProps<{ list: Person; a?: string }>(),{ a:"我是默认值",list: ()=> [{ id: "131v", name: "张三", age: 18 }] });
    
// 4.接收数据并保存
let x = defineProps(["a","list"]); // Proxy(Object) {a: '哈哈', list: Proxy(Array)}
console.log(x.a); // 哈哈
x.a = "呵呵"; // 无法对a赋值,因为它是只读属性

注意:

  1. defineProps 接收到的数据为响应式的可以直接在模板中使用,但不能直接在 js 代码中使用,需要进行接收
  2. props 传递的数据只可读,不可修改
  3. reactive 定义数据时使用泛型限制数据类型 let fruits = reactive<TypeFruits>() ,不可以直接 let fruits: TypeFruits = reactive()

3.14 生命周期

  • 概念:

    Vue 组件实例在创建时要经历一系列的初始化步骤,在此过程中 Vue 会在合适的时机,调用特定的函数,从而让开发者有机会在特定阶段运行自己的代码,这些特定的函数统称为:生命周期钩子

  • 规律:

    生命周期整体分为四个阶段,分别是:创建挂载更新销毁、每个阶段都有两个钩子,一前一后

  • Vue2的生命周期(选项式API):

创建阶段:beforeCreatecreated

挂载阶段:beforeMountmounted

更新阶段:beforeUpdateupdated

销毁阶段:beforeDestroydestroyed

  • Vue3 的生命周期(组合式API):

创建阶段:setup(执行setup函数中的内容)

挂载阶段:onBeforeMountonMounted(渲染组件)

更新阶段:onBeforeUpdateonUpdated(更新组件)

卸载阶段:onBeforeUnmountonUnmounted

  • 常用的钩子:onMounted(挂载完毕:发请求)、onUpdated(更新完毕)、onBeforeUnmount(卸载之前:关闭定时器)

示例:

vue
<script setup lang="ts" name="Person">
import {
  onBeforeMount,
  onBeforeUnmount,
  onBeforeUpdate,
  onMounted,
  onUnmounted,
  onUpdated,
  ref,
} from "vue";
// 数据
let sum = ref(0);
// 方法
function add() {
  sum.value += 1;
}
// 创建
console.log("创建");

// 挂载前: Vue3在挂载前调用onBeforeMount中指定的回调函数
onBeforeMount(() => {
  console.log("挂载前");
});
// 挂载完毕:Vue3在挂载完毕调用onMounted中指定的回调函数
onMounted(() => {
  console.log("子---挂载完毕");
});
// 更新前:Vue3在更新前调用onBeforeUpdate中指定的回调函数
onBeforeUpdate(() => {
  console.log("更新前");
  // debugger;
});
// 更新完毕:Vue3在更新完毕调用onUpdated中指定的回调函数
onUpdated(() => {
  console.log("更新完毕");
});
// 卸载前:Vue3在卸载前调用onBeforeUnmount中指定的回调函数
onBeforeUnmount(() => {
  console.log("卸载前");
});
// 卸载完毕:Vue3在卸载完毕用onUnmount中指定的回调函数
onUnmounted(() => {
  console.log("卸载完毕");
});
</script>

3.15 自定义hook

  • 什么是 hook

    本质是一个函数,把 setup 函数中使用的 Compositions API 进行了封装,类似于 Vue2.x 中的 mixin

  • 自定义 hook 的优势:

    代码复用,让 setup 中的逻辑更加清楚易懂

  • 作用:

    将一个组件中不同功能的数据、方法、计算属性..... 分离成独立的 hook ,使不同功能代码维护起来更加方便,组件中的代码也更加清晰,也便于代码的复用

使用示例:

  1. src/hooks 下创建 usexxx.ts 文件,将功能代码放在一个函数中,将要暴露的数据使用 return 返回,最后将函数暴露。

scr/hooks/useSum.ts(求和相关功能):

ts
import { computed, onMounted, ref } from "vue";
// 将函数暴露
export default function () {
  // 数据
  let sum = ref(0);
  // 计算属性
  let bigSum = computed(() => {
    return sum.value * 10;
  });
  // 方法
  function add() {
    sum.value += 1;
  }
  // 钩子
  onMounted(() => {
    sum.value += 100;
  });
  // 向外部提供数据
  return { sum, bigSum, add };
}

useDog.ts(获取狗图片相关功能):

ts
import { onMounted, reactive } from "vue";
import axios from "axios";

// 将数据和方法放在一个函数中,然后将函数暴露
export default function () {
  // 数据
  let dogList = reactive<string[]>([]);

  // 方法
  // 使用async和await代替then,(try catch)/拦截器代替catch
  async function getDog() {
    try {
      // result 接收请求成功返回的数据
      let result = await axios.get(
        "https://dog.ceo/api/breed/pembroke/images/random"
      );
      dogList.push(result.data.message); // 成功处理
    } catch (error) {
      // 请求失败 error:错误信息
      alert(error); // 失败处理
    }
  }
  // 钩子
  onMounted(() => {
    getDog();
  });
  // 向外界提供的属性、方法
  return { dogList, getDog };
}
  1. 在组件中引入 hook 并调用 hook函数 获取到数据
vue
<template>
  <div class="person">
    <h2>当前求和为:{{ sum }}</h2>
    <h2>求和放大10倍:{{ bigSum }}</h2>
    <button @click="add">点我加一</button>
    <hr />
    <img v-for="(item, index) of dogList" :key="index" :src="" data-missing="item" /><br />
    <button @click="getDog">再来一只修狗</button>
  </div>
</template>

<script setup lang="ts" name="Person">
// 引入hooks
import useSum from "@/hooks/useSum";
import useDog from "@/hooks/useDog";
// 调用hook函数获取数据
const { sum, bigSum, add } = useSum();
const { dogList, getDog } = useDog();
</script>

注意:

  1. scr/hooks 下的文件名必须为 usexxx.ts
  2. 在每个hook中可以写钩子函数,计算属性....
  3. hook函数为匿名函数,且使用默认暴露,将要暴露的数据通过return返回

3.16 axios 配合 async+await

async await配合使用可以代替 then回调函数,使代码变得更加同步,但不能进行错误处理,可以使用 try catch包裹或使用请求拦截器进行处理

js
// 使用async和await代替then,(try catch)/拦截器代替catch
  async function getDog() {
    try {
      // result 接收请求成功返回的数据 await会阻塞后续代码执行
      let result = await axios.get(
        "https://dog.ceo/api/breed/pembroke/images/random"
      );
      // 返回成功的Promise后才会执行下面语句
      dogList.push(result.data.message); // 成功处理
    } catch (error) {
      // 请求失败 error:错误信息
      alert(error); // 失败处理
    }
  }

四、路由

4.1 对路由的理解

实现 SPA (单页面应用),前端路由(route)就是一组key(路径)value(组件)对应关系,多个路由,需要经过一个路由器(router)的管理

image-20240416122939751

4.2 路由基本使用

  1. 安装 vue-router

    npm i vue-router

  2. 创建 router/index.ts 文件夹配置路由器

ts
// 1.引入createRouter
import { createRouter, createWebHistory } from "vue-router";
// 引入路由路由组件
import About from "@/components/About.vue";
import Home from "@/components/Home.vue";
import News from "@/components/News.vue";

// 2.创建路由器
const router = createRouter({
    // 设置路由器的工作模式
    history: createWebHistory(),
    routes: [  // 路由规则
        {
     	 path: "/home",
      	 component: Home,
   		 },
    	{
      	path: "/about",
      	component: About,
   		 },
    	{
     	 path: "/news",
     	 component: News,
   		 },
    ]
})

// 3.将router暴露
export default router;
  1. 创建路由组件 src/views
  2. main.ts 中引入并使用路由器
ts
...
// 1.引入路由器
import router from "./router";

// 创建应用
const app = createApp(app);

// 2.使用路由器
app.use(router);

// 挂载
app.mount("#app");
  1. 在导航组件中设置 <RouterLink to='xx'></RouterLink><RouterView></RouterView>标签,用于切换路由和展示路由组件
vue
<template>
  <div class="app">
    <h2 class="title">Vue3路由测试</h2>
    <!-- 导航区 -->
    <div class="navigate">
      <!-- active-class='xx' 指定组件激活时的样式 -->
      <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>

tips:RouterLink 标签的 active-class="xxx" 属性可以指定组组件被激活时的样式类名

4.3 两个注意点

  1. 路由组件通常放在 viewpages 文件夹,公共组件通常放在 components 文件夹
  2. 路由组件的切换实际是进行了卸载和重新挂载的过程,如果需要缓存路由组件可以使用 <keep-alive><keep-alive>组件 keepAlive

4.4 路由器的工作模式

  1. history 模式(常用)

    React:BrowserRouter

    Vue2:mode: 'history'

    Vue3:history: createWebHistory()

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

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

  2. hash 模式

    Vue2:mode: 'hash'

    Vue3:history: createWebHashHistory()

    优点:兼容性更好,且服务器端不需要处理路径,#后面的路径不会带给服务器

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

4.5 to的两种写法

  1. 字符串写法
ts
<RouterLink to="/news" active-class="active">新闻</RouterLink>
  1. 对象写法(常用)
ts
<RouterLink :to="{path:'/about',name:"",params:"",query:""}" active-class="active">关于</RouterLink>

可以在对象中添加 name、query、params....属性

4.6 命名路由

使用:为每个路由规则添加一个 name 属性

作用:可以简化路由跳转及传参

使用 to 的对象写法配合 name 属性进行路由跳转

ts
// 路由规则
{
  name: "guanyu", // 命名路由
  path: "/about", // 可以替代写路径
  component: About,
},

// App.vue
<RouterLink :to="{ name: 'guanyu' }" active-class="active">关于</RouterLink>

4.7 嵌套路由

在一个路由规则中嵌套子路由规则,使用 children 配置项

作用:用于子路由组件中的导航

编写 News 的子路由:Detail.vue

ts
{
      name: "xinwen",
      path: "/news",
      component: News,
      // 嵌套路由使用children配置项
      children: [
        {
          name: "xiangqing", // 命名路由
          path: "detail", // 写法一
          path: "/news/detail" // 写法二
          component: Detail,
        },
      ],
},

在子路由组件中使用:

vue
<template>
  <div class="news">
    <!-- 导航区 -->
    <ul>
      <li v-for="item of newsList" :key="item.id">
        <RouterLink :to="{ name: 'xiangqing' }" active-class="active">{{
          item.title
        }}</RouterLink>
      </li>
    </ul>
    <!-- 展示区 -->
    <div class="news-content">
      <RouterView></RouterView>
    </div>
  </div>
</template>

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

const newsList = reactive([
  { id: "ndks01", title: "抗癌食物", content: "西瓜" },
  { id: "ndks01", title: "如何一夜暴富", content: "学IT" },
  { id: "ndks01", title: "震惊,万万没想到", content: "明天是周一" },
  { id: "ndks01", title: "好消息", content: "快放假了" },
]);
</script>

注意:

  1. 配置子路由的 path有两种写法
    1. 写路径全称 /parent/children
    2. 只写子路由路径 children(注意:不要写 /
  2. router-link 标签的 to 属性中路径要写完整
  3. 如果嵌套的层级较多,可以使用 to的对象写法,配合 name 属性

4.8 路由传参

4.8.1 query参数

形式:http://localhost:5173/news/detail?id=123title=haha

  • 传递参数(两种方法)

    1. to的字符串写法
    vue
    <li v-for="item of newsList" :key="item.id">
       <RouterLink
           :to="`/news/detail?id=${item.id}&title=${item.title}&content=${item.content}`"
        >{{ item.title }}</RouterLink >
    </li>
    1. to的对象写法(推荐)
    vue
    <li v-for="item of newsList" :key="item.id">
         <RouterLink
            :to="{
               name: 'xiangqing',
               query: { id: item.id, title: item.title, content: item.content },
              }"
          >{{ item.title }}</RouterLink>
    </li>
  • 接收参数并使用

    vue
    <template>
      <ul class="news-list">
        <li>编号:{{ query.id }}</li>
        <li>标题:{{ query.title }}</li>
        <li>内容:{{ query.content }}</li>
      </ul>
    </template>
    
    <script setup lang="ts" name="About">
    import { toRefs } from "vue";
    import { useRoute } from "vue-router";
        
    // 接受query参数,usePouter为一个hook
    const route = useRoute();
    console.log(route);
    // route为proxy响应式对象内部有当前路由的全部信息,且当route发生变化时会重新解析页面
        
    // 解构获取query参数
    const { query } = toRefs(route); // toRefs接收一个reactive定义的响应式对象
    // 注意:直接解构route获取到的属性不是响应式的,配合torefs获取响应式属性
    </script>

    注意:

    1. route 中保存着当前路由的全部信息,需要使用 useRoute() 去获取
    2. 直接解构一个reactive响应式对象,获取到的属性会失去响应式,需要配合 toRefs 进行解构
    3. toRefs 接收的是一个 reactive 的响应式对象
4.8.2 params参数

形式:http://localhost:5173/news/detail/params1/params2

  • 传递参数(两种方式):

    1. to 的字符串写法
    vue
    <li v-for="item of newsList" :key="item.id">
           <RouterLink
           :to="`/news/detail/${item.id}/${item.title}/${item.content}`"
             >{{ item.title }}</RouterLink >
    </li>
    1. to 的对象写法
    vue
    <li v-for="item of newsList" :key="item.id">
          <RouterLink
              :to="{
                name: 'xiangqing',
                params: {
                  id: item.id,
                  title: item.title,
                  content: item.content,
                },
              }"
          >{{ item.title }}</RouterLink>
    </li>

    注意:使用 to 的对象写法传递 params 参数时,必须使用 name 属性,而不能使用 path 属性

  • 声明接收:

    在路由规则中声明接收的参数

    js
    {
          name: "xinwen",
          path: "/news",
          component: News,
          // 嵌套路由使用children配置项
          children: [
            {
              name: "xiangqing",
              // path: "detail",
              path: "detail/:id/:title/:content?", 
              // 声明接收params参数 ?表示可选参数
              component: Detail,
            },
          ],
    },

    注意:传递 params 参数时,需要提前在路由规则中占位,/:aa? ?表示该参数是可选的,可以不传递

  • 接收参数并使用

    vue
    <template>
      <ul class="news-list">
        <li>编号:{{ params.id }}</li>
        <li>标题:{{ params.title }}</li>
        <li>内容:{{ params.content }}</li>
      </ul>
    </template>
    
    <script setup lang="ts" name="About">
    import { useRoute } from "vue-router";
    import { toRefs } from "vue";
    
    // 获取路由对象
    const route = useRoute();
    // 获取params参数并转换为响应式
    const { params } = toRefs(route); 
    </script>

4.9 路由的props配置

作用:让路由组件更加方便到的收到参数(可以将路由参数作为 props 传给组件)

使用:在路由配置中添加 props 配置项,通过 props 配置项将参数传递给路由组件(路由组件不使用组件标签,因此不能直接以 <Person a='xx' :b='xx'> 的形式传递props)

路由的props 配置的三种写法:

  1. props 的布尔值写法:

    将路由收到的所有params参数以props形式传递给路由组件

    ts
    {
        name: "xiangqing",
        path: "detail",
        // path: "detail/:id/:title/:content?",
        component: Detail,
        ....
        props: true,
    }

    只适用于 params 参数

  2. props 的函数写法:

    可以接收到route对象作为参数,return一个对象作为props传递给路由组件(可以传递 paramsquery参数)

    ts
    {
        name: "xiangqing",
        path: "detail",
        // path: "detail/:id/:title/:content?",
        component: Detail,
        .....
        props(route) {
            return {
               id: route.query.id,
               title: route.query.title,
               content: route.query.content,
            };
        },
    }

    通用写法:可以传递 paramsquery 参数

  3. 对象写法:

    自定义 props 传递的数据,(为固定数据)

    ts
    {
        name: "xiangqing",
        path: "detail",
        // path: "detail/:id/:title/:content?",
        component: Detail,
        .....
        props: {
           id: "0001",
           title: "标题1",
           content: "内容1",
        },
    }

    由于数据是固定的,在开发中一般不使用

路由组件接收使用路由props 配置传递的数据

vue
<template>
  <ul class="news-list">
    <li>编号:{{ id }}</li>
    <li>标题:{{ title }}</li>
    <li>内容:{{ content }}</li>
  </ul>
</template>

<script setup lang="ts" name="About">
// 使用 props 接受参数
// defineProps(["id", "title", "content"]);

// 使用泛型限制接收到参数的类型
defineProps<{ id: string; title: string; content: string }>();
</script>

4.10 replace属性

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

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

    1. push 是追加历史记录(默认值)
    2. replace 是替换当前记录
  • 开启 replace 模式:

    vue
    <RouterLink relpace ...>News</RouterLink>

4.11 编程式路由导航

  • 作用:脱离 <RouterLink>实现路由跳转

  • 使用:

    Vue2中使用:$route$router 获取路由对象、和路由器对象

    Vue3中使用:useRoute()useRouter() 两个 hooks获取路由对象、和路由器对象

  • API:

js
import { useRouter } from "vue-router";

// 获取路由器对象
const router = useRouter(); //useRouter()为一个hooks

// 1.使用push模式路由跳转
router.push(options) ;

// 2.使用replace模式路由跳转
router.replace(options);

// 3.前进
router.forward();

// 4.后退
router.back();

// 5.前进/后退指定步
router.go(n)

注意:optionsRouterLink标签中的to相同(可以为字符串/对象)

  • 编程式路由导航使用场景:

    1. 符合某些条件进行路由跳转(登录成功)

    2. 在一些特定标签、特定事件回调中进行路由跳转

示例:

vue
<template>
<ul>
      <!-- 传递query参数 -->
      <li v-for="item of newsList" :key="item.id">
        <button @click="showNewsDetail(item)">查看新闻</button>
        ......
      </li>
	</ul>
<!-- 展示区 -->
    <div class="news-content">
      <RouterView></RouterView>
    </div>
</template>

<script>
...
// 获取路由器实例
const router = useRouter();
// 定义一个接口限制参数类型
interface NewsInter {
  id: string;
  title: string;
  content: string;
}
// 点击按钮查看新闻
function showNewsDetail(item: NewsInter) {
  router.push({
    // punsh 内的配置对象与 RouterLink中的to相同(字符串/对象)
    name: "xiangqing",
    query: { id: item.id, title: item.title, content: item.content },
  });
}
</script>

4.12 路由重定向

  • 作用:将特定的路径、重新定向到已有路径
  • 使用:在路由配置中添加redirect:'path'
ts
	....
	{
      path: "/",
      redirect: "/home",
    },

注意:redirect 的值只能为 path 不能为 name

4.13 路由守卫

4.13.1 全局前置路由守卫

当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于等待中

使用 router.beforeEach 注册一个全局前置守卫(一般用于路由鉴权,登录判断使用)

js
router.beforeEach((to, from, next) => { 
  .....
  // 返回 false 以取消导航
  return false 
  // 将用户重定向到登录页面
  return { name: 'Login' }
})
  • to:要前往的路由对象
  • from:当前路由对象
  • next:放行函数
  • return:可以返回false: 取消当前的导航,一个路由地址
4.13.2 全局后置路由守卫

路由成功跳转后执行的钩子,可以用于改变页面标题

js
router.afterEach((to) => {
  // 设置页面标题  访问某一路由后会执行该钩子
  document.title = (setting.title.slice(0, 4) + '-' + to.meta.title) as string
  nprogress.done() //关闭进度条
})

4.14 路由缓存 + 两个全新的生命周期钩子

4.14.1 路由缓存

默认路由切换后被卸载,使用 <KeepAlive使组件不被卸载

vue
<router-view v-slot="{ Component }"> // 作用域插槽
  <keep-alive>
    <component :is="Component" /> // 动态组件
  </keep-alive>
</router-view>
4.14.2 两个路由组件的生命周期钩子
  • onActivated:被缓存的路由组件被激活时
  • onDeactivated:被缓存的路由组件失活时

4.15 滚动行为

使用前端路由,当切换到新路由时,想要页面滚到顶部,或者是保持原先的滚动位置,就像重新加载页面那样。

js
const router = createRouter({
  history: createWebHashHistory(),
  routes: [...],
  // 滚动行为(页面跳转时回到顶部)
  scrollBehavior: () => {
    return {
      top: 0,
      left: 0
    }
  }
})

五、pinia

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

  • Vue3中使用的是 pinia
  • Vue2中使用的是 Vuex
  • React中使用的是 redux

使用场景:多组件共享数据时

5.1 准备一个效果

[Missing Image: pinia_example.gif]

5.2 搭建pinia环境

第一步:安装 pinia

npm i pinia

第二步:在 src/main.ts 中安装 pinia

ts
import { createApp } from "vue";
import App from "./App.vue";

// 1.引入pinia
import { createPinia } from 'pinia';
const app = createApp(app);

//2.创建 pinia
const pinia = createPinia();

//3.安装 pinia
app.use(pinia);

app.mount("#app");

5.3 存储+读取数据

  • store 是一个保存:状态业务逻辑 的实体,每个组件都可以 读取写入
  • 它有三个概念:stategetteraction、相当于组件中的:datacomputedmethods
3.5.1 存储数据

将要存储的数据存放在 store 文件夹中,文件名与组件名保持一致。

  1. count.vue 中共享的数据存储到 store/count.ts
ts
// 1.引入 defineStore 用于创建 store
import { defineStore } from "pinia";

// 2.创建一个store用于存储count组件中的状态(数据)并暴露
export const useCountStore = defineStore("store",{
//3.储数据 state为一个函数,返回一个对象,对象内为要存储的数据
    stata(){
        return {
            sum: 6; // 为响应式数据
        }
    }
})

注意:

  1. 创建 store 时,命名使用 hooks 的命名方式 usexxxStore

  2. defineStore(id,options) 接收两个参数 id:store的唯一标识,options:配置项/setup函数

  1. LoveTalk.vue 中共享的数据存储到 LoveTalk.ts
ts
import { defineStore } from "pinia";

export const useTalkStore = defineStore("talk", {
  // 存储数据
  state() {
    return {
      talkList: [
        {
          id: "tw001",
          title: "今天你有点怪,哪里怪?怪好看的!",
        },
      ],
    };
  },
});
3.5.2 在组件中读取数据并使用
vue
<template>
<!-- 使用 -->
	<h2>当前求和为:{{ countStore.sum }}</h2>
</template>

<script>
// 获取
    
// 1.引入useCountStore
import { useCountStore } from "@/store/count";
    
// 2.调用useXxxxxStore得到对应的store
const countStore = useCountStore();// 返回值为一个 reactive 创建的响应式对象
    
// 3.获取state中的数据 方法一(常用)
console.log(countStore.sum); // sum为一个ref响应式对象
console.log(countStore.$state.sum) //方法二
</script>

Tips:访问 reactive 创建的响应式对象中 ref 创建的响应式数据时不需要加 .value

5.4 修改数据(三种方式)

  1. 第一种方式:在组件中直接修改(适用于逻辑简单的情况)

    ts
    countStore.sum += n.value;

    在组件中通过 countStore 直接修改数据,store中的数据和页面都会发生变化

  2. 第二种方式:在组件中批量修改

    ts
    countStore.$patch({
        school: "商丘学院",
        address: "开封市",
    })
  3. 第三种修改方式:借助 action 修改(action中可以编写一些业务逻辑)

    在组件中调用 actions 中的方法

    ts
    countStore.increment(n.value);

    store/count.ts

    ts
    export const useCountStore = defineStore("count", {
        
      // actions中放置的方法用于响应组件中的动作
      actions: {
        increment(value: number) {
          if (this.sum < 10) {
            this.sum += value; // this为当前store
          }
        },
      },
        
      state() {
        return {
          sum: 6,
          school: "sqxy",
          address: "商丘",
        };
      },
    });

    actions配置项内的函数的 this 指向当前 store 对象

当业务逻辑较为复杂时将逻辑性代码写在 actions 中可以简化让组件更加清晰,也提高了代码的复用

5.5 storeToRefs

store 是一个用 reactive 包装的对象,因此不能直接进行解构获取其 state但可以直接解构 action

store 实例对象是一个 reactive响应式对象,因此不能完全替换掉 store 的 state

借助 storeToRefsstore 中的数据解构为 ref 响应式对象,方便在模板中使用

vue
<template>
<h2>当前求和为:{{ sum }}</h2>
<h3>欢迎带到 {{ school }},地址 {{ address }}</h3>
...
</template>
<script>
import { useCountStore } from "@/store/count";
import { storeToRefs } from "pinia";

// 得到CountStore
const countStore = useCountStore();
// 进行解构并转化为ref响应式对象
const {sum, school, address} = storeToRefs(countStore);
// 直接进行解构会失去响应式,对数据修改就会影响到仓库中的数据
.....
</script>

注意:不要使用 toRefs 进行转换

pinia 提供的 storeToRefs 只会将 state/getter中的数据转换,而 Vue 提供的 toRefs 会将 store 中的全部属性和方法转换为 ref 对象

Tips:可以直接解构获取 storeactions 中的方法

ts
// 解构获取store中actions中的方法
const { increment } = countStore;

5.6 getters

  • 作用:当 state 中的数据,需要经过处理后再使用时,可以通过 getters配置,相当于计算属性用法与计算属性也类似

  • 使用:追加 getters配置

    ts
    export const useCountStore = defineStore("count", {
      actions: {...},
      state() {...},
      getters: {
        // 写法一:使用state参数
        bigSum(state): number {// 可以接收一个参数state:当前store对象
          return state.sum * 10;
        },
        // 写法二:使用this
        upperSchool(): string { 
          return this.school.toUpperCase();// this为当前store对象
        },
      },
    });
  • 获取

    在组件中直接通过 storeToRefs 解构 countStore 进行获取 或 countStore.xxx获取

    ts
    const { sum, bigSum, school, address } = storeToRefs(countStore);

5.7 $subscribe

通过 store$subscribe() 方法监听 state 的变化

ts
// 监听state改变
talkStore.$subscribe((mutate, state) => { // state:变化后的state
  console.log("talkStore中保存的数据发生了变化", mutate, state);
  // 当store中的数据发生变化时将数据存储到localStorage中
  localStorage.setItem("talkList", JSON.stringify(state.talkList));
});

5.8 store组合式写法(推荐)

store 选项式写法:defineStore的第二个参数为一个配置对象

ts
export const useTalkStore = defineStore("talk", {
  // 方法
  actions: {
    async getATalk() {.....},
  }
    
  // 数据
  state() {
    return {
      talkList: ....
    };
  },
  
  // getters
   getters:{
       bingSum(){
          return ...
       }
   }
});

store 组合式写法:defineStore的第二个参数为一个 setup 函数

ts
export const useTalkStore = defineStore("talk", () => {
  // state:使用ref/reactive 定义的响应式数据
  const talkList = ref(....) 
  
  // actions:function定义的方法
  async function getATalk() {....}
    
  // getters:computed定义的计算属性
  const bigSum = computed(() => {
    return sum.value * 10;
  });
    
  // 暴露数据
  return { talkList, getATalk };
});

推荐使用组合式,无需使用this就可以直接在函数中访问到数据,且没有太多层级

注意:使用组合式时必须对数据和方法进行return暴露。

拓展 持久化存储pinia中的数据

持久化存储pinia中的数据的方案:

  1. 使用 localStroge.setItem().....
  2. 使用插件:pinia-plugin-persistedstate

pinia-plugin-persistedstate使用步骤:

  1. 安装:

    pnpm i pinia-plugin-persistedstate
  2. 在pinia 中使用插件

    ts
    import { createPinia } from 'pinia'
    + import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
    
    const pinia = createPinia()
    + pinia.use(piniaPluginPersistedstate)
  3. 自动存储 store 中的数据

    创建 Store 时,将 persist 选项设置为 true

    ts
    import { defineStore } from 'pinia'
    import { ref } from 'vue'
    
    export const useStore = defineStore(
      'main',
      () => {
        const someState = ref('你好 pinia')
        return { someState }
      },
    +  {
    +    persist: true,
    +  },
    )
  4. 存储数据,清除数据

    当设置仓库中属性值时,会自动将该属性存储到浏览器本地存储中

    存储格式:
    store名       {"属性名":"值"}

    当将属性值修改为 undefined 时,会将该属性值从浏览器本地存储中清除

六、组件通信

6.1 vue3组件通信与 Vue2的区别:

  1. 移除了全局事件总线$bus,使用 mitt 代替
  2. vuex换成了 pinia
  3. .sync 优化到了 v-model
  4. $listeners 所有的东西,合并到了 $attrs
  5. $children 被砍掉了

常见搭配形式:

image-20240418171243431

6.1 props

作用:props 是使用频率最高的一种通信方式,常用于:父组件 ↔ 子组件

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

    父组件通过 props 将数据直接传递给子组件,子组件通过 definProps进行接收即可在模板中使用

  • 子传父:属性值是 函数

    父组件给子组件通过 props 传递一个函数,子组件在合适的时候调用,通过参数的形式将数据传递给父组件

父组件:

vue
<template>
  <div class="father">
    <h3>父组件</h3>
    <h4>汽车:{{ car }}</h4>
    <h4 v-show="toy">玩具: {{ toy }}</h4>
	<!-- 父组件向子组件传递数据和方法(使用v-bind) -->
    <Child :car="car" :sendToy="getToy" />
  </div>
</template>

<script setup lang="ts" name="Father">
import { ref } from "vue";
import Child from "./Child.vue";

// 数据
let car = ref("奔驰");
let toy = ref("");

// 方法
function getToy(value: string) {
  console.log("父组件收到子组件的值:", value);
  toy.value = value;
}
</script>

子组件:

vue
<template>
  <div class="child">
    <h3>子组件</h3>
    <h4>玩具:{{ toy }}</h4>
    <!-- 将父组件传递的数据进行展示 -->
    <h4>父给的车:{{ car }}</h4>
    <!-- 子组件调用父组件中的方法将数据传递给父组件 -->
    <button @click="sendToy(toy)">把玩具给父亲</button>
  </div>
</template>

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

// 数据
let toy = ref("奥特曼");

// 声明接收props
const props = defineProps(["car", "sendToy"]);
</script>

注意:在 defineProps中接收到的数据只能在模板中使用,若要在 JS 代码中使用需要进行接收

单向数据流:(不可进行修改)

所有的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。

每次父组件更新后,所有的子组件中的 props 都会被更新到最新值,这意味着不应该在子组件中去更改一个 prop。

6.2 自定义事件

作用:自定义事件常用于:子组件 => 父组件 间的通信

原生事件与自定义事件的区别:

  • 原生事件:
    • 事件名是特定的(clickmosueenter等等)
    • 事件对象$event: 是包含事件相关信息的对象(pageXpageYtargetkeyCode
  • 自定义事件:
    • 事件名是任意名称(推荐使用 kebab-case 命名)
    • 事件对象$event: 是调用emit时所提供的数据,可以是任意类型!!!

示例:

父组件:为子组件标签绑定自定义事件,并将回调留在父组件中,子组件在合适的时候触发自定义事件。

vue
<template>
  <div class="father">
    <h3>父组件</h3>
    <h4 v-show="toy">子组件给我的玩具 {{ toy }}</h4>
    <!-- 为子组件Child绑定多个自定义事件 -->
    <Child @send-toy="getToy" @test="test" />
  </div>
</template>

<script setup lang="ts" name="Father">
import { ref } from "vue";
import Child from "./Child.vue";

let toy = ref("");

// 将事件回调留在父组件中(用于获取数据)
function getToy(value: string) {
  toy.value = value;
}
function test(){
	console.log('test')
}
</script>

子组件:使用 defineEmits 进行声明自定义事件,并在合适的时候 使用 emit 进行调用

vue
<template>
  <div class="child">
    <h3>子组件</h3>
    <h4>玩具:{{ toy }}</h4>
    <!-- 在模板中使用 emit触发自定义事件,并传递数据-->
    <button @click="emit('send-toy', toy)">给父组件玩具</button>
    <button @click="emit('test')">test</button>
  </div>
</template>

<script setup lang="ts" name="Child">
import { onMounted, ref } from "vue";
// 数据
let toy = ref("奥特曼");

// 声明自定义事件,可以在任意时刻进行调用
const emit = defineEmits(["send-toy", "test"]); // 可以声明接收多个自定义事件
// 使用emit或$emit  固定写法

// 组件挂载三秒后给父组件玩具
onMounted(() => {
  setTimeout(() => {
    emit("sendToy", toy.value); // 触发自定义事件,并传递数据
  }, 3000);	
}); 
</script>

Tips:自定义事件名推荐使用 kebab-case 命名

注意:使用emit$emit 存储自定义事件,固定写法

6.3 mitt

描述:与全局事件总线 $bus 和消息订阅与发布(pubsub)功能类似,可以实现任意组件间通信

使用:

  1. 安装 mittnpm i mitt

  2. 新建文件:src\utils\emitter.ts

    创建并暴露 emitter

    ts
    // 引入mitt
    import mitt from "mitt";
    // 调用mitt()得到一个emitter对象并暴露:可以进行绑定、触发、解绑事件
    const emitter = mitt()
    export default emitter

emitter 身上的 API

  • emitter.on("事件名", 回调); 绑定事件
  • emitter.emit("事件名", [数据]); 触发事件并传递参数
  • emitter.off("事件名"); 解绑事件
  • emitter.all.clear(); 获取全部绑定事件并解绑
  1. 在需要获取数据的组件中引入 emitter 并绑定事件,并在组件被卸载时解绑事件

    vue
    <template>
      <div class="child2">
        ....
        <h4 v-show="toy">收到了哥哥的玩具:{{ toy }}</h4>
      </div>
    </template>
    
    <script setup lang="ts" name="Child2">
    // 引入emitter
    import emitter from "@/utils/emitter";
    import { onUnmounted, ref } from "vue";
    
    // 数据
    let computer = ref("XXX电脑");
    let toy = ref("");
    
    // 给emitter绑定send-toy事件
    onMounted(()=>{
      emitter.on("send-toy", (value: any) => {
      	 toy.value = value; // 将获取到的数据进行保存
      });
    })
    
    // 当组件被卸载时,解绑在emitter上绑定的事件
    onUnmounted(() => {
      emitter.off("send-toy");
    });
    </script>
  2. 提供数据的组件,在合适的时候触发事件

    vue
    <template>
      <div class="child1">
        ...
    <!-- 通过触发emitter中绑定的事件将数据传递给绑定事件的组件 -->
        <button @click="emitter.emit('send-toy', toy)">玩具给弟弟</button>
      </div>
    </template>
    
    <script setup lang="ts" name="Child1">
    // 引入emitter
    import emitter from "@/utils/emitter";
    import { ref } from "vue";
    
    let toy = ref("奥特曼");
    </script>

注意:

  1. 在组件被卸载时 onUnmounted 钩子内,将绑定的事件进行解绑
  2. 哪个组件需要数据就在该组件中绑定事件,在提供数据的组件中触发事件

6.4 v-model

描述:实现 父↔子 组件之间相互通信

  1. v-model用在 input 标签上,实现数据双向绑定

    vue
    <input type="text" v-model="username" />
    
    < v-model 底层原理 动态数据绑定+input事件>
    <input 
        type="text" 
        :value="username"
        @input="username = (<HTMLInputElement>$event.target).value"
    />
  2. v-model 用在组件标签上,实现父↔子组件通信

    父组件:

    vue
    <templete>
    <AtguiguInput v-model="username" />
    
    <!-- v-model 底层原理 -->
    <AtguiguInput
        :modelValue="username" // props传递数据
        @update:modelValue="username = $event" // 自定义方法修改username的值
    />
    </templete>
    
    <script setup lang="ts" name="Father">
    import { ref } from "vue";
    import AtguiguInput from "./AtguiguInput.vue";
    // 数据
    let username = ref("张三");
    </script>

    子组件(UI组件):

    vue
    <template>
      <input
        type="text"
        :value="modelValue"
        @input="emit('update:modelValue',($event.target as HTMLInputElement).value)"
      />
    </template>
    
    <script setup lang="ts" name="AtguiguInput">
    // 父组件在子组件标签上添加v-model 子组件就可以接收到modelValue属性和update:modelValue事件
    defineProps(["modelValue"]); // 获取父组件传递的数据
    const emit = defineEmits(["update:modelValue"]) // 获取自定义事件,实现向父组件传递数据
    </script>
  3. 更换 modelValue并为组件绑定多个 v-model

    父组件:

    vue
    <!-- 修改modelValue -->
    <AtguiguInput v-model:name="username" v-model:psd="password"/>

    子组件(UI 组件):

    vue
    <template>
      <input
        type="text"
        :value="name"
        @input="emit('update:name', ($event.target as HTMLInputElement).value)"
      />
      <input
        type="password"
        :value="psd"
        @input="emit('update:psd', (<HTMLInputElement>$event.target).value)"
      />
    </template>
    
    <script setup lang="ts" name="AtguiguInput">
    // 获取父组件使用v-model传递的数据和自定义事件
    defineProps(["name", "psd"]); // 获取父组件传递的数据
    const emit = defineEmits(["update:name", "update:psd"]); // 获取自定义事件,实现向父组件传递数据
    </script>

注意:$event 到底是什么?啥时候能用 .target

  1. 对于原生事件,$event就是事件对象,可以使用 .target 获取触发事件的 DOM 元素
  2. 对于自定义事件,$event 就是触发事件时,所传递的数据(参数),不可以使用 .target

Tips:v-model 绑定的组件标签一般为 UI 组件标签

6.5 $attrs

  1. 描述:$attrs 用于实现当前组件的父组件 ,向当前组件的子组件通信(祖→孙
  2. 具体说明:$attrs是一个对象,包含未被当前组件接收的父组件传递的数据

注意:$attrs会自动排除当前组件使用 defineProrps接收的属性

Tips:v-bind 的对象写法 v-bind="{a:123,b:456}" === :a="123" :b="456"

  1. 使用:

    父组件:

    vue
    <template>
      <div class="father">
        <h3>父组件</h3>
        <h4>a:{{ a }}</h4>
        <h4>b:{{ b }}</h4>
        <h4>c:{{ c }}</h4>
        <h4>d:{{ d }}</h4>
        <!-- v-bind="{ x: 100, y: 200 }" === :x="100" :y="200" -->
       <!-- 将数据传递给子组件 -->
        <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 { ref } from "vue";
    import Child from "./Child.vue";
    let a = ref(1);
    let b = ref(2);
    let c = ref(3);
    let d = ref(4);
    // 实现孙组件和父组件通信
    function updateA(value: number) {
      a.value += value;
    }
    </script>

    子组件:

    vue
    <template>
      <div class="child">
        <h3>子组件</h3>
        <!-- 父组件使用props传递给子组件的属性,子组件未接收的就被存储在$attrs中 -->
        <!-- <h4>{{ $attrs }}</h4> -->
    	<!-- 将子组件未接收的数据传递给孙组件 -->
        <GrandChild v-bind="$attrs"/>
      </div>
    </template>

    孙组件:

    vue
    <template>
      <div class="grand-child">
        <h3>孙组件</h3>
        <h4>a:{{ a }}</h4>
        <h4>b:{{ b }}</h4>
        <h4>c:{{ c }}</h4>
        <h4>d:{{ d }}</h4>
        <h4>x:{{ x }}</h4>
        <h4>y:{{ y }}</h4>
        <button @click="updateA(1)">点我将爷爷组件的a值加1</button>
      </div>
    </template>
    
    <script setup lang="ts" name="GrandChild">
    // 接收父组件使用 v-bind="$attrs" 传递的的数据
    defineProps(["a", "b", "c", "d", "x", "y", "updateA"]);
    </script>

    本质:父组件通过 props 传递给子组件数据,子组件再通过 v-bind="$attrs" 将未接收的数据通过 props 传递给孙组件

6.6 $refs $parent

  1. 描述:

    $refs用于:父→子 (通过 $refs 获取到全部使用 ref属性绑定的子组件实例对象)

    $parent用于:子→父(通过 $parent 获取当前组件的父组件的实例对象)

  2. 原理:

    属性说明
    $refs值为对象,包含所有ref 属性标识的 DOM 元素或组件实例对象
    $parent值为对象,当前组件的父组件的实例对象
  3. 回顾 ref 在组件上使用

    vue
    <template>
      <div class="father">
        <h3>父组件</h3>
        <button @click="changeToy">点我修改Child1的玩具</button>
        <button @click="changeComputer">点我修改Child2的电脑</button>
        
    	<!-- 为子组件添加ref属性 -->
    	<Child1 ref="c1" />
        <Child2 ref="c2" />
      </div>
    </template>
    
    <script setup lang="ts" name="Father">
    import { ref } from "vue";
    import Child1 from "./Child1.vue";
    import Child2 from "./Child2.vue";
    
    // 获取子组件的实例对象
    let c1 = ref();
    let c2 = ref();
    
    // 修改子组件中的数据,实现父->子的组件通信
    function changeToy() {
      console.log(c1.value);
      // c1.value 获取子组件的实例对象(可以访问到子组件通过defneExpose暴露的数据)
      c1.value.toy = "小猪佩奇"; 
    }
    function changeComputer() {
      // c2 为Child2组件的实例对象
      c2.value.computer = "DELL";
    }
    // 向外部提供数据
    defineExpose({ house });
    </script>
  4. $refs

    在事件回调中传递 $refs 获取 ref 标识的全部子组件实例对象

    vue
    <template>
      <div class="father">
        <button @click="getAllChild($refs)">让所有孩子的书变多</button>
        <!-- $refs作为事件回调的参数进行传递,$refs是一个对象包含全部使用ref标识的子组件的实例对象 -->
        <!-- 为子组件添加ref属性 -->
        <Child1 ref="c1" />
        <Child2 ref="c2" />
      </div>
    </template>
    
    <script setup lang="ts" name="Father">
    import Child1 from "./Child1.vue";
    import Child2 from "./Child2.vue";
    
    // 修改子组件中的数据,实现父->子的组件通信
    function getAllChild(refs: object) {
      console.log(refs); // Proxy(Object) {c1: Proxy(Object), c2: Proxy(Object)}
      for (const ref of Object.values(refs)) {
        ref.book += 3;
      }
    }
    // 向外部提供数据
    defineExpose({ house });
    </script>
  5. $parent

    在事件回调中传递 $parent 获取当前组件的父组件的实例对象

    vue
    <template>
      <div class="child1">
        <h3>子组件1</h3>
        <button @click="minusHouse($parent)">干掉父亲的一套房产</button>
      </div>
    </template>
    
    <script setup lang="ts" name="Child1">
    import { ref } from "vue";
    
    let toy = ref("奥特曼");
    let book = ref(3);
    
    // 修改父组件中的数据,实现 子组件->父组件间的通信
    function minusHouse(parent: any) {
      console.log(parent);
    	// 在子组件中操作父组件的数据
      parent.house--;
    }
    
    // 使用defineExpose 暴露数据供付组件使用ref进行访问
    defineExpose({ toy, book });
    </script>

注意:

  1. 要想通过 ref 获取的子组件实例对象访问子组件上的数据,需要在子组件中通过 defineExpose 暴露数据
  2. $refs$parent 只能事件回调中作为参数使用

Tips:读取 ref 定义的响应式数据是否需要 .value

  1. 读取 reactive 定义的响应式对象内 ref 定义的属性,不需要 .value
  2. 读取 ref 直接定义的响应式数据必须 .value
ts
let obj = reactive({
  a: 1,
  b: 2,
  c: ref(3),
});

console.log(obj.a, obj.b);
console.log(obj.c); // 1.读取reactive定义的响应式对象内的ref定义的属性,不需要.value

let x = ref(4);
console.log(x.value);// 2.读取ref直接定义的响应式对象必须.value

6.7 provide inject

描述:实现 祖↔任意后代组件 直接通信,无需借助父组件

具体使用:

  1. 在祖先组件中通过 provide 配置向后代提供数据
  2. 在任意后代组件中通过 inject 配置来声明接收数据

祖 -> 孙:祖先为其提供数据,孙 ->祖:祖先为其提供方法

具体编码:

  1. 在父组件中使用 provide提供数据

    语法:provide(名, 值)

    vue
    <script setup lang="ts" name="Father">
    import { provide, reactive, ref } from "vue";
    import Child from "./Child.vue";
    
    // 数据
    let money = ref(100);
    let car = reactive({
      brand: "奔驰",
      price: "199",
    });
    // 方法
    function updateMoney(value: number) {
      money.value -= value;
    }
    
    // 向后代提供数据和方法,ref定义的数据不要加.value
    provide("moneyContext", { money, updateMoney });
    provide("car", car);
    </script>
  2. 在后代组件中使用 inject 配置接收数据

    语法:inject(名,[默认值])

    vue
    <template>
      <div class="grand-child">
        <h3>我是孙组件</h3>
        <h4>银子{{ money }}w</h4>
        <h4>车子:一辆{{ car.brand }}价值{{ car.price }}w</h4>
        <button @click="updateMoney(10)">花爷爷的money</button>
      </div>
    </template>
    
    <script setup lang="ts" name="GrandChild">
    import { inject } from "vue";
    
    // inject 获取祖先组件传递的值,可以使用默认值进行类型的推断
    let { money, updateMoney } = inject("moneyContext", {
      money: 0,
      updateMoney: (n: number) => {},
    });
    console.log(money); // 获取的是Ref响应式对象,当祖先组件的数据发生改变了,这里也会自动更新
    let car = inject("car", { brand: "未知", price: 0 });
    </script>

注意:

  1. 在使用 provide 提供数据时,要想 inject 接收到的数据响应父组件数据的变化就不能传递具体的值(eg:.value.xx接收到为一个值,而不是响应式数据),应为 xx或xx.value这样子组件接收到的为一个响应式数据(原因:组件中的响应式数据发生变化会重新解析模板,但不会再次执行 js 代码,不会再次注入新的数据)
  2. 在子组件中出现类型检查错误时,可以通过设置默认值的方式解决

6.8 pinia

五、pinia

6.9 slot

作用:让父组件可以向子组件指定位置插入 html结构,也是一种组件间通信的方式,适用于 父组件 <==> 子组件

分类:默认插槽、具名插槽、作用域插槽

使用场景:父组件向子组件传递带数据的标签,当一个组件有不确定的结构时, 就需要使用slot 技术

注意:

  1. 组件标签要写成双标签

  2. 插槽内容是在父组件中编译后,再传递给子组件的,因此插槽内元素的样式要写在父组件中。

image-20240420174651400

6.9.1 默认插槽

父组件中:

在组件标签中添加要传递给子组件的 html 代码

vue
<template>
  <div class="father">
    <h3>父组件</h3>
    <div class="content">
<!-- 插槽内容 -->
      <Category title="热门游戏列表">
        <ul>
          <li v-for="game of games" :key="game.id">{{ game.name }}</li>
        </ul>
      </Category>

      <Category title="今日美食城市">
        <img :src="" data-missing="imgURL" alt="" />
      </Category>

      <Category title="今日影视推荐">
        <video :src="" data-missing="videoURL" controls></video>
      </Category>
    </div>
  </div>
</template>

<script setup lang="ts" name="Father">
import { reactive, ref } from "vue";
import Category from "./Category.vue";
// 数据
let games = reactive([
  { id: "asv1", name: "王者荣耀" },
  { id: "asv2", name: "英雄联盟" },
  { id: "asv3", name: "QQ飞车" },
  { id: "asv4", name: "地下城与勇士" },
]);
let imgURL = ref("https://z1.ax1x.com/2023/11/19/piNxLo4.jpg");
let videoURL = ref("http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4");
</script>
// 样式
<style scoped>
.father {
  background-color: rgb(165, 164, 164);
  padding: 20px;
  border-radius: 10px;
}
.content {
  display: flex;
  justify-content: space-evenly;
}
img,
video {
  width: 100%;
}
</style>

子组件中:

使用 slot 标签指定 html 代码展示的位置

vue
<template>
  <div class="category">
    <h2>{{ title }}</h2>
<!-- 指定插槽的位置 -->
    <slot>默认内容</slot>
  </div>
</template>

<script setup lang="ts" name="Category">
defineProps(["title"]);
</script>

注意:组件标签内的html 代码是在父组件中编译完成再传递给子组件的,因此样式需要写在父组件的 <style>

6.9.2 具名插槽

为每个插槽 <solt name="xxx"> 指定一个 name 属性用来区分不同的插槽,在父组件中使用 <template v-slot:name>来指定所要使用的插槽

父组件:

vue
<template>
  <div class="father">
    <h3>父组件</h3>
    <div class="content">
      <Category>
<!-- 将<template>内的内容放在<slot name="s1">标签处 -->
        <template v-slot:s1>
          <h2>热门游戏列表</h2>
        </template>
<!-- 将<template>内的内容放在<slot name="s2">标签处 -->
        <template v-slot:s2>
          <ul>
            <li v-for="game of games" :key="game.id">{{ game.name }}</li>
          </ul>
        </template>
      </Category>

      <Category>
<!-- 将<template>内的内容放在<slot name="s1">标签处 -->
        <template #s1>
          <h2>今日影视推荐</h2>
        </template>
<!-- 将<template>内的内容放在<slot name="s2">标签处 -->
        <template #s2>
          <video :src="" data-missing="videoURL" controls></video>
        </template>
      </Category>
    </div>
  </div>
</template>

子组件:

vue
<template>
  <div class="category">
    <!-- 指定插槽的位置 -->
    <slot name="s1">标题</slot>
    <hr>
    <!-- 具名插槽 -->
    <slot name="s2">默认内容</slot>
  </div>
</template>

注意:v-slot 只能使用在 <template> 标签或组件标签

Tips:v-slot 语法糖:<template v-slot:xx> === <template #xx>

6.9.3 作用域插槽

描述:数据在子组件中,但结构需要根据数据在父组件中生成(即数据在子组件中,父组件使用插槽生成的结构需要子组件中的数据) 子组件 ==> 父组件

此时会产生数据的作用域问题,需要使用作用域插槽来实现

使用:

  1. 子组件:

    <slot> 标签中使用 props 将数据传递给插槽的使用者

vue
<template>
  <div class="game">
    <h2>热门游戏列表</h2>
    <!-- slot 将数据传递给插槽的使用者 -->
    <slot name="qwe" :games="games" a="哈哈我是作用域插槽"></slot>
  </div>
</template>

<script setup lang="ts" name="Game">
import { reactive } from "vue";
// 数据
let games = reactive([
  { id: "asv1", name: "王者荣耀" },
  { id: "asv2", name: "英雄联盟" },
  { id: "asv3", name: "QQ飞车" },
  { id: "asv4", name: "地下城与勇士" },
]);
</script>
  1. 父组件:通过在<template v-slot="xx"> 接收 <slot> 传递的数据
vue
<Game>
<!-- <template v-slot="xx"> 接收solt标签传递的props xx为一个包含所有props属性的对象 -->
  <template #qwe="params">
<!-- <template #qwe="{games, a}"> -->
    <span>{{ params }}</span><!-- {games: Array(4), a: '哈哈我是作用域插槽'} -->
     <ul>
         <li v-for="game of params.games" :key="game.id">{{ game.name }}
         </li>
     </ul>
  </template>
</Game>

v-slot:"xx"v-slot="xx" 的区别:

  • v-slot:"xx"用于具名插槽中,用于区分不同的插槽,可以简写为 #xx

  • v-slot="xx" 用于接收子组件的 <slot> 标签传递的数据

注意:

  1. v-slot必须写在 <template> 标签中

  2. v-slot="xx"xx为一个包含传递所有数据的对象,也可以对其进行解构

  3. 作用域插槽 v-slot="xx" 和具名插槽 v-slot:xx 一起使用时的写法

    vue
    <template v-slot:name="params"> 或
    <template #name="params"> 
    <!-- name为slot的名字,params为slot传递的数据-->
    ....
    </template>

七、其他API

7.1 shallowRef 与 shallowReactive

7.1.1 shallowRef
  1. 作用:创建一个响应式数据,但只对顶层属性进行响应式处理

  2. 用法:

    js
    let shallowPerson = shallowRef({...})
  3. 特点:只跟踪引用值的变化,不关心值内部数据变化

7.1.2 shallowReactive
  1. 作用:创建一个浅层响应式对象,只会使对象的最顶层属性变成响应式的,对象内部的嵌套属性则不会变成响应式的

  2. 用法:

    js
    let shallowPerson = shallowReactive({...})
  3. 特点:对象的顶层属性是响应式的,但嵌套对象的属性不是

总结:

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

image-20240422123729832

7.2 readonly 与 shallowReadonly

7.2.1 readonly
  1. 作用:创建一个响应式数据深只读副本

  2. 用法:

    js
    const sum1 = ref(0);
    const sum2 = readonly(sum1);// readonly的参数必须为一个响应式数据
    // sum2会随着sum1的变化而变化
    const person1 = reactive({name:"张三",age:18})
    const person2 = readonly(person1)
    
    sum2.value = 1;// 无法赋值因为sum2是只读的
    person2.name = "李四" // 无法赋值因为person2是只读的
  3. 特点:

    • 对象的所有嵌套属性都变为只读
    • 任何尝试修改这个对象的操作都会被阻止
    • const s2 = readonly(s1)s1 发生改变时,s2 也会随之改变
  4. 应用场景:

    • 创建不可变的状态快照
    • 保护全局状态或配置不被修改
7.2.2 shallowReadonly
  1. 作用:与 readonly类似,但只作用于对象的顶层属性

  2. 用法:

    js
    const car1 = reactive({
        brand:"奔驰",
        options:{
            color:"pink"
            price:18
        }
    })
    const car2 = shallowReadonly(car1)
    
    car2.brand = "小鸟" // 不可修改
    car2.options.color = "black" // 可以修改
  3. 特点:

    • 只将对象的顶层属性(对象内的一级属性)设置为只读,对象内部的嵌套属性仍然是可变的
    • 适用于只需要保护对象顶层属性的场景

7.3 toRaw 与 markRow

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

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

  2. 使用:

    js
    const person = reactive({
      name: "Tony",
      age: 18,
    });
    // 用于获取一个响应式对象的原始对象
    let person2 = toRaw(person);
    console.log("响应式数据", person); // Proxy(Object) {name: 'Tony', age: 18}
    console.log("原始数据", person2); // {name: 'Tony', age: 18}

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

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

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

  2. 使用:

    js
    // markRaw:标记一个普通对象,使其永远不会变成响应式对象
    let car = markRaw({
      brand: "奔驰",
      price: 100,
    });
    let car2 = reactive(car);
    console.log("car:", car); // {brand: '奔驰', price: 100}
    console.log("car2:", car2); // {brand: '奔驰', price: 100}

7.4 customRef

  1. 作用:创建一个自定义 ref ,当自定义响应式数据发生变化时可以增加一些逻辑的控制

  2. 使用:

    利用 customRef 实现响应式数据发生变化后一秒页面再进行更新

js
// 使用vue默认提供的默认ref定义响应式数据,数据一变化,页面就更新
let msg = ref("你好");

// 使用自定义ref去定义响应式数据
let initValue = "你好"
let timer:number
// track:跟踪  trigger:触发
let msg = customRef((track,trigger)=>{
    return{
        get(){// 当msg被读取时调用
            track() //告诉Vue数据msg很重要,要对msg进行持续关注,一旦msg发生变化就去更新
            return initValue
        }
        set(value){
            // 实现数据修改一秒后再更新数据
            clearTimeout(timer)
            timer = setTimeout(()=>{
                initValue = value // 修改数据
                trigger() // 通知Vue数据msg变化了
            },1000)
        }
    }
})

注意:track()trigger()的作用

  • track():告诉 Vue 自定义的响应式数据很重要,要对其持续关注,一旦发生变化就去更新
  • trigger():通知 Vue 自定义响应式数据变化了
  1. customRef 定义的响应式数据 msg 封装为一个 hooks

    useMsgRef.ts

ts
// 将自定义ref msg 封装为一个hooks
import { customRef } from "vue";

export default function (initValue: string, delay: number) {
  // 使用自定义ref(customRef)去定义响应式数据
  let timer: number;
  // track:跟踪  trigger:触发
  let msg = customRef((track, trigger) => {
    return {
      // get何时被调用:msg被读取时调用
      get() {
        track(); // 告诉Vue数据msg很重要,你要对msg进行持续关注,一旦msg变化就去更新
        return initValue;
      },
      // set何时被调用:msg被修改时调用
      set(value) {
        console.log("msg被修改了", value);
        // 实现一秒钟后再更新数据
        clearTimeout(timer);
        timer = setTimeout(() => {
          initValue = value; // 修改数据
          trigger(); // 通知Vue数据msg变化了
        }, delay);
      },
    };
  });
  return { msg };
}

​ 使用:

js
// 使用useMsgRef来定义一个响应式数据且有延迟效果
const { msg } = useMsgRef("你好", 1000);

八、Vue3新组件

8.1 Teleport

Teleport 是一种能将我们组件中的html结构移动到指定位置的技术

用法:<teleport to="xx">html</teleport> xx 可以为任意标签选择器

使用场景:当组件内的html结构受到了其他原因的影响,必须移动到指定的位置时,就可以使用 Teleport 标签

eg:使用 Teleport解决 CSS 滤镜影使 fix 定位不参照浏览器页面的问题

css
filter: saturate(0%); /* 网站置灰  滤镜*/
/* filter: saturate(200%); 色彩增强*/
vue
<template>
  <button @click="isShow = true">展示弹窗</button>
  <!-- 由于App组件使用了滤镜导致弹窗的定位不参照浏览器页面 -->
  <teleport to='body' > <!-- 使用Teleport使指定html结构传送到指定位置 -->>
    <!-- teleport传送门 to可以为CSS选择器 -->
    <div v-show="isShow" class="modal">
    <h2>我是弹窗的标题</h2>
    <p>我是弹窗的内容</p>
    <button @click="isShow = false">关闭弹窗</button>
  </div>
  </teleport>
</template>

8.3 suspense

作用:等待异步组件时渲染一些额外内容,让应用有更好的用户体验

使用场景:子组件中有异步任务,且在网速慢加载时,让页面显示一些东西时可以使用 suspense

使用步骤:

  • 使用 Suspense 包裹有异步任务的组件,并配置好 defaultfallback两个插槽

    default:用于展示异步组件

    fallback:用于异步组件未加载出来时进行展示

Child.vue 子组件:存在异步任务需要等请求成功后才会渲染页面

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

let sum = ref(0);
// 存在异步任务 await需要写在async函数中,setup自带async函数
let {data: { content },} = await axios.get("https://api.uomg.com/api/rand.qinghua?format=json");
console.log(content);
</script>

App.vue 父组件:使用 suspense 在等待异步组件时渲染一些额外内容,让应用有更好的用户体验

vue
<template>
  <div class="app">
    <h2>我是App组件</h2>
    <!-- 使用 Suspense 组件包裹子组件,当子组件加载失败时,会显示一个loading组件 -->
    <Suspense>
      <!-- 内部有两个插槽,default和fallback -->
      <template #default>
        <Child />
      </template>
      <template #fallback>
        <h2>加载中....</h2>
      </template>
    </Suspense>
  </div>
</template>

8.3 全局 API 转移到应用对象

Vue2 中的 Vue.xx 变成了 app.xx

API作用描述
app.component注册全局组件在任意一个组件中都可以直接该组件标签使用
app.config全局配置对象设置全局的配置
app.directive注册全局种指令使自定义指令在全局可用
app.mount挂载应用将应用挂载到指定的html元素的位置
app.unmount卸载应用
app.use安装插件使用 Router,Pinia

8.4 其他

非兼容性改变

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

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

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

  • v-ifv-for 在同一个元素身上使用时的优先级发生了变化。v-if优先级比 v-for更高,因此不能使用 v-if来控制 v-for渲染元素,推荐使用 v-show

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

  • 移除了过滤器 filter

  • 移除了$children 实例 propert

    ......