vue3 + ts 快速入门(全)

文章目录

  • 学习链接
  • 1. Vue3简介
    • 1.1. 性能的提升
    • 1.2.源码的升级
    • 1.3. 拥抱TypeScript
    • 1.4. 新的特性
  • 2. 创建Vue3工程
    • 2.1. 基于 vue-cli 创建
    • 2.2. 基于 vite 创建(推荐)
      • vite介绍
      • 创建步骤
      • 项目结构
        • 安装插件
        • 项目结构
        • 总结
    • 2.3. 一个简单的效果
      • Person.vue
      • App.vue
  • 3. Vue3核心语法
    • 3.1. OptionsAPI 与 CompositionAPI
      • Options API 的弊端
      • Composition API 的优势
    • 3.2. 拉开序幕的 setup
      • setup 概述
      • setup 的返回值
      • setup 与 Options API 的关系
      • setup 语法糖
    • 3.3. ref 创建:基本类型的响应式数据
    • 3.4. reactive 创建:对象类型的响应式数据
    • 3.5 ref 创建:对象类型的响应式数据
    • 3.6. ref 对比 reactive
      • 宏观角度
      • 区别
      • 使用原则
    • 3.7 toRefs 与 toRef
      • 现象
      • toRefs&toRef的使用
    • 3.8 computed
    • 3.9 watch
      • 作用
      • 特点
      • 场景
        • * 情况一
        • * 情况二
          • 示例1
          • 示例2
        • * 情况三
        • * 情况四
          • 没有监视的代码
          • 监视reactive定义的对象类型中的某个基本属性
          • 监视reactive定义的对象类型中的某个对象属性
        • * 情况五
    • 3.10 watchEffect
    • 3.11. 标签的 ref 属性
      • 用在普通DOM标签上
      • 用在组件标签上(defineExpose)
    • 3.12 回顾TS
      • main.ts
      • App.vue
      • index.ts
      • Person.vue
    • 3.13 props(defineProps)
      • App.vue
      • index.ts
      • Person.vue
    • 3.14 生命周期
      • App.vue
      • Person.vue
    • 3.15 自定义hooks
      • 未使用hooks前
        • App.vue
        • Person.vue
      • 使用hooks
        • App.vue
        • Person.vue
        • hooks/useSum.ts
        • hooks/useDog.ts
  • 4.路由
    • 4.1 路由的基本理解
    • 4.2 基本切换效果
      • 安装vue-router
      • 配置路由规则router/index.ts
      • 使用router路由管理器main.ts
      • 路由展示区App.vue
      • 路由组件
        • Home.vue
        • New.vue
        • About.vue
      • 路由切换效果图
    • 4.3. 两个注意点
      • About.vue
    • 4.4. 路由器工作模式
    • 4.5. to的两种写法
    • 4.6. 命名路由
    • 4.7 嵌套路由
      • main.ts
      • router/index.ts
      • App.vue
      • News.vue
      • Detail.vue
      • 效果
    • 4.8 路由传参
      • query参数
      • params参数
    • 4.9 路由的props配置
    • 4.10 replace属性
      • 示例
    • 4.11 编程式导航
      • 示例
    • 4.12 重定向
      • 示例
  • 5. pinia
    • 5.1 准备一个效果
      • main.ts
      • App.vue
      • Count.vue
      • LoveTalk.vue
    • 5.2 搭建 pinia 环境
      • 使用步骤
    • 5.3 存储+读取数据
      • store/count.ts
      • store/loveTalk.ts
      • Count.vue
      • LoveTalk.vue
      • App.vue
      • main.ts
    • 5.4 修改数据(三种方式)
      • 第一种方式
        • count.ts
        • Count.vue
      • 第二种方式
        • count.ts
        • Count.vue
      • 第三种方式
        • count.ts
        • Count.vue
    • 5.5 storeToRefs用法
      • LoveTalk.ts
      • LoveTask.vue
      • count.ts
      • Count.vue
    • 5.6 getters用法
      • count.ts
      • Count.vue
    • 5.7 $subscribe的使用
      • loveTalk.ts
      • LoveTalk.vue
    • 5.8 store组合式写法
      • loveTalk.js
      • LoveTalk.vue
  • 6. 组件通信
    • 6.1 props
      • Father.vue
      • Child.vue
    • 6.2 自定义事件
      • Father.vue
      • Child.vue
    • 6.3 mitt
      • emitter.ts
      • Father.vue
      • Child1.vue
      • Child2.vue
    • 6.4 v-model
      • Father.vue
      • AtguiguInput.vue
    • 6.5 $attrs
      • Father.vue
      • Child.vue
      • GrandChild.vue
    • 6.6 r e f s 、 refs、 refsparent、proxy
      • Father.vue
      • Child1.vue
      • Child2.vue
    • 6.7 provide、inject
      • Father.vue
      • Child.vue
      • GrandChild.vue
    • 6.8 pinia
    • 6.9 slot插槽
      • 1. 默认插槽
        • Father.vue
        • Category.vue
      • 2. 具名插槽
        • Father.vue
        • Category.vue
      • 3. 作用域插槽
        • Father.vue
        • Category.vue
  • 7. 其它 API
    • 7.1 shallowRef 与 shallowReactive
      • shallowRef
      • shallowReactive
      • 示例
    • 7.2 readonly 与 shallowReadonly
      • readonly
      • shallowReadonly
      • 示例
    • 7.3 toRaw 与 markRaw
      • toRaw
      • markRaw
      • 示例
    • 7.4 customRef
      • 示例
        • App.vue
        • useMsgRef.ts
  • 8. Vue3新组件
    • 8.1 Teleport传送门
      • 示例
        • App.vue
        • Modal.vue
    • 8.2 Suspense
      • 示例
        • App.vue
        • Child.vue
    • 8.3 全局API转移到应用对象
      • 示例
    • 8.4 其他

学习链接

尚硅谷Vue3入门到实战,最新版vue3+TypeScript前端开发教程

Vue3+Vite4+Pinia+ElementPlus从0-1 web项目搭建

Vue3.2后台管理系统

深入Vue3+TypeScript技术栈 coderwhy

尚硅谷Vue项目实战硅谷甄选,vue3项目+TypeScript前端项目一套通关

Vue3 + vite + Ts + pinia + 实战 + 源码 + electron - 百万播放量哦

1. Vue3简介

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

  • 经历了:4800+次提交、40+个RFC、600+次PR、300+贡献者

  • 官方发版地址:Release v3.0.0 One Piece · vuejs/core

  • 截止2023年10月,最新的公开版本为:3.3.4

在这里插入图片描述

1.1. 性能的提升

  • 打包大小减少41%

  • 初次渲染快55%, 更新渲染快133%

  • 内存减少54%

1.2.源码的升级

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

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

1.3. 拥抱TypeScript

  • Vue3可以更好的支持TypeScript

1.4. 新的特性

  1. Composition API(组合API):

    • setup

    • refreactive

    • computedwatch

  2. 新的内置组件:

    • Fragment

    • Teleport

    • Suspense

  3. 其他改变:

    • 新的生命周期钩子

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

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

2. 创建Vue3工程

2.1. 基于 vue-cli 创建

点击查看 Vue-Cli 官方文档,(基于vue-cli创建,其实就是基于webpack来创建vue项目)

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

## 查看@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介绍

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

  • 轻量快速的热重载(HMR),能实现极速的服务启动。
  • TypeScriptJSXCSS 等支持开箱即用(不用配置,直接就可以用)。
  • 真正的按需编译,不再等待整个应用编译完成。
  • webpack构建 与 vite构建对比图如下:
    webpack构建 vite构建

创建步骤

具体操作如下(点击查看官方文档)

## 1.创建命令(基于vite创建vue3项目,前提是需要安装nodejs环境)
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

构建过程如下:

在这里插入图片描述

访问vue3项目如下:

在这里插入图片描述

项目结构

安装插件

安装官方推荐的vscode插件:

在这里插入图片描述

在这里插入图片描述

项目结构

在这里插入图片描述

index.html

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><link rel="icon" href="/favicon.ico"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Vite App</title></head><body><div id="app"></div><script type="module" src="/src/main.ts"></script></body>
</html>

main.ts

import './assets/main.css'// 引入createApp用于创建应用
import { createApp } from 'vue'// 引入App根组件
import App from './App.vue'createApp(App).mount('#app')

App.vue

<!-- 自己动手编写的一个App组件 -->
<template><div class="app"><h1>你好啊!</h1></div>
</template><script lang="ts"> // 添加lang="ts", 里面写ts或js都可以export default {name:'App' //组件名}</script><style>.app {background-color: #ddd;box-shadow: 0 0 10px;border-radius: 10px;padding: 20px;}
</style>
总结
  • Vite 项目中,index.html 是项目的入口文件,在项目最外层。
  • 加载index.html后,Vite 解析 <script type="module" src="xxx"> 指向的JavaScript
  • Vue3在main.ts中是通过 createApp 函数创建一个应用实例。

2.3. 一个简单的效果

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

Person.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 lang="ts">export default {name:'App',data() {return {name:'张三',age:18,tel:'13888888888'}},methods:{changeName(){this.name = 'zhang-san'},changeAge(){this.age += 1},showTel(){alert(this.tel)}},}
</script>

App.vue

