文章目录
- 学习链接
- 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、 refs、parent、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. 新的特性
-
Composition API
(组合API
):-
setup
-
ref
与reactive
-
computed
与watch
…
-
-
新的内置组件:
-
Fragment
-
Teleport
-
Suspense
…
-
-
其他改变:
-
新的生命周期钩子
-
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
),能实现极速的服务启动。 - 对
TypeScript
、JSX
、CSS
等支持开箱即用(不用配置,直接就可以用)。 - 真正的按需编译,不再等待整个应用编译完成。
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
Vue2
的API
设计是Options
(配置)风格的。Vue3
的API
设计是Composition
(组合)风格的。
Options API 的弊端
Options
类型的 API
,数据、方法、计算属性等,是分散在:data
、methods
、computed
中的,若想新增或者修改一个需求,就需要分别修改:data
、methods
、computed
,不便于维护和复用。
Composition API 的优势
可以用函数的方式,更加优雅的组织代码,让相关功能的代码更加有序的组织在一起。
3.2. 拉开序幕的 setup
setup 概述
介绍
setup
是Vue3
中一个新的配置项,值是一个函数。- 它是
Composition API
“表演的舞台”,组件中所用到的:数据、方法、计算属性、监视…等等,均配置在setup
中。
特点如下:
setup
函数返回的对象中的内容,可直接在模板中使用。setup
中访问this
是undefined
。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 = 18
let 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
的配置(data
、methos
…)中可以访问到setup
中的属性、方法。- 但在
setup
中不能访问到Vue2
的配置(data
、methos
…)。 - 如果与
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 = 18
let 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 = 18
let tel = '13888888888'
// 方法
function changName(){
name = '李四'//注意:此时这么修改name页面是不变化的
}
function changAge(){
console.log(age)
age += 1 //注意:此时这么修改age页面是不变化的
}
function showTel(){
alert(tel)
}
</script>
扩展:上述代码,还需要编写一个不写
setup
的script
标签,去指定组件名字,比较麻烦,我们可以借助vite
中的插件简化
- 第一步:
npm i vite-plugin-vue-setup-extend -D
- 第二步:
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))
}
}
})
- 第三步:
<script setup lang="ts" name="Person">
3.3. ref 创建:基本类型的响应式数据
- **作用:**定义响应式变量。
- 语法:
let xxx = ref(初始值)
。 - **返回值:**一个
RefImpl
的实例对象,简称ref对象
或ref
,ref
对象的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对象时候需要.value
name.value = '李四' // 页面得到刷新
console.log(name.value)
// 注意:name不是响应式的,name.value是响应式的,所以如下代码并不会引起页面的更新。
// name = ref('zhang-san')
}
function changeAge(){
// JS中操作ref对象时候需要.value
age.value += 1 // 页面得到刷新
console.log(age.value)
}
function showTel(){
// 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 += 10
console.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中勾选 ,它会在使用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
。 -
若需要一个响应式对象,层级不深,
ref
、reactive
都可以。 -
若需要一个响应式对象,且层级较深,推荐使用
reactive
。
3.7 toRefs 与 toRef
- 作用:将一个响应式对象中的每一个属性,转换为
ref
对象。 - 备注:
toRefs
与toRef
功能一致,但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 } = person
console.log(name, age); // 张三 18
// 方法
function changeName() {
name += '~'
console.log(name, person.name); // 变化的是name, 而person.name仍然未修改
}
function changeAge() {
age += 1
console.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 += 1
console.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
定义的对象中的属性,newValue
和oldValue
都是新值,因为它们是同一个对象。 -
若修改整个
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>
* 情况四
监视ref
或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: '爱玛' }
}
</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
都能监听响应式数据的变化,不同的是监听数据变化的方式不同
watch
:要明确指出监视的数据
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、height
watch([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
的生命周期创建阶段:
beforeCreate
、created
挂载阶段:
beforeMount
、mounted
更新阶段:
beforeUpdate
、updated
销毁阶段:
beforeDestroy
、destroyed
-
Vue3
的生命周期创建阶段:
setup
(替代了之前vue2中的beforeCreate、created)挂载阶段:
onBeforeMount
、onMounted
更新阶段:
onBeforeUpdate
、onUpdated
卸载阶段:
onBeforeUnmount
、onUnmounted
(就对应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、路由组件通常存放在pages
或 views
文件夹,一般组件通常存放在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. 路由器工作模式
-
history
模式优点:
URL
更加美观,不带有#
,更接近传统的网站URL
。缺点:后期项目上线,需要服务端配合处理路径问题,否则刷新会有
404
错误。const router = createRouter({ history:createWebHistory(), //history模式 /******/ })
-
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参数
- 定义路由规则,并定义路由路径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
}
]
})
- 传递参数
<!-- 跳转并携带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>
- 接收参数:
// 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属性
-
作用:控制路由跳转时操作浏览器历史记录的模式。
-
浏览器的历史记录有两种写入方式:分别为
push
和replace
:push
是追加历史记录(默认值)。replace
是替换当前记录。
-
开启
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 重定向
-
作用:将特定的路径,重新定向到已有路由。
-
具体编码:
{ 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 准备一个效果
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 存储+读取数据
-
Store
是一个保存:状态、业务逻辑 的实体,每个组件都可以读取、写入它。 -
它有三个概念:
state
、getter
、action
,相当于组件中的:data
、computed
和methods
。
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";
// 引入useCountStore
import { useCountStore } from '@/store/count'
// 使用useCountStore,得到一个专门保存count相关的store
const countStore = useCountStore()
// 数据
let n = ref(1) // 用户选择的数字
// 方法
function add() {
// 第一种修改方式, 直接拿到countStore去改, 注意: 这和vuex不同, vuex是不能直接修改的
countStore.sum += 1
countStore.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";
// 引入useCountStore
import {useCountStore} from '@/store/count'
// 使用useCountStore,得到一个专门保存count相关的store
const 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";
// 引入useCountStore
import {useCountStore} from '@/store/count'
// 使用useCountStore,得到一个专门保存count相关的store
const 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用法
- 借助
storeToRefs
将store
中的数据转为ref
对象,方便在模板中使用。 - 注意:
pinia
提供的storeToRefs
只会将数据做转换,而Vue
的toRefs
会转换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'
// 引入useCountStore
import {useCountStore} from '@/store/count'
// 使用useCountStore,得到一个专门保存count相关的store
const 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'
// 引入useCountStore
import {useCountStore} from '@/store/count'
// 使用useCountStore,得到一个专门保存count相关的store
const 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) => {
// 注意: 箭头函数中没有this
console.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就是state
const talkList = reactive(
JSON.parse(localStorage.getItem('talkList') as string) || []
)
// getATalk函数相当于action
async function getATalk(){
// 发请求,下面这行的写法是:连续解构赋值+重命名
let {data:{content:title}} = await
axios.get('https://api.uomg.com/api/rand.qinghua?format=json')
// 把请求回来的字符串,包装成一个对象
let obj = {id:nanoid(),title}
// 放到数组中
talkList.unshift(obj)
}
return {talkList,getATalk}
})
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('奥特曼')
// 声明接收props
defineProps(['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
)功能类似,可以实现任意组件间通信。
安装mitt
,npm 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
-
概述:
$attrs
用于实现**当前组件的父组件,向当前组件的子组件**通信(祖→孙)。 -
具体说明:
$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、 refs、parent、proxy
-
概述:
$refs
用于 :父→子。$parent
用于:子→父。
-
原理如下:
属性 说明 $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语法糖中不能直接访问到$refs
console.log(this.proxy); // 这个等价于getCurrentInstance()返回的值
console.log(this.proxy == proxy); // true
console.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); // undefined
console.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
-
概述:实现祖孙组件直接通信
-
具体使用:
- 在祖先组件中通过
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
-
作用:创建一个响应式数据,但只对顶层属性进行响应式处理。
-
用法:
let myVar = shallowRef(initialValue);
-
特点:只跟踪引用值的变化,不关心值内部的属性变化。
shallowReactive
-
作用:创建一个浅层响应式对象,只会使对象的最顶层属性变成响应式的,对象内部的嵌套属性则不会变成响应式的
-
用法:
const myObj = shallowReactive({ ... });
-
特点:对象的顶层属性是响应式的,但嵌套对象的属性不是。
总结
通过使用 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
-
作用:用于创建一个对象的深只读副本。
-
用法:
const original = reactive({ ... }); const readOnlyCopy = readonly(original);
-
特点:
- 对象的所有嵌套属性都将变为只读。
- 任何尝试修改这个对象的操作都会被阻止(在开发模式下,还会在控制台中发出警告)。
-
应用场景:
- 创建不可变的状态快照。
- 保护全局状态或配置不被修改。
shallowReadonly
-
作用:与
readonly
类似,但只作用于对象的顶层属性。 -
用法:
const original = reactive({ ... }); const shallowReadOnlyCopy = shallowReadonly(original);
-
特点:
-
只将对象的顶层属性设置为只读,对象内部的嵌套属性仍然是可变的。
-
适用于只需保护对象顶层属性的场景。
-
示例
<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
-
作用:用于获取一个响应式对象的原始对象,
toRaw
返回的对象不再是响应式的,不会触发视图更新。 -
官网描述:这是一个可以用于临时读取而不引起代理访问/跟踪开销,或是写入而不触发更改的特殊方法。不建议保存对原始对象的持久引用,请谨慎使用。
-
何时使用? 在需要将响应式对象传递给非
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 去标记mockjs
let 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 = value
trigger() // 通知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
包裹组件,并配置好default
与fallback
示例
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 += value
element.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-if
和v-for
在同一个元素身上使用时的优先级发生了变化。 -
移除了
$on
、$off
和$once
实例方法。 -
移除了过滤器
filter
。 -
移除了
$children
实例propert
。