<template><div class="app"><h1>你好啊!</h1><Person/></div>
</template><script lang="ts">import Person from './components/Person.vue'export default {name:'App', //组件名components:{Person} //注册组件}
</script><style>.app {background-color: #ddd;box-shadow: 0 0 10px;border-radius: 10px;padding: 20px;}
</style>

3. Vue3核心语法

3.1. OptionsAPI 与 CompositionAPI

  • Vue2API设计是Options(配置)风格的。
  • Vue3API设计是Composition(组合)风格的。

Options API 的弊端

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

1.gif2.gif

Composition API 的优势

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

3.gif4.gif

3.2. 拉开序幕的 setup

setup 概述

介绍

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

特点如下:

  • setup函数返回的对象中的内容,可直接在模板中使用。
  • setup中访问thisundefined
  • setup函数会在beforeCreate之前调用,它是“领先”所有钩子执行的。
<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 lang="ts">export default {name:'Person',// 生命周期函数beforeCreate(){console.log('beforeCreate')},setup(){// 先打印的setup..., 再打印的beforeCreate, 说明了setup函数与beforeCreate生命周期函数的执行顺序console.log('setup ...')// 【setup函数中的this是undefined】console.log(this); // undefined// 数据,原来写在data中【注意:此时的name、age、tel数据都不是响应式数据】//                   (不是响应式的意思是:当这些数据变化,并不会触发dom更新,//                                     模板中应用这些变量的地方没有重新渲染)let name = '张三'let age = 18let tel = '13888888888'// 方法,原来写在methods中function changeName(){name = 'zhang-san'        // 注意:此时这么修改name页面是不变化的console.log(name)         // (name确实改了,但name不是响应式的)}function changeAge(){age += 1                  // 注意:此时这么修改age页面是不变化的console.log(age)          // (age确实改了,但age不是响应式的)}function showTel(){alert(tel)}// 返回一个对象,对象中的内容,模板中可以直接使用(将数据、方法交出去,模板中才可以使用这些交出去的数据、方法)return {name,age,tel,changeName,changeAge,showTel}}}
</script>

setup 的返回值

  • 若返回一个对象:则对象中的:属性、方法等,在模板中均可以直接使用**(重点关注)。**
  • 若返回一个函数:则可以直接指定 自定义渲染的内容,代码如下:
<template><div class="person">我特么一点都不重要了</div>
</template><script lang="ts">export default {name:'Person',setup(){// setup的返回值也可以是一个渲染函数// (模板什么的都不重要了,直接在页面上渲染成:你好啊!这几个字)// return ()=>'哈哈'}}
</script><style scoped>.person {background-color: skyblue;box-shadow: 0 0 10px;border-radius: 10px;padding: 20px;}button {margin: 0 5px;}
</style>

setup 与 Options API 的关系

  • Vue2 的配置(datamethos…)中可以访问到 setup中的属性、方法。
  • 但在setup不能访问到Vue2的配置(datamethos…)。
  • 如果与Vue2冲突,则setup优先。
<template><div class="person"><h2>姓名:{{name}}</h2><h2>年龄:{{age}}</h2><button @click="changeName">修改名字</button><button @click="changeAge">修改年龄</button><button @click="showTel">查看联系方式</button><hr><h2>测试1:{{a}}</h2><h2>测试2:{{c}}</h2><h2>测试3:{{d}}</h2><button @click="b">测试</button></div>
</template><script lang="ts">export default {name:'Person',beforeCreate(){console.log('beforeCreate')},data(){return {a:100,// 在data配置项中, 可以使用this.name来使用setup中交出的数据, 因为setup执行时机更早。// 但是在setup中不能使用在data中定义的数据c:this.name, d:900,age:90}},methods:{b(){console.log('b')}},// setup可以与data、methods等配置项同时存在setup(){// 数据,原来是写在data中的,此时的name、age、tel都不是响应式的数据let name = '张三'let age = 18let tel = '13888888888'// 方法function changeName() {name = 'zhang-san' // 注意:这样修改name,页面是没有变化的console.log(name)  // name确实改了,但name不是响应式的}function changeAge() {age += 1           // 注意:这样修改age,页面是没有变化的console.log(age)   // age确实改了,但age不是响应式的}function showTel() {alert(tel)}// 将数据、方法交出去,模板中才可以使用return {name,age,tel,changeName,changeAge,showTel}// setup的返回值也可以是一个渲染函数// return ()=>'哈哈'}}
</script><style scoped>.person {background-color: skyblue;box-shadow: 0 0 10px;border-radius: 10px;padding: 20px;}button {margin: 0 5px;}
</style>

setup 语法糖

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

<template><div class="person"><h2>姓名:{{name}}</h2><h2>年龄:{{age}}</h2><button @click="changName">修改名字</button><button @click="changAge">年龄+1</button><button @click="showTel">点我查看联系方式</button></div></template><!-- 专门单个弄个script标签, 特地来配置组件的名字 -->
<script lang="ts">export default {name:'Person',}
</script><!-- 下面的写法是setup语法糖 -->
<!-- 1. 相当于写了setup函数; 2. 相当于自动把其中定义的变量交出去(包括里面引入的其它组件也会交出去, 可以在模板中使用引入的组件))-->
<script setup lang="ts">console.log(this) // undefined// 数据(注意:此时的name、age、tel都不是响应式数据)let name = '张三'let age = 18let tel = '13888888888'// 方法function changName(){name = '李四'//注意:此时这么修改name页面是不变化的}function changAge(){console.log(age)age += 1 //注意:此时这么修改age页面是不变化的}function showTel(){alert(tel)}
</script>

扩展:上述代码,还需要编写一个不写setupscript标签,去指定组件名字,比较麻烦,我们可以借助vite中的插件简化

  1. 第一步:npm i vite-plugin-vue-setup-extend -D
  2. 第二步:vite.config.ts
import { fileURLToPath, URL } from 'node:url'import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import VueSetupExtend from 'vite-plugin-vue-setup-extend'// https://vitejs.dev/config/
export default defineConfig({plugins: [vue(),VueSetupExtend(),],resolve: {alias: {'@': fileURLToPath(new URL('./src', import.meta.url))}}
})
  1. 第三步:<script setup lang="ts" name="Person">

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

  • **作用:**定义响应式变量。
  • 语法:let xxx = ref(初始值)
  • **返回值:**一个RefImpl的实例对象,简称ref对象refref对象的value属性是响应式的
  • 注意点:
    • JS中操作数据需要:xxx.value,但模板中不需要.value,直接使用即可。
    • 对于let name = ref('张三')来说,name不是响应式的,name.value是响应式的。
<template><div class="person"><!-- 模板中直接使用, 不需要.value --><h2>姓名:{{name}}</h2><h2>年龄:{{age}}</h2><h2>电话:{{tel}}</h2><button @click="changeName">修改名字</button><button @click="changeAge">年龄+1</button><button @click="showTel">点我查看联系方式</button></div>
</template><!-- 使用了setup语法糖, 会自动将定义的变量和方法交出去, 以供给模板使用 -->
<script setup lang="ts" name="Person">// 引入vue中的ref函数import { ref } from 'vue'// name和age是一个RefImpl的实例对象,简称ref对象,它们的value属性是响应式的。//(所谓的响应式指的是, 对数据的改变后, 能够让模板中使用该数据的地方得到重新渲染更新)// ref是1个函数, 向这个ref函数中传入参数, 返回的是1个RefImpl的实例对象let name = ref('张三')let age = ref(18)// tel就是一个普通的字符串,不是响应式的let tel = '13888888888'function changeName(){// JS中操作ref对象时候需要.valuename.value = '李四' // 页面得到刷新console.log(name.value)// 注意:name不是响应式的,name.value是响应式的,所以如下代码并不会引起页面的更新。// name = ref('zhang-san')}function changeAge(){// JS中操作ref对象时候需要.valueage.value += 1      // 页面得到刷新console.log(age.value)}function showTel(){// tel是普通数据      tel += '1'          // tel的确改了, 但页面并未刷新alert(tel)}
</script>

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

  • 作用:定义一个响应式对象(基本类型不要用它,要用ref,否则报错)
  • 语法:let 响应式对象= reactive(源对象)
  • **返回值:**一个Proxy的实例对象,简称:响应式对象。
  • 注意点:reactive定义的响应式数据是“深层次”的。
<template>
<div class="person"><h2>汽车信息:一台{{ car.brand }}汽车,价值{{ car.price }}万</h2><h2>游戏列表:</h2><ul><li v-for="g in games" :key="g.id">{{ g.name }}</li></ul><h2>测试:{{ obj.a.b.c.d }}</h2><button @click="changeCarPrice">修改汽车价格</button><button @click="changeFirstGame">修改第一游戏</button><button @click="test">测试</button></div>
</template><script lang="ts" setup name="Person">import { reactive } from 'vue'// 定义数据// reactive是1个函数, 向这个reactive函数中传入参数(传入对象或数组), 返回的是1个Proxy的实例对象//(Proxy是原生Js就有的函数)// reactive函数中传入对象let car = reactive({ brand: '奔驰', price: 100 }) console.log('car', car); // car Proxy {brand: '奔驰', price: 100}// reactive函数传入数组let games = reactive([  { id: 'ahsgdyfa01', name: '英雄联盟' },{ id: 'ahsgdyfa02', name: '王者荣耀' },{ id: 'ahsgdyfa03', name: '原神' }])// reactive定义的响应式数据是 深层次 的let obj = reactive({a: {b: {c: {d: 666}}}})// 修改对象中的属性(修改使用reactive包裹对象后返回的对象)function changeCarPrice() {car.price += 10}// 修改数组中的对象的属性(修改使用reactive包裹数组后返回的对象)function changeFirstGame() {games[0].name = '流星蝴蝶剑'}function test() {obj.a.b.c.d = 999}
</script>

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

  • 其实ref接收的数据可以是:基本类型对象类型
  • ref接收的是对象类型,内部其实也是调用了reactive函数。
<template><div class="person"><h2>汽车信息:一台{{ car.brand }}汽车,价值{{ car.price }}万</h2><h2>游戏列表:</h2><ul><li v-for="g in games" :key="g.id">{{ g.name }}</li></ul><h2>测试:{{ obj.a.b.c.d }}</h2><button @click="changeCarPrice">修改汽车价格</button><button @click="changeFirstGame">修改第一游戏</button><button @click="test">测试</button></div>
</template><script lang="ts" setup name="Person">import { ref,reactive } from 'vue'// 使用ref定义对象类型响应式数据let car = ref({ brand: '奔驰', price: 100 })// 使用reactive定义对象类型响应式数据let car2 = reactive({brand: '奔驰', price: 100})// reactive只能用来定义对象类型的响应式数据// let name = reactive('zhangsan') // 错误, value cannot be made reactive: zhangsan// 使用ref定义对象(数组)类型响应式数据let games = ref([{ id: 'ahsgdyfa01', name: '英雄联盟' },{ id: 'ahsgdyfa02', name: '王者荣耀' },{ id: 'ahsgdyfa03', name: '原神' }])// 使用ref定义对象类型响应式数据也是深层次的let obj = ref({a: {b: {c: {d: 666}}}})// 若ref接收的是对象类型,内部其实也是使用的reactive函数console.log(car)       // RefImpl {__v_isShallow: false, dep: undefined, //          __v_isRef: true, _rawValue: {…}, _value: Proxy}console.log(car.value) // Proxy {brand: '奔驰', price: 100}console.log(car2)      // Proxy {brand: '奔驰', price: 100}function changeCarPrice() {// 使用ref函数定义的响应式数据, 在js操作时, 需要带上.value, 才能碰到内部的Proxy对象car.value.price += 10console.log(car.value.price);}function changeFirstGame() {// 使用ref函数定义的响应式数据, 在js操作时, 需要带上.value, 才能碰到内部的Proxy对象games.value[0].name = '流星蝴蝶剑'console.log(games.value);   // Proxy {0: {…}, 1: {…}, 2: {…}}}function test() {// 使用ref函数定义的响应式数据, 在js操作时, 需要带上.value, 才能碰到内部的Proxy对象obj.value.a.b.c.d = 999}</script>

3.6. ref 对比 reactive

宏观角度

  • ref可以定义:基本类型、对象类型的响应式数据

  • reactive只能定义:对象类型的响应式数据

区别

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

    可以在齿轮->设置->扩展->volar中勾选自动补充value ,它会在使用ref创建的变量时,自动添加上.value

  • reactive重新分配一个新对象,会失去响应式(可以使用Object.assign去整体替换)。

    <template><div class="person"><h2>汽车信息:一台{{ car.brand }}汽车,价值{{ car.price }}万</h2><button @click="changeBrand">改品牌</button><button @click="changePrice">改价格</button><button @click="changeCar">改car</button></div>
    </template><script lang="ts" setup name="Person">import { ref,reactive } from 'vue'let car = reactive({brand:'奔驰', price:100})function changeBrand() {// 正常修改car的brand, 并且是响应式car.brand = '宝马'
    }function changePrice() {// 正常修改car的price, 并且是响应式car.price += 10
    }function changeCar() {// 错误做法1// 不可以直接给reactive重新分配一个新对象,这会让car直接失去响应式// car = {brand:'奥托', price:10}// 错误做法2// 这样也不行, 因为模板中用的car是上面定义的响应式对象, // 现在car指向的是1个新的响应式对象, 而模板中压根就没有使用这个新的响应式对象// car =  reactive({brand:'奥托', price:10})// 正确做法(car仍然是响应式的)// API介绍: Object.assign(obj1, obj2, obj3, ..), //         将obj2中的每一组属性和值设置到obj1中, 然后obj3的每一组属性和值设置到obj1中Object.assign(car, {brand:'奥托', price:10})
    }</script>
    
    <template><div class="person"><h2>汽车信息:一台{{ car.brand }}汽车,价值{{ car.price }}万</h2><button @click="changeBrand">改品牌</button><button @click="changePrice">改价格</button><button @click="changeCar">改car</button></div>
    </template><script lang="ts" setup name="Person">import { ref,reactive } from 'vue'let car = ref({brand:'奔驰', price:100})function changeBrand() {// 正常修改car的brand, 并且是响应式car.value.brand = '宝马'
    }function changePrice() {// 正常修改car的price, 并且是响应式car.value.price += 10
    }function changeCar() {// 错误做法1// 不能直接给car换了个ref, 因为模板中压根就没有使用这个新的RefImpl对象// car = ref({brand:'奥托', price:10})// 正确做法1(car仍然是响应式的)// API介绍: Object.assign(obj1, obj2, obj3, ..), 将obj2中的每一组属性和值设置到obj1中, //         然后obj3的每一组属性和值设置到obj1中// Object.assign(car.value, {brand:'奥托', price:10})// 正确做法2//(这里相比于对car使用reactive定义而言, 使用ref定义则可以直接给car.value整体赋值// 原因在于car.value获取的是Proxy响应式对象, 凡是对Proxy响应式对象的操作都可以被拦截到)car.value = {brand:'奥托', price:10}}</script>
    

使用原则

  • 若需要一个基本类型的响应式数据,必须使用ref

  • 若需要一个响应式对象,层级不深,refreactive都可以。

  • 若需要一个响应式对象,且层级较深,推荐使用reactive

3.7 toRefs 与 toRef

  • 作用:将一个响应式对象中的每一个属性,转换为ref对象。
  • 备注:toRefstoRef功能一致,但toRefs可以批量转换。

现象

对响应式对象直接结构赋值,得到的数据不是响应式的

<template><div class="person"><h2>姓名:{{ person.name }} {{ name }}</h2><h2>年龄:{{ person.age }}  {{ age }}</h2><button @click="changeName">修改名字</button><button @click="changeAge">修改年龄</button></div>
</template><script lang="ts" setup name="Person2">import { ref, reactive, toRefs, toRef } from 'vue'// 数据let person = reactive({ name: '张三', age: 18 })console.log(person);                // Proxy {name: '张三', age: 18}// 这里的解构赋值其实就等价于: let name = person.name; let age = person.age;// 只是记录了此时person.name、person.age的值, 仅此而已// 因此, 此处使用结构赋值语法获取的name和age都不是响应式的let {name, age } = personconsole.log(name, age);              // 张三 18// 方法function changeName() {name += '~'console.log(name, person.name);  // 变化的是name, 而person.name仍然未修改}function changeAge() {age += 1console.log(age, person.age);    // 变化的是age, 而person.age仍然未修改}</script>

toRefs&toRef的使用

通过toRefs将person对象中的所有属性都批量取出, 且依然保持响应式的能力

<template>
<div class="person"><h2>姓名:{{ person.name }}</h2><h2>年龄:{{ person.age }}</h2><h2>性别:{{ person.gender }}  {{ gender }}</h2><button @click="changeName">修改名字</button><button @click="changeAge">修改年龄</button><button @click="changeGender">修改性别</button><button @click="changeGender2">修改性别2</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对象中的所有属性都批量取出, 且依然保持响应式的能力//(使用toRefs从person这个响应式对象中,解构出name、age, 且name和age依然是响应式的,//  name和gender的值是ref类型, 其value值指向的是person.name和person.age,//  对name.value和对age.value的修改将会修改person.name和person.age, 并且会页面渲染刷新)let { name, age } = toRefs(person)console.log(name.value, name);     // '张三' ObjectRefImpl {_object: Proxy, _key: 'name', //                 _defaultValue: undefined, __v_isRef: true}console.log(age.value, age.value); // 18 ObjectRefImpl {_object: Proxy, _key: 'age', //                   _defaultValue: undefined, __v_isRef: true}console.log(toRefs(person));       // {name: ObjectRefImpl, age: ObjectRefImpl, //  gender: ObjectRefImpl}// 通过toRef将person对象中的gender属性取出,且依然保持响应式的能力let gender = toRef(person, 'gender')console.log(gender, gender.value); // ObjectRefImpl {_object: Proxy, _key: 'gender', //               _defaultValue: undefined, __v_isRef: true} '男'// 方法function changeName() {// 此处修改name.value, 将会修改person.name, 并且页面会刷新person.name的值name.value += '~'console.log(name.value, person.name);}function changeAge() {// 此处修改age.value, 将会修改person.age, 并且页面会刷新person.age的值age.value += 1console.log(age.value, person.age);}function changeGender() {// 此处修改gender.value, 将会修改person.age, 并且页面会刷新person.gender的值gender.value = '女'console.log(gender.value, person.gender);}function changeGender2() {// 此处对person.gender的修改, 将会修改上面的let gender = toRef(person, 'gender')// 并且页面会刷新person.gender和gender的值person.gender = '男'console.log(gender.value, person.gender);}
</script>

3.8 computed

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

<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">// 引入computed计算属性函数
import { ref, computed } from 'vue'let firstName = ref('zhang')
let lastName = ref('san')// 计算属性——只读取,不修改
/* 
// 1. 使用时, 在computed中传入1个函数。在模板中, 直接使用计算属性即可。
// 2. 当计算属性依赖的数据只要发生变化, 它就会重新计算, 如果页面中有使用到该计算属性, 那么就会重新渲染模板 
// 3. 只会计算1次, 后面会使用缓存, 而方法是没有缓存的
let fullName = computed(()=>{return firstName.value + '-' + lastName.value
}) 
console.log(fullName); // ComputedRefImpl {dep: undefined, __v_isRef: true, //                  __v_isReadonly: true, effect: ReactiveEffect, _setter: ƒ, …}*/// 计算属性——既读取又修改
let fullName = computed({// 读取get() {// 当firstName或lastName变化时, 计算属性会重新计算, 并刷新页面渲染return firstName.value + '-' + lastName.value},// 修改// 当修改计算属性时(或者说给计算属性赋值时, 注意要.value), 此方法会被调用set(val) {console.log('有人修改了fullName', val)firstName.value = val.split('-')[0]lastName.value = val.split('-')[1]}
})function changeFullName() {// 修改fullName计算属性(会触发计算属性中set方法的调用)fullName.value = 'li-si'
}
</script>

3.9 watch

作用

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

特点

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

  • ref定义的数据。

  • reactive定义的数据。

  • 函数返回一个值(getter函数,所谓的getter函数就是能返回一个值的函数)。

  • 一个包含上述内容的数组。

场景

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

* 情况一

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

<template><div class="person"><h1>情况一:监视【ref】定义的【基本类型】数据</h1><h2>当前求和为:{{ sum }}</h2><button @click="changeSum">点我sum+1</button></div>
</template><script lang="ts" setup name="Person">// 引入watch监视函数
import { ref, watch } from 'vue'// 数据
let sum = ref(0)// 方法
function changeSum() {sum.value += 1
}// 监视,情况一:监视【ref】定义的【基本类型】数据
//(注意:这里监视写的是sum, 而不是sum.value哦)
const stopWatch = watch(sum, (newValue, oldValue) => {console.log('sum变化了', newValue, oldValue) // 注意: 这里也没带.value哦if (newValue >= 10) {// 解除监视(即: 当调用此方法后, 不会再监视sum的变化了, 也就是当sum变化时, 当前的监视函数不再执行了)stopWatch()}
})</script><style scoped>
...
</style>
* 情况二

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

注意:

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

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

示例1
<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 += '~'   // 当修改person.value.name时, 监视函数未被触发
}function changeAge() {person.value.age += 1      // 当修改person.value.age时, 监视函数也未被触发
}function changePerson() {person.value = { name: '李四', age: 90 }  // 当整体修改person.value时, 此时监视函数被触发
}                                            // (因为监视的是对象的地址值, 所以这里每次修改都会触发监视函数)/* 监视,情况一:监视【ref】定义的【对象类型】数据,监视的是对象的地址值。watch的第一个参数是:被监视的数据watch的第二个参数是:监视的回调
*/
watch(person, (newValue, oldValue) => {console.log('person变化了', newValue, oldValue)// 一直调用changePerson方法, 控制台如下输出// person变化了 Proxy {name: '李四', age: 90} Proxy {name: '张三', age: 18}// person变化了 Proxy {name: '李四', age: 90} Proxy {name: '李四', age: 90}// person变化了 Proxy {name: '李四', age: 90} Proxy {name: '李四', age: 90}// person变化了 Proxy {name: '李四', age: 90} Proxy {name: '李四', age: 90}// ...
})</script><style scoped>
...
</style>
示例2
<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 += '~'// 因为开启了深度监视, 当修改person.value.name时, 监视函数被触发
}                         //(但由于原对象并未修改, 所以监视函数中输出的newVal和oldVal是一样的)// 每次调用changeName都修改, 变化如下:// person变化了 Proxy {name: '张三~', age: 18} Proxy {name: '张三~', age: 18}// person变化了 Proxy {name: '张三~~', age: 18} Proxy {name: '张三~~', age: 18}// person变化了 Proxy {name: '张三~~', age: 18} Proxy {name: '张三~~', age: 18}// ...
function changeAge() {person.value.age += 1   // 因为开启了深度监视, 当修改person.value.name时, 监视函数被触发
}                           //(但由于原对象并未修改, 所以监视函数中输出的newVal和oldVal是一样的)// 每次调用changeName都修改, 变化如下:// person变化了 Proxy {name: '张三', age: 19} Proxy {name: '张三', age: 19}// person变化了 Proxy {name: '张三', age: 20} Proxy {name: '张三', age: 20}// person变化了 Proxy {name: '张三', age: 21} Proxy {name: '张三', age: 21}// ...function changePerson() {person.value = { name: '李四', age: 90 }// 当整体修改person.value时, 监视函数被触发//(但由于原对象都改了, 所以监视函数中输出的newVal和oldVal是不一样的)// 每次调用changeName都修改, 变化如下:
}                           // person变化了 Proxy {name: '李四', age: 90} Proxy {name: '张三', age: 18}// person变化了 Proxy {name: '李四', age: 90} Proxy {name: '李四', age: 90}// person变化了 Proxy {name: '李四', age: 90} Proxy {name: '李四', age: 90}// ...
/* 监视,情况二:监视【ref】定义的【对象类型】数据,监视的是对象的地址值,若想监视对象内部属性的变化,需要手动开启深度监视watch的第一个参数是:被监视的数据watch的第二个参数是:监视的回调watch的第三个参数是:配置对象(deep、immediate等等) 
*/
watch(person, (newValue, oldValue) => {console.log('person变化了', newValue, oldValue)
}, { deep: true, immediate: true })</script><style scoped>
.person {background-color: skyblue;box-shadow: 0 0 10px;border-radius: 10px;padding: 20px;
}button {margin: 0 5px;
}li {font-size: 20px;
}
</style>
* 情况三

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

<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 += '~'// 每次调用changeName都修改, 变化如下:// person变化了 Proxy {name: '张三~', age: 18} Proxy {name: '张三~', age: 18}// person变化了 Proxy {name: '张三~~', age: 18} Proxy {name: '张三~~', age: 18}// person变化了 Proxy {name: '张三~~~', age: 18} Proxy {name: '张三~~~', age: 18}// ...//(如上结果, //  1. 证明监视到了person的name //  2. oldVal和newVal是一样的输出, 是因为虽然监测到person的变化, 但oldVal和newVal是同一对象, 从这来说并未改变)}function changeAge(){person.age += 1// 每次调用changeAge都修改, 变化如下:// person变化了 Proxy {name: '张三', age: 19} Proxy {name: '张三', age: 19}// person变化了 Proxy {name: '张三', age: 20} Proxy {name: '张三', age: 20}// person变化了 Proxy {name: '张三', age: 21} Proxy {name: '张三', age: 21}// ...//(如上结果, //  1. 证明监视到了person的age//  2. oldVal和newVal是一样的输出, 是因为虽然监测到person的变化, 但oldVal和newVal是同一对象, 从这来说并未改变)}function changePerson(){// 此处注意: 使用reactive函数定义的数据, 不能直接替换, 可以如下方式对person中的属性做批量修改 Object.assign(person,{name:'李四',age:80})// 多次调用changePerson, 仅有1次监视到到修改, 变化如下:// person变化了 Proxy {name: '李四', age: 80} Proxy {name: '李四', age: 80}//(如上结果, //  1. 证明监视到了person的name和age的改变//  2. oldVal和newVal是一样的输出, 是因为虽然监测到person的变化, 但oldVal和newVal仍是同一对象, 从这来说并未改变)}function test(){obj.a.b.c = 888// 此处证明watch监控reactive定义的对象类型数据, 默认是开启了深度监视的}// 监视,情况三:监视【reactive】定义的【对象类型】数据,且默认是开启深度监视的(隐式创建了深层次的监听, 无法关闭)watch(person,(newValue,oldValue)=>{console.log('person变化了',newValue,oldValue)})watch(obj,(newValue,oldValue)=>{console.log('Obj变化了',newValue,oldValue)})</script><style scoped>
...
</style>
* 情况四

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

  1. 若该属性值不是【对象类型】,需要写成函数形式。
  2. 若该属性值是依然是【对象类型】,可直接编,也可写成函数,建议写成函数。

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

没有监视的代码
<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是使用reactive定义的, 所以person整体不能改(改是可以改, 但是不再响应式了, //                                                         所以说不能整体直接改), //          但是person里面的car属性可以改, 因此可以如下改person.car = { c1: '雅迪', c2: '爱玛' }}</script><style scoped>...
</style>
监视reactive定义的对象类型中的某个基本属性
<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 += '~'// 一直调用changeName方法, 控制台如下输出// person.name变化了 张三~ 张三// person.name变化了 张三~~ 张三~// person.name变化了 张三~~~ 张三~~// ...}function changeAge() {person.age += 1}function changeC1() {person.car.c1 = '奥迪'}function changeC2() {person.car.c2 = '大众'}function changeCar() {// 注意此处: 因为person是使用reactive定义的, 所以person整体不能改(改是可以改, 但是不再响应式了, //                                                         所以说不能整体直接改), //          但是person里面的car属性可以改, 因此可以如下改person.car = { c1: '雅迪', c2: '爱玛' }}// 监视,情况四:监视响应式对象中的某个属性,且该属性是基本类型的,要写成函数式(不能直接写person.name哦)//(如下监视, 将会只监视person的name属性的变化, //  当person的name属性发生变化时, 将会触发监听函数执行, 其它属性变化不会触发监听函数的执行)watch(()=> person.name,(newValue,oldValue)=>{console.log('person.name变化了',newValue,oldValue)}) // 错误写法, 因为person的name属性是基本类型, 所以不能直接写为第1个参数, 应该要用函数包一下/*watch(person.name,(newValue,oldValue)=>{console.log('person.name变化了',newValue,oldValue)})*/// 监视person的car属性中的c1属性//(当调用changeC1方法时, 此处能够监测到person.car.c1的改变;//  多次调用changeC1方法, 此处只监测到了1次, 因为后面都没改person.car.c1的值;//  当调用changeCar方法, 此处能够监测到person.car.c1的改变;//  多次调用changeCar方法, 此处只监测到了1次, 因为后面都没改person.car.c1的值;)watch(()=> person.car.c1,(newValue,oldValue)=>{console.log('person.car.c1变化了',newValue,oldValue)})// 错误写法, 因为person的car.c1属性是基本类型, 所以不能直接写为第1个参数, 应该要用函数包一下/*watch(person.car.c1,(newValue,oldValue)=>{console.log('person.car.c1变化了',newValue,oldValue)})*/</script><style scoped>
...
</style>
监视reactive定义的对象类型中的某个对象属性
<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是使用reactive定义的, 所以person整体不能改(改是可以改, 但是不再响应式了, //                                                             所以说不能整体直接改), //          但是person里面的car属性可以改, 因此可以如下改person.car = { c1: '雅迪', c2: '爱玛' }}// 监视,情况四:监视响应式对象中的某个属性,且该属性是对象类型的,可以直接写,也能写函数,更推荐写函数// 建议写成函数的形式// 当调用changeC1或changeC2方法时, 会触发此处的监测函数执行// 当调用changeCar方法时, 会触发此处的监测函数执行// 【最佳实践】(函数式来开启对person.car的地址值的监测, 然后deep:true开启对该对象的深度监视)watch(() => person.car, (newValue, oldValue) => {console.log('person.car变化了', newValue, oldValue)}, { deep: true })// 如果写成下面这样, 监测的其实是person.car的地址值, 只有在person.car整体改变时, 才会触发此处的监测函数执行//  当调用changeC1或changeC2方法时, 不会触发此处的监测函数执行/* watch(() => person.car, (newValue, oldValue) => {console.log('person.car变化了', newValue, oldValue)}) */// 如果写成下面这样(直接写的做法), 那么当调用changeCar方法时, 不会触发此处的监测函数执行// 当调用changeC1或changeC2方法时, 会触发此处的监测函数执行//(因为person.car是person中的对象类型属性, 因此这里可以直接写)/* watch(person.car, (newValue, oldValue) => {console.log('person.car变化了', newValue, oldValue)}) */</script><style scoped>...
</style>
* 情况五

监视上述的多个数据

<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:'爱玛'}}// 监视,情况五:监视上述的多个数据//(person.name是基本类型, 所以要写成函数式; person.car是对象类型, 所以可以直接写;// 这里的newVal和oldVal都是数组, 跟监视的2个源相对应; // deep开启深度监视, 不止可以监视地址值, 还包括内部属性的变化;)watch([()=>person.name, person.car],(newValue, oldValue)=>{console.log('person.car变化了',newValue,oldValue)},{deep:true})</script><style scoped>
...
</style>

3.10 watchEffect

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

watch对比watchEffect

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

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

  3. watchEffect:不用明确指出监视的数据(函数中用到哪些属性,那就监视哪些属性)。

<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、heightwatch([temp,height],(value)=>{// 从value中获取最新的temp值、height值const [newTemp,newHeight] = value// 室温达到50℃,或水位达到20cm,立刻联系服务器if(newTemp >= 50 || newHeight >= 20){console.log('联系服务器')}})// 用watchEffect实现,不用明确的指出要监视变量// 1. 它会从监听函数中自动分析需要监视的数据 (而watch则需要指定需要监视的数据)// 2. 一上来就会执行1次函数const stopWtach = watchEffect(()=>{// 室温达到50℃,或水位达到20cm,立刻联系服务器if(temp.value >= 50 || height.value >= 20){console.log(document.getElementById('demo')?.innerText)console.log('联系服务器')}// 水温达到100,或水位达到50,取消监视if(temp.value === 100 || height.value === 50){console.log('清理了')stopWtach()}})
</script>

3.11. 标签的 ref 属性

作用:用于注册模板引用。

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

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

用在普通DOM标签上

<template><div class="person"><!-- ref标记在普通DOM标签上 --><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()  // 使用ref来获取对应的节点, 其中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>

用在组件标签上(defineExpose)

defineExpose它属于宏函数,不需要引入

<!-- 父组件App.vue -->
<template><!-- ref标记在组件标签上 --><Person ref="ren"/><button @click="test">测试</button></template><script lang="ts" setup name="App">// 在setUp中不需要注册Person组件, 直接使用即可import Person from './components/Person.vue'import {ref} from 'vue'// 变量名需要与ref标记的值相同let ren = ref()function test(){// 需要子组件通过defineExpose暴露出来的属性或方法, 父组件才可以在这里访问到console.log(ren.value.name)console.log(ren.value.age)}
</script><!-- 子组件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>

3.12 回顾TS

main.ts

// 引入createApp用于创建应用
import { createApp } from 'vue'// 引入App根组件
import App from './App.vue'createApp(App).mount('#app')

App.vue

<template><Person/>
</template><script lang="ts" setup name="App">import Person from '@/components/Person.vue'
</script>

index.ts

在src下创建types文件夹,并在这个文件夹中创建如下index.ts文件。

在其中定义接口和自定义泛型

// 定义一个接口,用于限制person对象的具体属性
export interface PersonInter {id: string,name: string,age: number,x?: number /* x是可选属性, 该类型中可以有该属性, 也可以无该属性 */
}// 一个自定义类型
// export type Persons = Array<PersonInter>
export type Persons = PersonInter[] // 与上面等价

Person.vue

注意把vetur这个插件给禁掉, 否则,老是有飘红。就开启本篇中上述的推荐的插件即可。

<template><div class="person"></div>
</template><script lang="ts" setup name="Person">// 引入接口 或 自定义类型 的时候, 需要在前面加上type; import { type PersonInter, type Persons } from '@/types'// 定义1个变量, 它要符合PersonInter接口let person: PersonInter = {id: 'a01', name: 'john', age:60}// 定义1个数组, 首先它是个数组, 并且里面元素类型都是符合PersonInter接口的(如果里面有属性名写错会有飘红提示)let personList: Array<PersonInter> = [{id: 'a01', name: 'john', age:60}]// 定义1个数组, 它符合 Persons 自定义类型(如果里面有属性名写错会有飘红提示)let personList2: Persons = [{id: 'a01', name: 'john', age:60}]</script><style scoped></style>

3.13 props(defineProps)

defineProps它属于宏函数,不需要引入

App.vue

<template><!-- Person子组件定义了list属性, 并且限定为Persons类型 --><Person :list="personList" />
</template><script lang="ts" setup name="App">import Person from '@/components/Person.vue'import {reactive} from 'vue'import {type Persons} from '@/types'let personList = reactive<Persons>([{ id: 'asudfysafd01', name: '张三', age: 18 },{ id: 'asudfysafd02', name: '李四', age: 20 },{ id: 'asudfysaf)d03', name: '王五', age: 22 }])</script>

index.ts

// 定义一个接口,用于限制person对象的具体属性
export interface PersonInter {id: string,name: string,age: number,x?: number /* x是可选属性, 该类型中可以有该属性, 也可以无该属性 */
}// 一个自定义类型
// export type Persons = Array<PersonInter>
export type Persons = PersonInter[] // 与上面等价

Person.vue

<template><div class="person"><ul><!-- 在模板中直接使用list, 不需要加props.list --><li v-for="p in list" :key="p.id">{{p.name}} -- {{p.age}}</li></ul></div>
</template><script lang="ts" setup name="Person">import {reactive, withDefaults} from 'vue'// 引入接口 或 自定义类型 的时候, 需要在前面加上type; import { type PersonInter, type Persons } from '@/types'// 不推荐的写法, 但可用let personList:Persons = reactive([{id: 'a01', name: 'john', age:60}])// 推荐的写法, 意为: personList2这个变量须符合 Persons 类型的规范let personList2 = reactive<Persons>([{id: 'a01', name: 'john', age:60}])// 推荐的写法, 意为: personList3这个变量须符合 PersonInter[] 类型的规范let personList3 = reactive<PersonInter[]>([{id: 'a01', name: 'john', age:60}])// 只接收// 定义接收父组件传过来的a属性, 并赋值给props以便于访问。并且defineProps只能使用1次/* let props = defineProps(['a', 'b'])// 在js代码中使用props.a来访问父组件传过来的a属性对应的值, 在模板中直接使用a来访问父组件传过来的a属性对应的值console.log(props.a); */    // 接收 + 限制类型 + 限制必要性// (list2可不传; list必须传, 并且必须是Persons类型的)/* let props = defineProps<{list:Persons, list2?:Persons}>()console.log(props.list);  */// 接收 + 限制类型 + 限制必要性 + 指定默认值// (list属性可不传, 如果没有传的话, 就是用下面默认定义的数据)const props = withDefaults(defineProps<{list?: Persons}>(),{list: () => [{id:'A001',name:'张三',age:18}]})console.log(props.list);</script><style scoped></style>

3.14 生命周期

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

  • 规律:

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

  • Vue2的生命周期

    创建阶段:beforeCreatecreated

    挂载阶段:beforeMountmounted

    更新阶段:beforeUpdateupdated

    销毁阶段:beforeDestroydestroyed

  • Vue3的生命周期

    创建阶段:setup(替代了之前vue2中的beforeCreate、created)

    挂载阶段:onBeforeMountonMounted

    更新阶段:onBeforeUpdateonUpdated

    卸载阶段:onBeforeUnmountonUnmounted(就对应vue2中的销毁阶段)

  • 常用的钩子:onMounted(挂载完毕)、onUpdated(更新完毕)、onBeforeUnmount(卸载之前)

App.vue

<template><Person v-if="isShow"/>
</template><script lang="ts" setup name="App">import Person from './components/Person.vue'import {ref,onMounted} from 'vue'let isShow = ref(true)// 挂载完毕(先子组件挂载完毕, 再父挂载完毕)onMounted(()=>{console.log('父---挂载完毕')})</script>

Person.vue

<template><div class="person"><h2>当前求和为:{{ sum }}</h2><button @click="add">点我sum+1</button></div>
</template><script lang="ts" setup name="Person">import {ref,onBeforeMount, onMounted,onBeforeUpdate, onUpdated,onBeforeUnmount, onUnmounted } from 'vue'// 数据let sum = ref(0)// 方法function add(){sum.value += 1}// 创建(替代了之前vue2中的beforeCreate、created)console.log('创建')// 挂载前(这里面传入的函数由vue3帮我们调用, 这里只是将这个函数注册进去)onBeforeMount(()=>{// console.log('挂载前')})// 挂载完毕onMounted(()=>{console.log('子---挂载完毕')})// 更新前onBeforeUpdate(()=>{// console.log('更新前')})// 更新完毕onUpdated(()=>{// console.log('更新完毕')})// 卸载前onBeforeUnmount(()=>{// console.log('卸载前')})// 卸载完毕onUnmounted(()=>{// console.log('卸载完毕')})
</script>

3.15 自定义hooks

未使用hooks前

App.vue
<template><Person />
</template><script lang="ts" setup name="App">
import Person from './components/Person.vue'
</script>
Person.vue
<template><div class="person"><h2>当前求和为:{{ sum }},放大10倍后:{{ bigSum }}</h2><button @click="add">点我sum+1</button><hr><img v-for="(dog, index) in dogList" :src="dog" :key="index"><button @click="getDog">再来一只小狗</button></div>
</template><script lang="ts" setup name="Person">import { ref, reactive, onMounted, computed } from 'vue'import axios from 'axios'// ---- 求和// 数据let sum = ref(0)let bigSum = computed(() => {return sum.value * 10})// 方法function add() {sum.value += 1}// 钩子onMounted(() => {add()})// --- 发起请求获取图片// 数据let dogList = reactive(['https://images.dog.ceo/breeds/pembroke/n02113023_4373.jpg'])// 方法async function getDog() {try {let result = await axios.get('https://dog.ceo/api/breed/pembroke/images/random')dogList.push(result.data.message)} catch (error) {alert(error)}}// 钩子onMounted(() => {getDog()})</script><style scoped></style>

使用hooks

vue3本身就推荐使用组合式api,但是如果各种功能都放到setup里面,显得就有点乱了,所以,使用hooks将单独的功能所使用的各种数据、方法等抽离出去,当需要某个功能时,再引入进来。

hooks中不仅可以定义数据,还可以使用声明周期钩子函数,还可以写计算属性。

App.vue
<template><Person />
</template><script lang="ts" setup name="App">
import Person from './components/Person.vue'
</script>
Person.vue
<template><div class="person"><h2>当前求和为:{{ sum }},放大10倍后:{{ bigSum }}</h2><button @click="add">点我sum+1</button><hr><img v-for="(dog,index) in dogList" :src="dog" :key="index"><br><button @click="getDog">再来一只小狗</button></div>
</template><script lang="ts" setup name="Person">import useSum from '@/hooks/useSum'import useDog from '@/hooks/useDog'// 调用函数获得数据const {sum,add,bigSum} = useSum()// 调用函数获得数据const {dogList,getDog} = useDog()</script><style scoped></style>
hooks/useSum.ts
import { ref ,onMounted,computed} from 'vue'// 暴露此函数(默认暴露)
export default function () {// 数据let sum = ref(0)// 这里面也可以写计算属性的哦let bigSum = computed(()=>{return sum.value * 10})// 方法function add() {sum.value += 1}// 钩子(hooks这里面也能写钩子的哦)onMounted(()=>{add()})// 给外部提供东西(要把东西放出去,让外界使用)return {sum,add,bigSum}
}
hooks/useDog.ts
import {reactive,onMounted} from 'vue'
import axios from 'axios'export default function (){// 数据let dogList = reactive(['https://images.dog.ceo/breeds/pembroke/n02113023_4373.jpg'])// 方法async function getDog(){try {let result = await axios.get('https://dog.ceo/api/breed/pembroke/images/random')dogList.push(result.data.message)} catch (error) {alert(error)}}// 钩子(hooks这里面也能写钩子的哦)onMounted(()=>{getDog()})// 向外部提供东西return {dogList,getDog}
}

4.路由

4.1 路由的基本理解

在这里插入图片描述

当路由变化,路由器会监听到此变化,就会根据路由规则找到对应的组件,将这个组件展示在路由出口

在这里插入图片描述

4.2 基本切换效果

安装vue-router

# 现在查看package.json,发现安装的版本是【"vue-router": "^4.3.2"】
# 路由器是用来管理路由的, 并且当路径变化时, 根据路由规则将对应的组件 展示在路由出口处
npm install vue-router

配置路由规则router/index.ts

// 创建一个路由器,并暴露出去// 第一步:引入createRouter
import {createRouter,createWebHistory} from 'vue-router'
// 引入一个一个可能要呈现组件
import Home from '@/components/Home.vue'
import News from '@/components/News.vue'
import About from '@/components/About.vue'// 第二步:创建路由器
const router = createRouter({history:createWebHistory(), //路由器的工作模式(稍后讲解)routes:[ //一个一个的路由规则{path:'/home',component:Home},{path:'/news',component:News},{path:'/about',component:About},]
})// 暴露出去router
export default router

使用router路由管理器main.ts

// 引入createApp用于创建应用
import {createApp} from 'vue'
// 引入App根组件
import App from './App.vue'
// 引入路由器
import router from './router'// 创建一个应用
const app = createApp(App)
// 使用路由器
app.use(router)
// 挂载整个应用到app容器中
app.mount('#app')

路由展示区App.vue

<template><div class="app"><h2 class="title">Vue路由测试</h2><!-- 导航区, 使用<router-link>标签来切换路由路径 --><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><!-- 展示区 , 使用<Router-view>标签作为路由出口 --><div class="main-content"><RouterView></RouterView></div></div>
</template><script lang="ts" setup name="App">import {RouterView,RouterLink} from 'vue-router'</script><style>/* App */.title {text-align: center;word-spacing: 5px;margin: 30px 0;height: 70px;line-height: 70px;background-image: linear-gradient(45deg, gray, white);border-radius: 10px;box-shadow: 0 0 2px;font-size: 30px;}.navigate {display: flex;justify-content: space-around;margin: 0 100px;}.navigate a {display: block;text-align: center;width: 90px;height: 40px;line-height: 40px;border-radius: 10px;background-color: gray;text-decoration: none;color: white;font-size: 18px;letter-spacing: 5px;}.navigate a.active {background-color: #64967E;color: #ffc268;font-weight: 900;text-shadow: 0 0 1px black;font-family: 微软雅黑;}.main-content {margin: 0 auto;margin-top: 30px;border-radius: 10px;width: 90%;height: 400px;border: 1px solid;}
</style>

路由组件

Home.vue
<template><div class="home"><img src="http://www.atguigu.com/images/index_new/logo.png" alt=""></div>
</template><script setup lang="ts" name="Home"></script><style scoped>.home {display: flex;justify-content: center;align-items: center;height: 100%;}
</style>
New.vue
<template><div class="news"><ul><li><a href="#">新闻001</a></li><li><a href="#">新闻002</a></li><li><a href="#">新闻003</a></li><li><a href="#">新闻004</a></li></ul></div>
</template><script setup lang="ts" name="News"></script><style scoped>
/* 新闻 */
.news {padding: 0 20px;display: flex;justify-content: space-between;height: 100%;
}
.news ul {margin-top: 30px;list-style: none;padding-left: 10px;
}
.news li>a {font-size: 18px;line-height: 40px;text-decoration: none;color: #64967E;text-shadow: 0 0 1px rgb(0, 84, 0);
}
.news-content {width: 70%;height: 90%;border: 1px solid;margin-top: 20px;border-radius: 10px;
}
</style>
About.vue
<template><div class="about"><h2>大家好,欢迎来到尚硅谷直播间</h2></div>
</template><script setup lang="ts" name="About"></script><style scoped>
.about {display: flex;justify-content: center;align-items: center;height: 100%;color: rgb(85, 84, 84);font-size: 18px;
}
</style>

路由切换效果图

在这里插入图片描述

4.3. 两个注意点

1、路由组件通常存放在pagesviews文件夹,一般组件通常存放在components文件夹。

2、通过点击导航,视觉效果上“消失” 了的路由组件,默认是被卸载掉的,需要的时候再去挂载

About.vue

当通过切换路由路径的方式而控制About.vue组件的显示和隐藏时,会分别执行onMounted 和 onUnmounted 中定义的函数

<template><div class="about"><h2>大家好,欢迎来到尚硅谷直播间</h2></div></template><script setup lang="ts" name="About">import {onMounted,onUnmounted} from 'vue'// 挂载时执行的函数onMounted(()=>{console.log('About组件挂载了')})// 卸载时执行的函数onUnmounted(()=>{console.log('About组件卸载了')})
</script><style scoped>.about {display: flex;justify-content: center;align-items: center;height: 100%;color: rgb(85, 84, 84);font-size: 18px;}
</style>

4.4. 路由器工作模式

  1. history模式

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

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

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

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

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

    const router = createRouter({history:createWebHashHistory(), //hash模式/******/
    })
    

4.5. to的两种写法

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

4.6. 命名路由

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

给路由规则命名:

// 创建一个路由器,并暴露出去// 第一步:引入createRouter
import {createRouter,createWebHistory,createWebHashHistory} 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:createWebHashHistory(), //路由器的工作模式(稍后讲解)routes:[ //一个一个的路由规则{name:'zhuye',path:'/home',component:Home},{name:'xinwen',path:'/news',component:News},{name:'guanyu',path:'/about',component:About},]
})// 暴露出去router
export default router

跳转路由:

<template><div class="app"><Header/><!-- 导航区 --><div class="navigate"><!--简化前:需要写完整的路径(to的字符串写法) --><RouterLink to="/home" active-class="active">首页</RouterLink><!--简化后:直接通过路由规则中定义的路由的名字(route的name属性)跳转(to的对象写法配合name属性) --><RouterLink :to="{name:'xinwen'}" active-class="active">新闻</RouterLink><RouterLink :to="{path:'/about'}" active-class="active">关于</RouterLink></div><!-- 展示区 --><div class="main-content"><RouterView></RouterView></div></div>
</template><script lang="ts" setup name="App">import {RouterView,RouterLink} from 'vue-router'import Header from './components/Header.vue'</script>

4.7 嵌套路由

main.ts

// 引入createApp用于创建应用
import {createApp} from 'vue'
// 引入App根组件
import App from './App.vue'
// 引入路由器
import router from './router'// 创建一个应用
const app = createApp(App)
// 使用路由器
app.use(router)
// 挂载整个应用到app容器中
app.mount('#app')

router/index.ts

当访问/news/detail时,先根据路由规则匹配到News组件,这个News组件应该要展示在App.vue中的路由出口处,然后匹配到子级路由找到Detail.vue,然后将Detail.vue组件展示在News组件的路由出口处。

// 创建一个路由器,并暴露出去// 第一步:引入createRouter
import {createRouter,createWebHistory,createWebHashHistory} from 'vue-router'
// 引入一个一个可能要呈现组件
import Home from '@/pages/Home.vue'
import News from '@/pages/News.vue'
import About from '@/pages/About.vue'
import Detail from '@/pages/Detail.vue'// 第二步:创建路由器
const router = createRouter({history:createWebHistory(), //路由器的工作模式(稍后讲解)routes:[ //一个一个的路由规则{name:'zhuye',path:'/home',component:Home},{name:'xinwen',path:'/news',component:News,children:[{path:'detail',component:Detail}]},{name:'guanyu',path:'/about',component:About},]
})// 暴露出去router
export default router

App.vue

在App.vue中有1个路由出口(一级路由出口)

<template><div class="app"><Header/><!-- 导航区 --><div class="navigate"><RouterLink to="/home" active-class="active">首页</RouterLink><RouterLink :to="{name:'xinwen'}" active-class="active">新闻</RouterLink><RouterLink :to="{path:'/about'}" active-class="active">关于</RouterLink></div><!-- 展示区 --><div class="main-content"><RouterView></RouterView></div></div>
</template><script lang="ts" setup name="App">import {RouterView,RouterLink} from 'vue-router'import Header from './components/Header.vue'</script><style>/* App */.navigate {display: flex;justify-content: space-around;margin: 0 100px;}.navigate a {display: block;text-align: center;width: 90px;height: 40px;line-height: 40px;border-radius: 10px;background-color: gray;text-decoration: none;color: white;font-size: 18px;letter-spacing: 5px;}.navigate a.active {background-color: #64967E;color: #ffc268;font-weight: 900;text-shadow: 0 0 1px black;font-family: 微软雅黑;}.main-content {margin: 0 auto;margin-top: 30px;border-radius: 10px;width: 90%;height: 400px;border: 1px solid;}
</style>

News.vue

在News.vue中有1个子级路由出口

<template><div class="news"><!-- 导航区 --><ul><li v-for="news in newsList" :key="news.id"><RouterLink to="/news/detail">{{news.title}}</RouterLink></li></ul><!-- 展示区 --><div class="news-content"><RouterView></RouterView></div></div>
</template><script setup lang="ts" name="News">import {reactive} from 'vue'import {RouterView,RouterLink} from 'vue-router'const newsList = reactive([{id:'asfdtrfay01',title:'很好的抗癌食物',content:'西蓝花'},{id:'asfdtrfay02',title:'如何一夜暴富',content:'学IT'},{id:'asfdtrfay03',title:'震惊,万万没想到',content:'明天是周一'},{id:'asfdtrfay04',title:'好消息!好消息!',content:'快过年了'}])</script><style scoped>
/* 新闻 */
.news {padding: 0 20px;display: flex;justify-content: space-between;height: 100%;
}
.news ul {margin-top: 30px;list-style: none;padding-left: 10px;
}
.news li>a {font-size: 18px;line-height: 40px;text-decoration: none;color: #64967E;text-shadow: 0 0 1px rgb(0, 84, 0);
}
.news-content {width: 70%;height: 90%;border: 1px solid;margin-top: 20px;border-radius: 10px;
}
</style>

Detail.vue

<template><ul class="news-list"><li>编号:xxx</li><li>标题:xxx</li><li>内容:xxx</li></ul>
</template><script setup lang="ts" name="About"></script><style scoped>.news-list {list-style: none;padding-left: 20px;}.news-list>li {line-height: 30px;}
</style>

效果

可以看到在App.vue中有1个路由出口,在News.vue中也有1个路由出口

在这里插入图片描述

4.8 路由传参

query参数

1.定义路由规则

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}]
})

2.传递参数

<!-- 跳转并携带query参数(to的字符串写法) -->
<router-link to="/news/detail?a=1&b=2&content=欢迎你">跳转
</router-link><!-- 跳转并携带query参数(to的对象写法) -->
<RouterLink :to="{//name:'xiang', //用name也可以跳转path:'/news/detail',query:{id:news.id,title:news.title,content:news.content}}"
>{{news.title}}
</RouterLink>

3.接收参数:

import {useRoute} from 'vue-router'
import {toRefs} from 'vue' const route = useRoute()// 从1个响应式对象直接解构属性(route是响应式对象),会丢失响应式
// 然后试图在模板中使用此query, 发现点击不同的新闻时数据没有变化, 因为在解构时这里已经丢失了响应式了
// 应该使用toRefs
// const {query} = route // 应该如下使用toRefs
// 然后在模板中使用, 发现点击不同的新闻时, 数据有了变化
const {query} = toRefs(route)// 打印query参数
console.log(route.query)

params参数

  1. 定义路由规则,并定义路由路径params参数
const router = createRouter({history:createWebHistory(), //路由器的工作模式(稍后讲解)routes:[ //一个一个的路由规则{name:'zhuye',path:'/home',component:Home},{name:'xinwen',path:'/news',component:News,children:[{name:'xiang',// 添加路径参数来占位path:'detail/:id/:title/:content?', // 这里加个问号的意思是可传可不传, 否则必须传component:Detail}]},{name:'guanyu',path:'/about',component:About}]
})
  1. 传递参数
<!-- 跳转并携带params参数(to的字符串写法) -->
<RouterLink :to="`/news/detail/001/新闻001/内容001`">{{news.title}}</RouterLink><!-- 跳转并携带params参数(to的对象写法) -->
<RouterLink :to="{name:'xiang', // 用name跳转, 注意这里不能用path, 并且下面的params的属性对应的值不能是对象或数组params:{id:news.id,title:news.title,content:news.title}}"
>{{news.title}}
</RouterLink>
  1. 接收参数:
// useRoute是hooks钩子
import {useRoute} from 'vue-router'const route = useRoute()// 从1个响应式对象直接解构属性(route是响应式对象),会丢失响应式
// 然后试图在模板中使用此query, 发现点击不同的新闻时数据没有变化, 因为在解构时这里已经丢失了响应式了
// 应该使用toRefs
// const {params} = route // 应该如下使用toRefs
// 然后在模板中使用, 发现点击不同的新闻时, 数据有了变化
const {params} = toRefs(route)// 打印params参数
console.log(route.params)

备注1:传递params参数时,若使用to的对象写法,必须使用name配置项,不能用path

备注2:传递params参数时,需要提前在规则中占位。

4.9 路由的props配置

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

{name:'xiang',path:'detail/:id/:title/:content',component:Detail,// 第一种写法:将路由收到的【所有params参数】作为props传给路由组件// props的布尔值写法,作用:把收到了每一组params参数,作为props传给Detail组件,// (类似于: <Detail :id='xx' :title='xx' :content='xx' />)// 这样在Detail组件中通过defineProps(['id','title','content'])声明属性, // 然后在模板中直接使用id,title,content就可以访问这些属性了// props:true// 第二种写法:函数写法,可以自己决定将什么作为props给路由组件// props的函数写法,作用:把返回的对象中每一组key-value作为props传给Detail组件// 这里的形参可以不叫route, 换成其它任何名字都代表路由对象// 这样在Detail组件中通过defineProps(['k'])声明属性, // 然后在模板中直接使用k就可以访问k属性对应的值了, route.query中的属性也是一样props(route){return {...route.query, k:'v'}}// 第三种写法:对象写法,可以自己决定将什么作为props给路由组件// props的对象写法,作用:把对象中的每一组key-value作为props传给Detail组件// props:{a:1,b:2,c:3}, // 以上写法请注意, 都是在指定Detail作为路由组件展示在路由出口时, 给该【路由组件】传递的props, // 注意与直接使用<Detail/>标签的形式的【一般组件】区别开来
}

4.10 replace属性

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

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

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

    <RouterLink replace to='/news/detail/1'>News</RouterLink>
    

示例

<template><div class="app"><Header/><!-- 导航区 --><div class="navigate"><RouterLink to="/home" active-class="active">首页</RouterLink><RouterLink replace :to="{name:'xinwen'}" active-class="active">新闻</RouterLink><RouterLink replace :to="{path:'/about'}" active-class="active">关于</RouterLink></div><!-- 展示区 --><div class="main-content"><RouterView></RouterView></div></div>
</template>

4.11 编程式导航

路由组件的两个重要的属性:$route$router变成了两个hooks

import {useRoute,useRouter} from 'vue-router'const route = useRoute()
const router = useRouter()console.log(route.query)
console.log(route.parmas)// <RouterLink to=''/>标签中的to属性能怎么写, 那么router.push(..)中的参数就能怎么写
console.log(router.push) 
console.log(router.replace)

示例

<template><div class="news"><!-- 导航区 --><ul><li v-for="news in newsList" :key="news.id"><button @click="showNewsDetail(news)">查看新闻</button><RouterLink :to="{name:'xiang',query:{id:news.id,title:news.title,content:news.content}}">{{news.title}}</RouterLink></li></ul><!-- 展示区 --><div class="news-content"><RouterView></RouterView></div></div>
</template><script setup lang="ts" name="News">import {reactive} from 'vue'import {RouterView,RouterLink,useRouter} from 'vue-router'const newsList = reactive([{id:'asfdtrfay01',title:'很好的抗癌食物',content:'西蓝花'},{id:'asfdtrfay02',title:'如何一夜暴富',content:'学IT'},{id:'asfdtrfay03',title:'震惊,万万没想到',content:'明天是周一'},{id:'asfdtrfay04',title:'好消息!好消息!',content:'快过年了'}])const router = useRouter()interface NewsInter {id:string,title:string,content:string}function showNewsDetail(news:NewsInter){router.replace({name:'xiang',query:{id:news.id,title:news.title,content:news.content}})}</script><style scoped>
/* 新闻 */
.news {padding: 0 20px;display: flex;justify-content: space-between;height: 100%;
}
.news ul {margin-top: 30px;/* list-style: none; */padding-left: 10px;
}
.news li::marker {color: #64967E;
}
.news li>a {font-size: 18px;line-height: 40px;text-decoration: none;color: #64967E;text-shadow: 0 0 1px rgb(0, 84, 0);
}
.news-content {width: 70%;height: 90%;border: 1px solid;margin-top: 20px;border-radius: 10px;
}
</style>

4.12 重定向

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

  2. 具体编码:

    {path:'/',redirect:'/about'
    }
    

示例

// 创建一个路由器,并暴露出去// 第一步:引入createRouter
import {createRouter,createWebHistory,createWebHashHistory} from 'vue-router'// 引入一个一个可能要呈现组件
import Home from '@/pages/Home.vue'
import News from '@/pages/News.vue'
import About from '@/pages/About.vue'
import Detail from '@/pages/Detail.vue'// 第二步:创建路由器
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,props(route){return route.query}}]},{name:'guanyu',path:'/about',component:About},{path:'/',// 使用重定向, 当用户访问/时, 跳转到/home// 即: 让指定的路径重新定位到另一个路径redirect:'/home'}]
})// 暴露出去router
export default router

5. pinia

5.1 准备一个效果

pinia_example

main.ts

// 引入createApp用于创建应用
import {createApp} from 'vue'// 引入App根组件
import App from './App.vue'// 创建一个应用
const app = createApp(App)// 挂载整个应用到app容器中
app.mount('#app')

App.vue

<template><Count/><br><LoveTalk/>
</template><script setup lang="ts" name="App">import Count from './components/Count.vue'import LoveTalk from './components/LoveTalk.vue'
</script>

Count.vue

<template><div class="count"><h2>当前求和为:{{ sum }}</h2><!-- 如果不写.number, 那么绑定所获取的值是字符串 --><!-- 当然也可以这样使用v-bind来绑定, 如: <option :value="1">1</option> --><select v-model.number="n"><option value="1">1</option><option value="2">2</option><option value="3">3</option></select><button @click="add"></button><button @click="minus"></button></div>
</template><script setup lang="ts" name="Count">import { ref } from "vue";// 数据let sum = ref(1) // 当前求和let n = ref(1) // 用户选择的数字// 方法function add(){sum.value += n.value}function minus(){sum.value -= n.value}
</script><style scoped>.count {background-color: skyblue;padding: 10px;border-radius: 10px;box-shadow: 0 0 10px;}select,button {margin: 0 5px;height: 25px;}
</style>

LoveTalk.vue

<template><div class="talk"><button @click="getLoveTalk">获取一句土味情话</button><ul><li v-for="talk in talkList" :key="talk.id">{{talk.title}}</li></ul></div>
</template><script setup lang="ts" name="LoveTalk">import {reactive} from 'vue'import axios from "axios";import {nanoid} from 'nanoid'// 数据let talkList = reactive([{id:'ftrfasdf01',title:'今天你有点怪,哪里怪?怪好看的!'},{id:'ftrfasdf02',title:'草莓、蓝莓、蔓越莓,今天想我了没?'},{id:'ftrfasdf03',title:'心里给你留了一块地,我的死心塌地'}])// 方法async function getLoveTalk(){// 发请求,下面这行的写法是:连续解构赋值+重命名let {data:{content:title}} = await axios.get('https://api.uomg.com/api/rand.qinghua?format=json')// 把请求回来的字符串,包装成一个对象let obj = {id:nanoid(),title}// 放到数组中talkList.unshift(obj)}
</script><style scoped>.talk {background-color: orange;padding: 10px;border-radius: 10px;box-shadow: 0 0 10px;}
</style>

5.2 搭建 pinia 环境

使用步骤

第一步:npm install pinia(此处安装的版本是:“pinia”: “^2.1.7”,)

第二步:操作src/main.ts

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选项

5.3 存储+读取数据

  1. Store是一个保存:状态业务逻辑 的实体,每个组件都可以读取写入它。

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

store/count.ts

import { defineStore } from 'pinia'// defineStore返回的值的命名 格式为: use{文件名}Store
export const useCountStore = defineStore('count', /* 建议这里的名字与文件名保持一直, 首字母小写 */{// 真正存储数据的地方state() { // 这个只能写成1个函数return {sum: 6}}
})

store/loveTalk.ts

import {defineStore} from 'pinia'export const useTalkStore = defineStore('talk',{// 真正存储数据的地方state(){return {talkList:[{id:'ftrfasdf01',title:'今天你有点怪,哪里怪?怪好看的!'},{id:'ftrfasdf02',title:'草莓、蓝莓、蔓越莓,今天想我了没?'},{id:'ftrfasdf03',title:'心里给你留了一块地,我的死心塌地'}]}}
})

Count.vue

<template><div class="count"><!-- 直接使用countStore --><h2>当前求和为:{{ countStore.sum }}</h2><select v-model.number="n"><option value="1">1</option><option value="2">2</option><option value="3">3</option></select><button @click="add"></button><button @click="minus"></button></div>
</template><script setup lang="ts" name="Count">import { ref, reactive } from "vue";import { useCountStore } from '@/store/count'const countStore = useCountStore()// 以下两种方式都可以拿到state中的数据// console.log('@@@',countStore.sum) // 注意: 这里后面不要写.value哦, 因为会自动拆包// console.log('@@@',countStore.$state.sum) // 也可以通过$state拿到sum/*   let obj = reactive({a:1,b:2,c:ref(3)})let x = ref(9)console.log(obj.a)console.log(obj.b)console.log(obj.c) // 注意, 这里最后面就不用.value了*/// 数据let n = ref(1) // 用户选择的数字// 方法function add() {}function minus() {}
</script><style scoped>.count {background-color: skyblue;padding: 10px;border-radius: 10px;box-shadow: 0 0 10px;}select,button {margin: 0 5px;height: 25px;}
</style>

LoveTalk.vue

<template><div class="talk"><button @click="getLoveTalk">获取一句土味情话</button><ul><li v-for="talk in talkStore.talkList" :key="talk.id">{{talk.title}}</li></ul></div>
</template><script setup lang="ts" name="LoveTalk">import {reactive} from 'vue'import axios from "axios";import {nanoid} from 'nanoid'import {useTalkStore} from '@/store/loveTalk'const talkStore = useTalkStore()// 方法async function getLoveTalk(){// 发请求,下面这行的写法是:连续解构赋值+重命名// let {data:{content:title}} = await //                        axios.get('https://api.uomg.com/api/rand.qinghua?format=json')// 把请求回来的字符串,包装成一个对象// let obj = {id:nanoid(),title}// 放到数组中// talkList.unshift(obj)}
</script><style scoped>.talk {background-color: orange;padding: 10px;border-radius: 10px;box-shadow: 0 0 10px;}
</style>

App.vue

<template><Count/><br><LoveTalk/>
</template><script setup lang="ts" name="App">import Count from './components/Count.vue'import LoveTalk from './components/LoveTalk.vue'
</script>

main.ts

import {createApp} from 'vue'
import App from './App.vue'
// 第一步:引入pinia
import {createPinia} from 'pinia'const app = createApp(App)
// 第二步:创建pinia
const pinia = createPinia()
// 第三步:安装pinia
app.use(pinia)
app.mount('#app')

5.4 修改数据(三种方式)

第一种方式

count.ts
import {defineStore} from 'pinia'export const useCountStore = defineStore('count',{// 真正存储数据的地方state(){return {sum:6,school:'atguigu',address:'宏福科技园'}}
})
Count.vue
<template><div class="count"><h2>当前求和为:{{ countStore.sum }}</h2><button @click="add"></button></div>
</template><script setup lang="ts" name="Count">import { ref, reactive } from "vue";// 引入useCountStoreimport { useCountStore } from '@/store/count'// 使用useCountStore,得到一个专门保存count相关的storeconst countStore = useCountStore()// 数据let n = ref(1) // 用户选择的数字// 方法function add() {// 第一种修改方式, 直接拿到countStore去改, 注意: 这和vuex不同, vuex是不能直接修改的countStore.sum += 1countStore.school = '尚硅谷'countStore.address = '北京'}
</script><style scoped>.count {background-color: skyblue;padding: 10px;border-radius: 10px;box-shadow: 0 0 10px;}select,button {margin: 0 5px;height: 25px;}
</style>

第二种方式

count.ts
import {defineStore} from 'pinia'export const useCountStore = defineStore('count',{// 真正存储数据的地方state(){return {sum:6,school:'atguigu',address:'宏福科技园'}}
})
Count.vue
<template><div class="count"><h2>当前求和为:{{ countStore.sum }}</h2><h3>欢迎来到:{{ countStore.school }},坐落于:{{ countStore.address }}</h3><select v-model.number="n"><option value="1">1</option><option value="2">2</option><option value="3">3</option></select><button @click="add"></button><button @click="minus"></button></div>
</template><script setup lang="ts" name="Count">import { ref,reactive } from "vue";// 引入useCountStoreimport {useCountStore} from '@/store/count'// 使用useCountStore,得到一个专门保存count相关的storeconst countStore = useCountStore()// 数据let n = ref(1) // 用户选择的数字// 方法function add(){// 第二种修改方式(如果很多数据都要统一一次性发生变化,推荐使用$patch)countStore.$patch({sum:888,school:'尚硅谷',address:'北京'})}function minus(){}
</script><style scoped>.count {background-color: skyblue;padding: 10px;border-radius: 10px;box-shadow: 0 0 10px;}select,button {margin: 0 5px;height: 25px;}
</style>

第三种方式

count.ts
import {defineStore} from 'pinia'export const useCountStore = defineStore('count',{// actions里面放置的是一个一个的方法,用于响应组件中的“动作”// (使用actions的意义在于可以将对组件共享数据统一操作的逻辑抽取放到这里)actions:{increment(value){ // value是调用方传过来的值console.log('increment被调用了',value)if( this.sum < 10){// 修改数据(this是当前的store)this.sum += value}}},// 真正存储数据的地方state(){return {sum:6,school:'atguigu',address:'宏福科技园'}}
})
Count.vue
<template><div class="count"><h2>当前求和为:{{ countStore.sum }}</h2><h3>欢迎来到:{{ countStore.school }},坐落于:{{ countStore.address }}</h3><select v-model.number="n"><option value="1">1</option><option value="2">2</option><option value="3">3</option></select><button @click="add"></button><button @click="minus"></button></div>
</template><script setup lang="ts" name="Count">import { ref,reactive } from "vue";// 引入useCountStoreimport {useCountStore} from '@/store/count'// 使用useCountStore,得到一个专门保存count相关的storeconst countStore = useCountStore()// 数据let n = ref(1) // 用户选择的数字// 方法function add(){// 第三种修改方式(直接调用count.ts中定义的actions方法)const result = countStore.increment(n.value)console.log('result', result); // result undefined}function minus(){}
</script><style scoped>.count {background-color: skyblue;padding: 10px;border-radius: 10px;box-shadow: 0 0 10px;}select,button {margin: 0 5px;height: 25px;}
</style>

5.5 storeToRefs用法

  • 借助storeToRefsstore中的数据转为ref对象,方便在模板中使用。
  • 注意:pinia提供的storeToRefs只会将数据做转换,而VuetoRefs会转换store中数据(虽然能实现功能,单不建议使用哦)。

LoveTalk.ts

import {defineStore} from 'pinia'
import axios from 'axios'
import {nanoid} from 'nanoid'export const useTalkStore = defineStore('talk',{actions:{async getATalk(){// 发请求,下面这行的写法是:连续解构赋值+重命名let {data:{content:title}} = await axios.get('https://api.uomg.com/api/rand.qinghua?format=json')// 把请求回来的字符串,包装成一个对象let obj = {id:nanoid(),title}// 放到数组中this.talkList.unshift(obj)}},// 真正存储数据的地方state(){return {talkList:[{id:'ftrfasdf01',title:'今天你有点怪,哪里怪?怪好看的!'},{id:'ftrfasdf02',title:'草莓、蓝莓、蔓越莓,今天想我了没?'},{id:'ftrfasdf03',title:'心里给你留了一块地,我的死心塌地'}]}}
})

LoveTask.vue

<template><div class="talk"><button @click="getLoveTalk">获取一句土味情话</button><ul><li v-for="talk in talkList" :key="talk.id">{{talk.title}}</li></ul></div>
</template><script setup lang="ts" name="LoveTalk">import {useTalkStore} from '@/store/loveTalk'import { storeToRefs } from "pinia";const talkStore = useTalkStore()// 这里如果直接这样解构写: const {talkList} = taskStore; 那么此时这里的talkList就已经丢失了响应式// 这里虽然也可以写:  const {talkList} = toRefs(taskStore); 虽然可以维持talkList的响应式, 但代价过大,//                 (toRefs会把talkStore中的全部数据包括函数,state啥的都给包了一遍)// 所以最好使用storeToRefs, 因为storeToRefs只会关注sotre中数据,不会对方法进行ref包裹const {talkList} = storeToRefs(talkStore)// 方法function getLoveTalk(){talkStore.getATalk()}
</script><style scoped>.talk {background-color: orange;padding: 10px;border-radius: 10px;box-shadow: 0 0 10px;}
</style>

count.ts

import {defineStore} from 'pinia'export const useCountStore = defineStore('count',{// actions里面放置的是一个一个的方法,用于响应组件中的“动作”actions:{increment(value:number){console.log('increment被调用了',value)if( this.sum < 10){// 修改数据(this是当前的store)this.sum += value}}},// 真正存储数据的地方state(){return {sum:1,school:'atguigu',address:'宏福科技园'}}
})

Count.vue

<template><div class="count"><h2>当前求和为:{{ sum }}</h2><h3>欢迎来到:{{ school }},坐落于:{{ address }}</h3><select v-model.number="n"><option value="1">1</option><option value="2">2</option><option value="3">3</option></select><button @click="add"></button><button @click="minus"></button></div>
</template><script setup lang="ts" name="Count">import { ref,reactive,toRefs } from "vue";import {storeToRefs} from 'pinia'// 引入useCountStoreimport {useCountStore} from '@/store/count'// 使用useCountStore,得到一个专门保存count相关的storeconst countStore = useCountStore()// storeToRefs只会关注sotre中数据,不会对方法进行ref包裹const {sum,school,address} = storeToRefs(countStore)// console.log('!!!!!',storeToRefs(countStore))// 数据let n = ref(1) // 用户选择的数字// 方法function add(){countStore.increment(n.value)}function minus(){countStore.sum -= n.value}
</script><style scoped>.count {background-color: skyblue;padding: 10px;border-radius: 10px;box-shadow: 0 0 10px;}select,button {margin: 0 5px;height: 25px;}
</style>

5.6 getters用法

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

count.ts

import {defineStore} from 'pinia'export const useCountStore = defineStore('count',{// actions里面放置的是一个一个的方法,用于响应组件中的“动作”actions:{increment(value:number){console.log('increment被调用了',value)if( this.sum < 10){// 修改数据(this是当前的store)this.sum += value}}},// 真正存储数据的地方state(){return {sum:3,school:'atguigu',address:'宏福科技园'}},getters:{bigSum:state => state.sum * 10,upperSchool():string{return this.school.toUpperCase()}}
})

Count.vue

<template><div class="count"><h2>当前求和为:{{ sum }},放大10倍后:{{ bigSum }}</h2><h3>欢迎来到:{{ school }},坐落于:{{ address }},大写:{{ upperSchool }}</h3><select v-model.number="n"><option value="1">1</option><option value="2">2</option><option value="3">3</option></select><button @click="add"></button><button @click="minus"></button></div>
</template><script setup lang="ts" name="Count">import { ref,reactive,toRefs } from "vue";import {storeToRefs} from 'pinia'// 引入useCountStoreimport {useCountStore} from '@/store/count'// 使用useCountStore,得到一个专门保存count相关的storeconst countStore = useCountStore()// storeToRefs只会关注sotre中数据,不会对方法进行ref包裹, 并且同时维持解构属性结果的响应式// (可以直接解构出state和getters中定义的数据)const {sum,school,address,bigSum,upperSchool} = storeToRefs(countStore)// console.log('!!!!!',storeToRefs(countStore))// 数据let n = ref(1) // 用户选择的数字// 方法function add(){countStore.increment(n.value)}function minus(){countStore.sum -= n.value}
</script><style scoped>.count {background-color: skyblue;padding: 10px;border-radius: 10px;box-shadow: 0 0 10px;}select,button {margin: 0 5px;height: 25px;}
</style>

5.7 $subscribe的使用

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

loveTalk.ts

import {defineStore} from 'pinia'
import axios from 'axios'
import {nanoid} from 'nanoid'export const useTalkStore = defineStore('talk',{actions:{async getATalk(){// 发请求,下面这行的写法是:连续解构赋值+重命名let {data:{content:title}} = await axios.get('https://api.uomg.com/api/rand.qinghua?format=json')// 把请求回来的字符串,包装成一个对象let obj = {id:nanoid(),title}// 放到数组中this.talkList.unshift(obj)}},// 真正存储数据的地方state(){return {talkList:JSON.parse(localStorage.getItem('talkList') as string) || []}}
})

LoveTalk.vue

<template><div class="talk"><button @click="getLoveTalk">获取一句土味情话</button><ul><li v-for="talk in talkList" :key="talk.id">{{ talk.title }}</li></ul></div>
</template><script setup lang="ts" name="LoveTalk">import { useTalkStore } from '@/store/loveTalk'import { storeToRefs } from "pinia";const talkStore = useTalkStore()const { talkList } = storeToRefs(talkStore)talkStore.$subscribe((mutate, state) => {// 注意: 箭头函数中没有thisconsole.log('talkStore里面保存的数据发生了变化', mutate, state)// 实现页面刷新时, 这里的talkList不丢失, 因为在loveTalk.ts中会取localStorage中读取talkList数据localStorage.setItem('talkList', JSON.stringify(state.talkList))})// 方法function getLoveTalk() {talkStore.getATalk()}
</script><style scoped>.talk {background-color: orange;padding: 10px;border-radius: 10px;box-shadow: 0 0 10px;}
</style>

5.8 store组合式写法

loveTalk.js

import {defineStore} from 'pinia'
import axios from 'axios'
import {nanoid} from 'nanoid'/* export const useTalkStore = defineStore('talk',{actions:{async getATalk(){// 发请求,下面这行的写法是:连续解构赋值+重命名let {data:{content:title}} = await axios.get('https://api.uomg.com/api/rand.qinghua?format=json')// 把请求回来的字符串,包装成一个对象let obj = {id:nanoid(),title}// 放到数组中this.talkList.unshift(obj)}},// 真正存储数据的地方state(){return {talkList:JSON.parse(localStorage.getItem('talkList') as string) || []}}
})*/import {reactive} from 'vue'
export const useTalkStore = defineStore('talk',()=>{// talkList就是stateconst talkList = reactive(JSON.parse(localStorage.getItem('talkList') as string) || [])// getATalk函数相当于actionasync 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}
})

LoveTalk.vue

<template><div class="talk"><button @click="getLoveTalk">获取一句土味情话</button><ul><li v-for="talk in talkList" :key="talk.id">{{talk.title}}</li></ul></div>
</template><script setup lang="ts" name="LoveTalk">import {useTalkStore} from '@/store/loveTalk'import { storeToRefs } from "pinia";const talkStore = useTalkStore()const {talkList} = storeToRefs(talkStore)talkStore.$subscribe((mutate,state)=>{console.log('talkStore里面保存的数据发生了变化',mutate,state)localStorage.setItem('talkList',JSON.stringify(state.talkList))})// 方法function getLoveTalk(){talkStore.getATalk()}
</script><style scoped>.talk {background-color: orange;padding: 10px;border-radius: 10px;box-shadow: 0 0 10px;}
</style>

6. 组件通信

6.1 props

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

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

这种不适合父子孙中父给孙组件传递数据,或者兄弟组件也可以找到同1个父组件来实现兄弟组件通信

Father.vue

<template><div class="father"><h3>父组件</h3><h4>汽车:{{ car }}</h4><h4 v-show="toy">子给的玩具:{{ toy }}</h4><Child :car="car" :sendToy="getToy" /></div>
</template><script setup lang="ts" name="Father">import Child from './Child.vue'import { ref } from 'vue'// 数据let car = ref('奔驰')let toy = ref('')// 方法function getToy(value: string) {toy.value = value}</script><style scoped>.father {background-color: rgb(165, 164, 164);padding: 20px;border-radius: 10px;}
</style>

Child.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('奥特曼')// 声明接收propsdefineProps(['car', 'sendToy'])</script><style scoped>.child {background-color: skyblue;padding: 10px;box-shadow: 0 0 10px black;border-radius: 10px;}
</style>

6.2 自定义事件

Father.vue

<template><div class="father"><h3>父组件</h3><h4 v-show="toy">子给的玩具:{{ toy }}</h4><!-- 给子组件Child绑定事件 --><Child @send-toy="saveToy" /></div>
</template><script setup lang="ts" name="Father">import Child from './Child.vue'import { ref } from "vue";// 数据let toy = ref('')// 用于保存传递过来的玩具function saveToy(value: string,e:any) {console.log('saveToy', value, e)toy.value = value}</script><style scoped>.father {background-color: rgb(165, 164, 164);padding: 20px;border-radius: 10px;}.father button {margin-right: 5px;}
</style>

Child.vue

<template><div class="child"><h3>子组件</h3><h4>玩具:{{ toy }}</h4><!-- 在模板中可以使用$event来代表事件对象 --><button @click="emit('send-toy', toy, $event)">测试</button></div>
</template><script setup lang="ts" name="Child">import { ref } from "vue";// 数据let toy = ref('奥特曼')// 声明事件const emit = defineEmits(['send-toy'])</script><style scoped>.child {margin-top: 10px;background-color: rgb(76, 209, 76);padding: 10px;box-shadow: 0 0 10px black;border-radius: 10px;}
</style>

6.3 mitt

概述:与消息订阅与发布(pubsub)功能类似,可以实现任意组件间通信。

安装mittnpm install mitt,版本是:“mitt”: “^3.0.1”

emitter.ts

// 引入mitt
import mitt from 'mitt'// 调用mitt得到emitter,emitter能:绑定事件、触发事件
const emitter = mitt()/* 
// 绑定事件
emitter.on('test1',()=>{console.log('test1被调用了')
})
emitter.on('test2',()=>{console.log('test2被调用了')
})// 触发事件
setInterval(() => {emitter.emit('test1')emitter.emit('test2')
}, 1000);setTimeout(() => {// emitter.off('test1')// emitter.off('test2')emitter.all.clear()
}, 3000); 
*/// 暴露emitter
export default emitter

Father.vue

<template><div class="father"><h3>父组件</h3><Child1/><Child2/></div>
</template><script setup lang="ts" name="Father">import Child1 from './Child1.vue'import Child2 from './Child2.vue'
</script><style scoped>.father{background-color:rgb(165, 164, 164);padding: 20px;border-radius: 10px;}.father button{margin-left: 5px;}
</style>

Child1.vue

<template><div class="child1"><h3>子组件1</h3><h4>玩具:{{ toy }}</h4><button @click="emitter.emit('send-toy',toy)">玩具给弟弟</button></div>
</template><script setup lang="ts" name="Child1">import {ref} from 'vue'import emitter from '@/utils/emitter';// 数据let toy = ref('奥特曼')
</script><style scoped>.child1{margin-top: 50px;background-color: skyblue;padding: 10px;box-shadow: 0 0 10px black;border-radius: 10px;}.child1 button{margin-right: 10px;}
</style>

Child2.vue

<template><div class="child2"><h3>子组件2</h3><h4>电脑:{{ computer }}</h4><h4>哥哥给的玩具:{{ toy }}</h4></div>
</template><script setup lang="ts" name="Child2">import { ref, onUnmounted } from 'vue'import emitter from '@/utils/emitter';// 数据let computer = ref('联想')let toy = ref('')// 给emitter绑定send-toy事件emitter.on('send-toy', (value: any) => {toy.value = value})// 在组件卸载时解绑send-toy事件onUnmounted(() => {emitter.off('send-toy')})
</script><style scoped>.child2 {margin-top: 50px;background-color: orange;padding: 10px;box-shadow: 0 0 10px black;border-radius: 10px;}
</style>

6.4 v-model

Father.vue

<template><div class="father"><h3>父组件</h3><h4>{{ username }}</h4><h4>{{ password }}</h4><!-- v-model用在html标签上 --><!-- <input type="text" v-model="username"> --><!-- <input type="text" :value="username" @input="username = (<HTMLInputElement>$event.target).value"> --><!-- v-model用在组件标签上 --><!-- <AtguiguInput v-model="username"/> --><!-- 上面这行等价于下面这行 --><!-- $event到底是啥? 啥时候能.target对于原生事件, $event就是事件对象 ===> 能.target对于自定义事件, $event就是触发事件时, 所传递的数据 ===> 不能.target--><!-- <AtguiguInput :modelValue="username" @update:modelValue="username = $event"/> --><!-- 修改modelValue --><AtguiguInput v-model:ming="username" v-model:mima="password"/></div>
</template><script setup lang="ts" name="Father">import { ref } from "vue";import AtguiguInput from './AtguiguInput.vue'// 数据let username = ref('zhansgan')let password = ref('123456')
</script><style scoped>.father {padding: 20px;background-color: rgb(165, 164, 164);border-radius: 10px;}
</style>

AtguiguInput.vue

<template><input type="text" :value="ming"@input="emit('update:ming',(<HTMLInputElement>$event.target).value)"><br><input type="text" :value="mima"@input="emit('update:mima',(<HTMLInputElement>$event.target).value)">
</template><script setup lang="ts" name="AtguiguInput">defineProps(['ming','mima'])const emit = defineEmits(['update:ming','update:mima'])</script><style scoped>input {border: 2px solid black;background-image: linear-gradient(45deg,red,yellow,green);height: 30px;font-size: 20px;color: white;}
</style>

6.5 $attrs

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

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

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

    (就是父组件给子组件通过标签的属性方式传递给子组件,子组件使用props的方式只接收了部分属性,其它没有接收的属性可以通过子组件的$attrs来访问)

Father.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" :e="e" 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)let e = ref(5)function updateA(value:number){a.value += value}
</script><style scoped>.father{background-color: rgb(165, 164, 164);padding: 20px;border-radius: 10px;}
</style>

Child.vue

<template><div class="child"><h3>子组件</h3><h4>{{ e }}</h4><!-- Father组件传给Child组件的属性,但是Father组件没有使用props接收的属性,就存在$attrs中 --><h4>{{ $attrs }}</h4><!-- Father组件传给Child组件的属性,但是Father组件没有使用props接收的属性,全部传递给GrandChild组件--><GrandChild v-bind="$attrs"/></div>
</template><script setup lang="ts" name="Child">import GrandChild from './GrandChild.vue'defineProps(['e'])
</script><style scoped>.child{margin-top: 20px;background-color: skyblue;padding: 20px;border-radius: 10px;box-shadow: 0 0 10px black;}
</style>

GrandChild.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><!-- Father组件通过Child组件的v-bind="$attr"将函数传给GrandChild组件,这样GrandChild组件就可以通过此函数传递数据给Father组件了 --><button @click="updateA(6)">点我将爷爷那的a更新</button></div>
</template><script setup lang="ts" name="GrandChild">// 接收Father组件传递过来并由Child组件通过v-bind="$attr"中转过来的属性defineProps(['a','b','c','d','x','y','updateA'])
</script><style scoped>.grand-child{margin-top: 20px;background-color: orange;padding: 20px;border-radius: 10px;box-shadow: 0 0 10px black;}
</style>

6.6 r e f s 、 refs、 refsparent、proxy

  1. 概述:

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

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

Father.vue

<template><div class="father"><h3>父组件</h3><h4>房产:{{ house }}</h4><button @click="changeToy">修改Child1的玩具</button><button @click="changeComputer">修改Child2的电脑</button><!-- 在模板中可以直接使用$refs --><button @click="getAllChild($refs)">让所有孩子的书变多</button><button @click="getAllChild2()">让c1孩子的书变多2</button><button @click="getAllChild3()">让c1孩子的书变多3</button><Child1 ref="c1"/><Child2 ref="c2"/></div>
</template><script setup lang="ts" name="Father">import Child1 from './Child1.vue'import Child2 from './Child2.vue'import { ref,reactive } from "vue";import { getCurrentInstance } from 'vue';const proxy = getCurrentInstance()let c1 = ref()let c2 = ref()// 注意点:当访问obj.c的时候,底层会自动读取value属性,因为c是在obj这个响应式对象中的/* let obj = reactive({a:1,b:2,c:ref(3)})let x = ref(4)console.log(obj.a)console.log(obj.b)console.log(obj.c)console.log(x) */// 数据let house = ref(4)// 方法function changeToy(){// 必须要Child1组件通过defineExpose将toy属性暴露出来, 这样Father组件才能访问到并修改此toy属性c1.value.toy = '小猪佩奇'}function changeComputer(){c2.value.computer = '华为'}function getAllChild(refs:{[key:string]:any}){console.log(refs)for (let key in refs){// 这里不需要refs[key].value.book += 3, 是因为refs本身就是个响应式对象, 它会自动解包refs[key].book += 3}}function getAllChild2(){// 使用getCurrentInstance来访问感觉更加方便console.log(proxy);console.log(proxy.refs);   // {c1: Proxy(Object), c2: Proxy(Object)}console.log(proxy.parent); // {uid: 0, vnode: {…}, type: {…}, parent: null, //                                 appContext: {…}, …}console.log(proxy.attrs);  // {__vInternal: 1}proxy.refs.c1.book += 2}function getAllChild3(){// console.log($refs);      // 注意, 在vue3的setup语法糖中不能直接访问到$refs// console.log(this.$refs); // 注意, 在vue3的setup语法糖中不能直接访问到$refsconsole.log(this.proxy);    // 这个等价于getCurrentInstance()返回的值console.log(this.proxy == proxy);    // trueconsole.log(this.c1);       // 这里可以直接访问到ref='c1'标识的组件this.c1.book += 2}// 向外部提供数据defineExpose({house})</script><style scoped>.father {background-color: rgb(165, 164, 164);padding: 20px;border-radius: 10px;}.father button {margin-bottom: 10px;margin-left: 10px;}
</style>

Child1.vue

<template><div class="child1"><h3>子组件1</h3><h4>玩具:{{ toy }}</h4><h4>书籍:{{ book }} 本</h4><button @click="minusHouse($parent)">干掉父亲的一套房产</button><button @click="minusHouse2()">干掉父亲的一套房产2</button><button @click="minusHouse3()">干掉父亲的一套房产3</button></div></template><script setup lang="ts" name="Child1">import { ref,getCurrentInstance } from "vue";const proxy = getCurrentInstance()// 数据let toy = ref('奥特曼')let book = ref(3)// 方法function minusHouse(parent:any){// 需要Father组件通过defineExpose将house属性暴露出来, 这里才可以访问到parent.house -= 1}function minusHouse2(){// 需要Father组件通过defineExpose将house属性暴露出来, 这里才可以访问到console.log(proxy);console.log(proxy.parent);console.log(proxy.parent.exposed);proxy.parent.exposed.house.value -= 1}function minusHouse3(){// 需要Father组件通过defineExpose将house属性暴露出来, 这里才可以访问到console.log(this); // Proxy(Object) {proxy: {…}, minusHouse: ƒ, minusHouse2: ƒ, …console.log(this.parent); // undefinedconsole.log(this.proxy);  // 这个等价于getCurrentInstance()返回的值console.log(this.proxy == proxy); // true}// 把数据交给外部defineExpose({toy,book})</script><style scoped>.child1{margin-top: 20px;background-color: skyblue;padding: 20px;border-radius: 10px;box-shadow: 0 0 10px black;}
</style>

Child2.vue

<template><div class="child2"><h3>子组件2</h3><h4>电脑:{{ computer }}</h4><h4>书籍:{{ book }} 本</h4></div>
</template><script setup lang="ts" name="Child2">import { ref } from "vue";// 数据let computer = ref('联想')let book = ref(6)// 把数据交给外部defineExpose({ computer, book })</script><style scoped>.child2 {margin-top: 20px;background-color: orange;padding: 20px;border-radius: 10px;box-shadow: 0 0 10px black;}
</style>

6.7 provide、inject

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

  2. 具体使用:

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

Father.vue

<template><div class="father"><h3>父组件</h3><h4>银子:{{ money }}万元</h4><h4>车子:一辆{{car.brand}}车,价值{{car.price}}万元</h4><Child/></div>
</template><script setup lang="ts" name="Father">import Child from './Child.vue'import {ref,reactive,provide} from 'vue'let money = ref(100)let car = reactive({brand:'奔驰',price:100})function updateMoney(value:number){money.value -= value}// 向后代提供数据provide('moneyContext',{money,updateMoney})// (注意数据的后面不要.value, 否则不具备响应式)provide('car',car)</script><style scoped>.father {background-color: rgb(165, 164, 164);padding: 20px;border-radius: 10px;}
</style>

Child.vue

<template><div class="child"><h3>我是子组件</h3><GrandChild/></div>
</template><script setup lang="ts" name="Child">import GrandChild from './GrandChild.vue'
</script><style scoped>.child {margin-top: 20px;background-color: skyblue;padding: 20px;border-radius: 10px;box-shadow: 0 0 10px black;}
</style>

GrandChild.vue

<template><div class="grand-child"><h3>我是孙组件</h3><h4>银子:{{ money }}</h4><h4>车子:一辆{{car.brand}}车,价值{{car.price}}万元</h4><button @click="updateMoney(6)">花爷爷的钱</button></div>
</template><script setup lang="ts" name="GrandChild">import { inject } from "vue";let {money,updateMoney} = inject('moneyContext',{money:0,updateMoney:(param:number)=>{}})// 第二个参数的含义是: 如果没有提供car, 那么就把第二个参数作为默认值(这样可以避免使用car时模板中红色波浪线)let car = inject('car',{brand:'未知',price:0})</script><style scoped>.grand-child{background-color: orange;padding: 20px;border-radius: 10px;box-shadow: 0 0 10px black;}
</style>

6.8 pinia

直接参考pinia章节即可。

6.9 slot插槽

1. 默认插槽

在这里插入图片描述

Father.vue
<template><div class="father"><h3>父组件</h3><div class="content"><Category title="热门游戏列表"><ul><li v-for="g in games" :key="g.id">{{ g.name }}</li></ul></Category><Category title="今日美食城市"><img :src="imgUrl" alt=""></Category><Category title="今日影视推荐"><video :src="videoUrl" controls></video></Category></div></div>
</template><script setup lang="ts" name="Father">import Category from './Category.vue'import { ref,reactive } from "vue";let games = reactive([{id:'asgytdfats01',name:'英雄联盟'},{id:'asgytdfats02',name:'王者农药'},{id:'asgytdfats03',name:'红色警戒'},{id:'asgytdfats04',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>
Category.vue
<template><div class="category"><h2>{{title}}</h2><!-- 1. 如果父组件在使用当前组件时, 父组件标签中没有传入内容, 那么这里就显示“默认内容” 2. 如果这里这里写多个slot, 那么父组件标签中传入的内容就会在每个slot地方都展示一遍3. 其实, 这里省略了name属性, 它的默认值为default, 即这里相当于: <slot name="default">默认内容</slot>--><slot>默认内容</slot><!-- 这里同样会再展示一遍 --><slot name="default">默认内容</slot></div></template><script setup lang="ts" name="Category">defineProps(['title'])</script><style scoped>.category {background-color: skyblue;border-radius: 10px;box-shadow: 0 0 10px;padding: 10px;width: 200px;height: 300px;}h2 {background-color: orange;text-align: center;font-size: 20px;font-weight: 800;}
</style>

2. 具名插槽

Father.vue
<template><div class="father"><h3>父组件</h3><div class="content"><Category><!-- v-slot只能用在组件标签上 或者 <template>标签中 --><template v-slot:s2><ul><!-- Category标签中的内容可以直接使用Father组件中的数据 --><li v-for="g in games" :key="g.id">{{ g.name }}</li></ul></template><template v-slot:s1><h2>热门游戏列表</h2></template></Category><!-- 还可以直接把v-slot直接写在组件上, 它将会把内部的所有内容都塞到s2的插槽中 --><Category v-slot:s2><ul><!-- Category标签中的内容可以直接使用Father组件中的数据 --><li v-for="g in games" :key="g.id">{{ g.name }}</li></ul></Category><Category><template v-slot:s2><img :src="imgUrl" alt=""></template><template v-slot:s1><h2>今日美食城市</h2></template></Category><!-- 简写写法 --><Category><template #s2><!-- Category标签中的内容可以直接使用Father组件中的数据 --><video video :src="videoUrl" controls></video></template><template #s1><h2>今日影视推荐</h2></template></Category></div></div>
</template><script setup lang="ts" name="Father">import Category from './Category.vue'import { ref,reactive } from "vue";let games = reactive([{id:'asgytdfats01',name:'英雄联盟'},{id:'asgytdfats02',name:'王者农药'},{id:'asgytdfats03',name:'红色警戒'},{id:'asgytdfats04',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%;}h2 {background-color: orange;text-align: center;font-size: 20px;font-weight: 800;}
</style>
Category.vue
<template><div class="category"><slot name="s1">默认内容1</slot><slot name="s2">默认内容2</slot></div>
</template><script setup lang="ts" name="Category"></script><style scoped>.category {background-color: skyblue;border-radius: 10px;box-shadow: 0 0 10px;padding: 10px;width: 200px;height: 300px;}
</style>

3. 作用域插槽

理解:数据在组件的自身,但根据数据生成的结构需要组件的使用者来决定。(新闻数据在News组件中,但使用数据所遍历出来的结构由App组件决定)

Father.vue
<template><div class="father"><h3>父组件</h3><div class="content"><Game><!-- 这里的params可以拿到所有子组件中传给<slot>插槽标签的所有属性和对应的值 --><!-- 形成的效果就是: 结构是由父组件决定的, 而数据的提供者是子组件(至于子组件的这个数据哪来的就不用管了, 反正就是有); 或者换句话说: 父组件通过插槽的方式“直接”访问到了子组件通过插槽传递的数据;--><!-- 这里默认其实是:  v-slot:default="params"--><template v-slot="params"><ul><li v-for="y in params.youxi" :key="y.id">{{ y.name }}</li></ul></template></Game><Game><template v-slot="params"><ol><li v-for="item in params.youxi" :key="item.id">{{ item.name }}</li></ol></template></Game><Game><template #default="{youxi}"><h3 v-for="g in youxi" :key="g.id">{{ g.name }}</h3></template></Game></div></div>
</template><script setup lang="ts" name="Father">import Game from './Game.vue'
</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>
Category.vue
<template><div class="game"><h2>游戏列表</h2><!-- 给插槽提供数据 --><slot :youxi="games" x="哈哈" y="你好"></slot></div>
</template><script setup lang="ts" name="Game">import {reactive} from 'vue'let games = reactive([{id:'asgytdfats01',name:'英雄联盟'},{id:'asgytdfats02',name:'王者农药'},{id:'asgytdfats03',name:'红色警戒'},{id:'asgytdfats04',name:'斗罗大陆'}])</script><style scoped>.game {width: 200px;height: 300px;background-color: skyblue;border-radius: 10px;box-shadow: 0 0 10px;}h2 {background-color: orange;text-align: center;font-size: 20px;font-weight: 800;}
</style>

7. 其它 API

7.1 shallowRef 与 shallowReactive

shallowRef

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

  2. 用法:

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

shallowReactive

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

  2. 用法:

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

总结

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

示例

<template><div class="app"><h2>求和为:{{ sum }}</h2><h2>名字为:{{ person.name }}</h2><h2>年龄为:{{ person.age }}</h2><h2>汽车为:{{ car }}</h2><button @click="changeSum">sum+1</button><button @click="changeName">修改名字</button><button @click="changeAge">修改年龄</button><button @click="changePerson">修改整个人</button><span>|</span><button @click="changeBrand">修改品牌</button><button @click="changeColor">修改颜色</button><button @click="changeEngine">修改发动机</button></div>
</template><script setup lang="ts" name="App">import { ref, reactive, shallowRef, shallowReactive } from 'vue'let sum = shallowRef(0)let person = shallowRef({name: '张三',age: 18})/* 如果使用ref来定义sum和person, 那么下面的方法被调用时, 数据都会发生改变, 并且都会有响应式;但因为使用shallowRef定义, 因此只有第1层修改才会数据发生改变, 具有响应式, (第1层指的是xxx.value, 不能再点下去了, 否则就不是第1层了)*/function changeSum() {sum.value += 1                       // 数据发生改变, 有响应式}function changeName() {person.value.name = '李四'           // 数据未发生改变}function changeAge() {person.value.age += 1                // 数据未发生改变}function changePerson() {person.value = { name: 'tony', age: 100 } // 数据发生改变, 有响应式}/* ****************** *//* 如果使用reactive来定义car, 那么下面的方法被调用时, 数据都会发生改变, 并且都会有响应式;但因为使用shallowReactive定义, 因此只有第1层修改才会数据发生改变, 具有响应式, (第1层指的是brand和options, 不能再点下去了, 否则就不是第1层了)*/let car = shallowReactive({brand: '奔驰',options: {color: '红色',engine: 'V8'}})function changeBrand() {car.brand = '宝马'}function changeColor() {car.options.color = '紫色'}function changeEngine() {car.options.engine = 'V12'}</script><style scoped>.app {background-color: #ddd;border-radius: 10px;box-shadow: 0 0 10px;padding: 10px;}button {margin: 0 5px;}
</style>

7.2 readonly 与 shallowReadonly

readonly

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

  2. 用法:

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

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

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

shallowReadonly

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

  2. 用法:

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

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

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

示例

<template><div class="app"><h2>当前sum1为:{{ sum1 }}</h2><h2>当前sum2为:{{ sum2 }}</h2><button @click="changeSum1">点我sum1+1</button><button @click="changeSum2">点我sum2+1</button><!-- ******************* --><h2>当前car1为:{{ car1 }}</h2><h2>当前car2为:{{ car2 }}</h2><button @click="changeBrand2">修改品牌(car2)</button><button @click="changeColor2">修改颜色(car2)</button><button @click="changePrice2">修改价格(car2)</button></div>
</template><script setup lang="ts" name="App">import { ref, reactive, readonly, shallowReadonly } from "vue";let sum1 = ref(0)// 这里要传入1个响应式对象, 注意不要.value// 当sum1数据发生变化的时候, sum2也会发生变化, 但不能直接改sum2, 因为sum2只读,// (这样就可以达到一种保护数据的目的)let sum2 = readonly(sum1)function changeSum1() {sum1.value += 1}function changeSum2() {sum2.value += 1 // sum2是不能修改的}/******************/let car1 = reactive({brand: '奔驰',options: {color: '红色',price: 100}})// 这里要传入1个响应式对象// 当car1数据发生变化的时候, car2也会发生变化, // 但不能直接改car2的第一层属性, 因为这里使用的是shallowReadOnly, 意味着car2的第一层属性都只读,// 这里也可以使用readOnly, 这就意味着car2的任何属性都不能改了// (这样就可以达到一种保护数据的目的)let car2 = shallowReadonly(car1)function changeBrand2() {car2.brand = '宝马'}function changeColor2() {// 由于car2是对car1使用了shallowReadOnly, 因此这里是允许改的car2.options.color = '绿色'}function changePrice2() {car2.options.price += 10}
</script><style scoped>.app {background-color: #ddd;border-radius: 10px;box-shadow: 0 0 10px;padding: 10px;}button {margin: 0 5px;}
</style>

7.3 toRaw 与 markRaw

toRaw

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

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

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

markRaw

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

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

示例

<template><div class="app"><h2>姓名:{{ person.name }}</h2><h2>年龄:{{ person.age }}</h2><button @click="person.age += 1">修改年龄</button>{{ rawPerson }}<!-- 这里修改rawPerson不会影响到person的数据的变化, 并且由于rawPerson不是响应式数据, 因此上面的{{ rawPerson }}也不会变化 --><button @click="rawPerson.age += 1">修改年龄rawPerson</button><hr><h2>{{ car2 }}</h2><button @click="car2.price += 10">点我价格+10</button></div>
</template><script setup lang="ts" name="App">import { reactive,toRaw,markRaw } from "vue";import mockjs from 'mockjs'/* toRaw */let person = reactive({name:'tony',age:18})// 用于获取一个响应式对象的原始对象let rawPerson = toRaw(person)console.log('响应式对象',person)  // Proxy(Object) {name: 'tony', age: 18}console.log('原始对象',rawPerson) // {name: 'tony', age: 18}console.log('------------------------');/* markRaw */// 如果这里没加markRaw, 那么这里的这个car就可以作为响应式对象的源头// 加上了markRaw之后, 就意味着car永远不能作为响应式对象的源头, 只能是1个原始的对象, 不能做成1个响应式对象let car = markRaw({brand:'奔驰',price:100})let car2 = reactive(car) // 这里的car2不是响应式的了// 从输出看, 其实就是加了个标记__v_skip: true, 当遇到这个标记时, 就不对这个对象做响应式处理console.log(car)  // {brand: '奔驰', price: 100, __v_skip: true}console.log(car2) // {brand: '奔驰', price: 100, __v_skip: true}// 例如使用mockjs时,为了防止误把mockjs变为响应式对象,可以使用 markRaw 去标记mockjslet mockJs = markRaw(mockjs)</script><style scoped>.app {background-color: #ddd;border-radius: 10px;box-shadow: 0 0 10px;padding: 10px;}button {margin:0 5px;}
</style>

7.4 customRef

作用:创建一个自定义的ref,并对其依赖项跟踪和更新触发进行逻辑控制。

示例

App.vue
<template><div class="app"><h2>{{ msg }}</h2><input type="text" v-model="msg"></div>
</template><script setup lang="ts" name="App">import {ref} from 'vue'import useMsgRef from './useMsgRef'// 使用Vue提供的默认ref定义响应式数据,数据一变,页面就更新//                               (这是vue给我们提供的功能, 也是承诺)// let msg = ref('你好')// 使用useMsgRef来定义一个响应式数据且有延迟效果let {msg} = useMsgRef('你好',1000)</script><style scoped>.app {background-color: #ddd;border-radius: 10px;box-shadow: 0 0 10px;padding: 10px;}button {margin:0 5px;}
</style>
useMsgRef.ts
import { customRef } from "vue";export default function (initValue: string, delay: number) {// 使用Vue提供的customRef定义响应式数据let timer: number// track(跟踪)、trigger(触发)let msg = customRef((track, trigger) => {return {// get何时调用?—— msg被读取时get() {track() // 告诉Vue数据msg很重要,你要对msg进行持续关注,一旦msg变化就去更新console.log('get');return initValue},// set何时调用?—— msg被修改时set(value) {console.log('set');clearTimeout(timer)timer = setTimeout(() => {initValue = valuetrigger() // 通知Vue一下数据msg变化了}, delay);}}})return { msg }
}

8. Vue3新组件

8.1 Teleport传送门

什么是Teleport?—— Teleport 是一种能够将我们的组件html结构移动到指定位置的技术。

示例

这个示例有个奇怪的地方(css还有这种操作的),给outer加上filter之后,fixed定位就变成相对于父元素定位了,而不是body定位,这时,使用teleport可以解决这个问题,因为它把dom都传送走了,当然,teleport不仅可以适用于这种情况,也可用于其它场景。

App.vue
<template><div class="outer"><h2>我是App组件</h2><img src="http://www.atguigu.com/images/index_new/logo.png" alt=""><br><!-- 遮罩 --><Modal/></div>
</template><script setup lang="ts" name="App">import Modal from "./Modal.vue";
</script><style>.outer{background-color: #ddd;border-radius: 10px;padding: 5px;box-shadow: 0 0 10px;width: 400px;height: 400px;filter: saturate(200%);}img {width: 270px;}
</style>
Modal.vue
<template><button @click="isShow = true">展示弹窗</button><!-- 数据用的还是当前组件的, 但渲染的地方被传送到了body那里;to这里写的是选择器哦;--><teleport to='body'><div class="modal" v-show="isShow"><h2>我是弹窗的标题</h2><p>我是弹窗的内容</p><button @click="isShow = false">关闭弹窗</button></div></teleport></template><script setup lang="ts" name="Modal">import {ref} from 'vue'let isShow = ref(false)</script><style scoped>.modal {width: 200px;height: 150px;background-color: skyblue;border-radius: 10px;padding: 5px;box-shadow: 0 0 5px;text-align: center;position: fixed;left: 50%;top: 20px;margin-left: -100px;}
</style>

8.2 Suspense

  • 等待异步组件时渲染一些额外内容,让应用有更好的用户体验
  • 使用步骤:
    • 异步引入组件
    • 使用Suspense包裹组件,并配置好defaultfallback

示例

App.vue
<template><div class="app"><h2>我是App组件</h2><Child/><Suspense><template v-slot:default><Child/></template><!-- 当组件未加载完成时, 显示的临时内容 --><template v-slot:fallback><h2>加载中......</h2></template></Suspense></div>
</template><script setup lang="ts" name="App">import {Suspense} from 'vue'import Child from './Child.vue'
</script><style>.app {background-color: #ddd;border-radius: 10px;padding: 10px;box-shadow: 0 0 10px;}
</style>
Child.vue
<template><div class="child"><h2>我是Child组件</h2><h3>当前求和为:{{ sum }}</h3></div>
</template><script setup lang="ts">import {ref} from 'vue'import axios from 'axios'let sum = ref(0);// 当下面多了这行请求数据的异步代码时, Child组件将不会展示出来(setup顶层最外面有async),// 需要父组件在使用时, 借助Suspense组件才能展示Child组件let {data:{content}} = await axios.get('https://api.uomg.com/api/rand.qinghua?format=json')console.log('content',content)/*   // 使用这种方式, 可以不借助Suspense组件也能展示Child组件let content = (async function() {let {data:{content}} = await axios.get('https://api.uomg.com/api/rand.qinghua?format=json')return content})(); */</script><style scoped>.child {background-color: skyblue;border-radius: 10px;padding: 10px;box-shadow: 0 0 10px;}
</style>

8.3 全局API转移到应用对象

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

示例

import {createApp} from 'vue'
import App from './App.vue'
import Hello from './Hello.vue'// 创建应用
const app = createApp(App)// 全局注册组件, 然后所有的地方都可以使用Hello这个组件了
app.component('Hello',Hello)// 全局挂载
// 类似于vue2的Vue.prototype.x=99, 然后所有的组件中都可以使用x了
app.config.globalProperties.x = 99// 解决全局挂载x的时候, ts报错的问题
declare module 'vue' {interface ComponentCustomProperties {x:number}
}// 全局注册指令, 然后所有的组件中都可以使用v-beauty了, 如: <h1 v-beauty="sum">好开心</h1>
app.directive('beauty',(element,{value})=>{element.innerText += valueelement.style.color = 'green'element.style.backgroundColor = 'yellow'
})// 挂载应用
app.mount('#app')// 卸载应用
setTimeout(() => {app.unmount()
}, 2000);

8.4 其他

  • 过渡类名 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/6232.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数字电路-5路呼叫显示电路和8路抢答器电路

本内容涉及两个电路&#xff0c;分别为5路呼叫显示电路和8路抢答器电路&#xff0c;包含Multisim仿真原文件&#xff0c;为掌握FPGA做个铺垫。紫色文字是超链接&#xff0c;点击自动跳转至相关博文。持续更新&#xff0c;原创不易&#xff01; 目录&#xff1a; 一、5路呼叫显…

Android BINDER是干嘛的?

1.系统架构 2.binder 源码位置&#xff1a; 与LINUX传统IPC对比

题目:吃奶酪

问题描述&#xff1a; 解题思路&#xff1a; 枚举每种吃奶酪顺序&#xff0c;并计算其距离&#xff0c;选择最小的距离即答案。v数组&#xff1a;记录顺序。 注意点&#xff1a;1. 每次用于min的s需要重置为0。 2. 实数包括小数&#xff0c;所以结构体内x,y为double类型。 3. 第…

Python | Leetcode Python题解之第65题有效数字

题目&#xff1a; 题解&#xff1a; from enum import Enumclass Solution:def isNumber(self, s: str) -> bool:State Enum("State", ["STATE_INITIAL","STATE_INT_SIGN","STATE_INTEGER","STATE_POINT","STATE_…

Vue基础:为什么要学Vue3,Vue3相较于Vue2有那些优势?

为什么要学Vue3&#xff1f; 1.框架层面 1.响应式底层API的变化 Proxy 数组下标的修改 对象动态添加属性 解释说明&#xff1a;1.vue2采用的是Object.definePrototype&#xff0c;它每次只能对单个对象中的单个数据进行劫持&#xff0c;所以在Vue2中data()中的数据一多就要进行…

基础IO认识

回顾文件 我们之前认识文件只是在语言程度上理解&#xff0c;但是我们理解的不够彻底&#xff0c;要想真正理解文件要在os上理解。 简单代码认识 1 #include<stdio.h>2 int main(){3 FILE* fpfopen("log.txt","w");4 if(fpNULL){5 p…

leetcode_43.字符串相乘

43. 字符串相乘 题目描述&#xff1a;给定两个以字符串形式表示的非负整数 num1 和 num2&#xff0c;返回 num1 和 num2 的乘积&#xff0c;它们的乘积也表示为字符串形式。 注意&#xff1a;不能使用任何内置的 BigInteger 库或直接将输入转换为整数。 示例 1: 输入: num1 &q…

蓝桥杯练习系统(算法训练)ALGO-951 预备爷的悲剧

资源限制 内存限制&#xff1a;512.0MB C/C时间限制&#xff1a;1.0s Java时间限制&#xff1a;3.0s Python时间限制&#xff1a;5.0s 问题描述 英语预备爷gzp是个逗(tu)比(hao)&#xff0c;为了在即将到来的英语的quiz中不挂科&#xff0c;gzp废寝忘食复习英语附录单词…

SpringBoot 基础简介

目录 1. SpringBoot 概述 1.1. 为什么会有springboot 1.1.1. 传统Spring 的两个缺点 1.1.2. Springboot 功能 2. SpringBoot 快速搭建 2.1. 创建Maven项目​编辑​编辑​编辑 2.2. 导入SpringBoot起步依赖 2.3. 定义controller 2.4. 添加引导类 2.5. 启动访问 3. Sprin…

使用node调用chrome(基于selenium-webdriver包)

下载测试版chrome和chromedriver https://googlechromelabs.github.io/chrome-for-testing/ 把chromedriver复制到chrome的文件里 设置环境变量 编写代码 const { Builder, Browser, By, Key, until } require(selenium-webdriver) const puppeteer require(puppeteer)//查…

2024/5/2 英语每日一段

Enablers’ fate is inherently linked to adopters, which are their ultimate customers, after all. That dynamic won’t flip overnight, but adopters able to demonstrate progress on AI initiatives will increasingly get credit from investors in the form of high…

基于SSM的“一汽租车辆共享平台”的设计与实现(源码+数据库+文档+PPT)

基于SSM的“一汽租车辆共享平台”的设计与实现&#xff08;源码数据库文档PPT) 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;SSM 工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 系统展示 登录界面 租车界面 订单管理界面 财务报表界面 理赔界面 …

I/O体系结构和设备驱动程序

I/O体系结构 为了确保计算机能够正常工作&#xff0c;必须提供数据通路&#xff0c;让信息在连接到个人计算机的CPU、RAM和I/O设备之间流动。这些数据通路总称为总线&#xff0c;担当计算机内部主通信通道的作用。 所有计算机都拥有一条系统总线&#xff0c;它连接大部分内部…

Java | Leetcode Java题解之第63题不同路径II

题目&#xff1a; 题解&#xff1a; class Solution {public int uniquePathsWithObstacles(int[][] obstacleGrid) {int n obstacleGrid.length, m obstacleGrid[0].length;int[] f new int[m];f[0] obstacleGrid[0][0] 0 ? 1 : 0;for (int i 0; i < n; i) {for (i…

二叉树的递归详解:以例题计算二叉树第k层为例

1.代码速览 1.1节点的构建 #include<iostream> using namespace std; class ListNode { public:friend void fun();friend int TreeKLevel(ListNode* root, int k);ListNode(int val):_val(val),leftnext(nullptr),rightnext(nullptr){} private:int _val 0;class Lis…

【doghead】ubuntu构建libuv

按照官方的文档2024年3月的版本。首先构建libuv 最终构建的还得了test 构建过程 zhangbin@DESKTOP-1723CM1:/mnt/d/XTRANS/thunderbolt/ayame/zhb-bifrost$ ls Bifrost-202403 README.md draw player-only worker 大神的带宽估计.png zhangbin@DESKTOP-1723CM1:/mnt/d/XTRANS/…

240503-关于VisualStudio2022社区版的二三事

240503-关于VisualStudio2022社区版的二三事 1 常用快捷键 快捷键描述AltEnter选中代码片段以提取方法Alt上下箭头移动选中的代码片段F12转到方法定义CtrlR*2批量修改选中的变量名称 2 自动生成构造函数 3 快速重写父类方法 4 节约时间&#xff1a;写代码使用“头插法”&…

华为OD机试 - 会议室占用时间段(Java 2024 C卷 100分)

华为OD机试 2024C卷题库疯狂收录中&#xff0c;刷题点这里 专栏导读 本专栏收录于《华为OD机试&#xff08;JAVA&#xff09;真题&#xff08;A卷B卷C卷&#xff09;》。 刷的越多&#xff0c;抽中的概率越大&#xff0c;每一题都有详细的答题思路、详细的代码注释、样例测试…

【iOS】pthread、NSThread

文章目录 前言一、pthread 使用方法pthread 其他相关方法 二、 NSThread创建、启动线程线程相关用法线程状态控制方法NSThread 线程安全和线程同步场景 线程的状态转换 前言 五一这两天准备将GCD相关的知识完&#xff0c;同时NSOperation与NSThread、pthread也是相关知识&…

前端Web开发基础知识

HTML定义 超文本标记语言&#xff08;英语&#xff1a;HyperText Markup Language&#xff0c;简称&#xff1a;HTML&#xff09;是一种用于创建网页的标准标记语言。 什么是 HTML? HTML 是用来描述网页的一种语言。 HTML 指的是超文本标记语言: HyperText Markup LanguageH…