前言:本篇笔记内容来自于 B 站:《尚硅谷Vue3入门到实战,最新版vue3+TypeScript前端开发教程》系列视频
链接地址: https://www.bilibili.com/video/BV1Za4y1r7KE/
创建 Vue 工程
NodeJS
安装 18.3 + 的 NodeJS https://nodejs.org/
初始化项目
通过 Vite 初始化一个 Vue 项目 Vite 会在运行目录创建一个 项目同名的目录
PS E:\code> npm create vue@latest
> npx
> create-vue
Vue.js - The Progressive JavaScript Framework
√ 请输入项目名称: ... test_vue3
√ 是否使用 TypeScript 语法? ... 否 / 是
√ 是否启用 JSX 支持? ... 否 / 是
√ 是否引入 Vue Router 进行单页面应用开发? ... 否 / 是
√ 是否引入 Pinia 用于状态管理? ... 否 / 是
√ 是否引入 Vitest 用于单元测试? ... 否 / 是
√ 是否要引入一款端到端(End to End)测试工具? » 不需要
√ 是否引入 ESLint 用于代码质量检测? ... 否 / 是
? 是否引入 Vue DevTools 7 扩展用于调试? (试验阶段) » 否 / 是
之后通过 vscode 打开项目目录.
运行项目
运行前端项目要先看一下项目根目录的 package.json
文件,里面包含了一些项目的信息:
{
"name": "test_vue3",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build --force"
},
"dependencies": {
"vue": "^3.4.29"
},
"devDependencies": {
"@tsconfig/node20": "^20.1.4",
"@types/node": "^20.14.5",
"@vitejs/plugin-vue": "^5.0.5",
"@vue/tsconfig": "^0.5.1",
"npm-run-all2": "^6.2.0",
"typescript": "~5.4.0",
"vite": "^5.3.1",
"vue-tsc": "^2.0.21"
}
}
可以看到 scripts
目录下可以 通过的 dev
命令来启动项目,通过 build
来打包编译。
我们使用 npm run dev
启动项目
项目结构
项目入口是根目录中的 index.html 文件,里面有两个核心操作,声明了 #app
容器,所有前端的内容渲染(挂载)到这个容器上,然后引入了 main.ts
文件
main.ts 文件中引入了 Vue 组件,并创建了一个 App 的组件,挂载了节点为 App 的容器上。
以根目录为例,代码如下:
/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>
/src/main.ts
文件
// 引入 createApp 用于创建应用
import {createApp} from 'vue'
// 引入 App 根组件
import App from './App.vue'
// 挂载 组件
createApp(App).mount('#app')
/src/App.vue
文件
<template>
<div class="app">
<h1>你好啊!</h1>
</div>
</template>
<script lang="ts">
export default {
name:'App' //组件名
}
</script>
<style>
.app {
background-color: #ddd;
box-shadow: 0 0 10px;
border-radius: 10px;
padding: 20px;
}
</style>
总结
Vite
项目中,index.html
是项目的入口文件,在项目最外层。- 加载
index.html
后,Vite
解析<script type="module" src="xxx">
指向的JavaScript
。 Vue3
中是通过createApp
函数创建一个应用实例。
Demo
src\components\Person.vue
声明一个组件
<template>
<div class="person">
<h2>姓名:{{ name }}</h2>
<h2>年龄:{{ age }}</h2>
<button @click="showTel">show</button>
</div>
</template>
<script lang="ts">
export default {
name:"Person",
data(){
return {
name:'张三',
age:18,
tel:153
}
},
methods: {
showTel(){
alert(this.tel)
}
}
}
</script>
<style>
.person{
background-color:#007acc;
padding: 20px;
}
</style>
src\App.vue
根文件
<template>
<!-- html -->
<div class="app">
<h1>123</h1>
<Person />
</div>
</template>
<script lang="ts">
// JS 或 TS
import Person from "./components/Person.vue"
export default {
name :'App',
components: {Person}
}
</script>
<style>
/* 样式 */
.app {
background: #ddd;
padding: 20px;
}
</style>
注:Vue3向下兼容Vue2语法,且Vue3中的模板中可以没有根标签。
Vue3 核心语法
OptionsAPI 和 CompositionAPI
Vue2
的API
设计是Options
(配置、选项)风格的Vue3
的API
设计是Composition
(组合、组件) 风格的
Options API 弊端
Options
类型的 API
,数据、方法、计算属性等,是分散在:data
、methods
、computed
中的,若想新增或者修改一个需求,就需要分别修改:data
、methods
、computed
,不便于维护和复用。
setup
setup 概述
setup
是 vue3 中一个新的配置项,它的值是一个函数。 setup
是 Composition API
的核心,组件中所用到的:数据、方法、计算属性、监视......等等,均配置在setup中。
特点如下:
setup
函数返回的对象中的内容,可直接在模板中使用。setup
中访问this
是undefined
。setup
函数会在beforeCreate
之前调用,它是“领先”所有钩子执行的。
<template>
<div class="person">
<h2>姓名:{{ name }}</h2>
<h2>年龄:{{ age }}</h2>
<button @click="showTel">show</button>
</div>
</template>
<script lang="ts">
export default {
name:"Person",
setup(){
// 数据 vue2 中需要写在 data 中 (注意 这时的 name age tel 不是响应式数据)
let name = 'dbkuaizi'
let age = 18
let tel = '1533'
// 方法 vue2 中需要写在 methods 中
function showTel() {
alert(tel)
}
// 返回一个对象 模板可以直接用的
return {name,age,showTel}
}
}
</script>
setup 返回值
- 若 返回一个对象:则对象中的:属性、方法等,在模板中均可以正常使用
- 若返回一个函数:则会直接渲染返回的内容,所以可以用作自定义渲染函数。代码如下:
setup(){
return ()=> 'hello Vue3!'
}
setup 与 Options API 的关系
Vue2
的配置(data
、methos
......)中可以访问到setup
中的属性、方法。- 但在
setup
中不能访问到Vue2
的配置(data
、methos
......)。 - 如果与
Vue2
冲突,则setup
优先。
setup 语法糖
setup
提供了一个语法糖,允许我们把 setup 独立出去,且不用 return,代码如下:
<template>
<div class="person">
<h2>姓名:{{ name }}</h2>
<h2>年龄:{{ age }}</h2>
<button @click="showTel">show</button>
</div>
</template>
<script lang="ts">
export default {
name:"Person"
}
</script>
<script lang="ts" setup>
// 数据
let name = 'dbkuaizi'
let age = 18
let tel = '1533'
// 方法
function showTel() {
alert(tel)
}
</script>
::: alert-info
当不定义组件名时(即 上方代码,省略 9 ~ 13 行 ),会自动通过文件名来推导组件名,例如文件名为 Person.vue
组件名就是 Person
。
当组件过于复杂时,可能会用一个单独的文件夹来编排代码,例如 /src/compositions/tree/index.vue
, 这种场景下 整个 tree
目录都是组件,如果通过文件名推导,这个组件名称就是 index
,肯定不合适,所以需要显式定义组件名。
:::
若想简写 组件名称可以通过 vite 插件来实现:
- 安装插件
npm i vite-plugin-vue-setup-extend -D
- 编辑
/vite.config.ts
文件
import { defineConfig } from 'vite'
import VueSetupExtend from 'vite-plugin-vue-setup-extend' // 增加
export default defineConfig({
plugins: [ VueSetupExtend() ] // 增加
})
- 的
<script setup lang="ts" name="Person">
补充一种 Vue 3.3 + 支持 不依赖插件的写法
<template>
<div class="person">
<h2>姓名:{{ name }}</h2>
<h2>年龄:{{ age }}</h2>
<button @click="showTel">show</button>
</div>
</template>
<script lang="ts" setup>
defineOptions({name:"Person666"})
// 数据
let name = 'dbkuaizi'
let age = 18
let tel = '1533'
// 方法
function showTel() {
alert(tel)
}
</script>
ref 响应式数据
ref 创建:基本类型响应式数据
- 作用:定义响应式数据
- 语法:
let xxx =ref(初始值)
- 返回值:一个
Reflmpt
的实例对象,简称ref 对象
或ref
,ref 对象
的value
属性是响应式的
:::alert-info
- JS 中操作数据 需要
xxx.vlue
不能直接覆盖ref 对象
, 但 模板中不需要.value
可以直接使用。 - 对于
let name = ref('dbkuaizi')
来说name
不是响应式的,name.value
才是。
:::
<template>
<div class="person">
<h2>姓名:{{ name }}</h2>
<h2>年龄:{{ age }}</h2>
<button @click="setAge">set age</button>
<button @click="showTel">show</button>
</div>
</template>
<script lang="ts" setup>
defineOptions({name:"Person"})
import {ref} from 'vue'
// name和age是一个RefImpl的实例对象,简称ref对象,它们的value属性是响应式的
let name = ref('dbkuaizi')
let age = ref(18)
// tel 不是响应式的
let tel = '1533'
// 方法
function showTel() {
alert(tel)
}
// 响应式数据在修改时必须 .value 但在模板中使用时不需要
function setAge(){
age.value++
}
</script>
reactive 创建,对象类型的响应式数据
- 作用:定义一个响应式的对象,基本类型用
ref
否则会报错 - 语法:
let 响应式对象 = reactive(源对象)
- 返回值:一个
Proxy
的实例对象,简称响应式对象。
::: alert-info
reactive
定义的响应式数据是 "深层次"的。reactive
只能定义响应式类型数据,定义基本类型会报错ref
用来定义:基本类型数据、对象类型数据;
:::
<template>
<div class="person">
<h2>一辆 {{car.name}},价值 {{ car.price }} </h2>
<button @click="setPrice">setPrice</button>
<hr>
<h2>游戏:</h2>
<ul>
<li v-for="g in games" :key="g.id">{{ g.name }}</li>
</ul>
<button @click="setGames">setGames</button>
</div>
</template>
<script lang="ts" setup>
defineOptions({name:"Person"})
import {reactive,ref} from 'vue'
let car = reactive({name:'喜德盛',price:1500})
let games = reactive([
{id:"yx1",name:"仙剑"},
{id:"yx2",name:"古剑"},
{id:"yx2",name:"轩辕剑"},
])
function setPrice(){
car.price += 10
}
function setGames() {
games[0].name = '流星蝴蝶剑'
}
</script>
ref 对比 reactive
宏观角度
ref
用来定义:基本类型数据、对象类型数据reactive
用来定义:对象类型数据
区别
ref
创建的变量必须使用.value
操作,(可以通过Vue - Official
插件设置自动添加.value
)
- Vscode 中找到
Vue - Official
插件设置 - 勾选上
Auto Insert Dot Value
reactive
重新分配一个对象,会失去 响应式(可以使用Object.assign
替换源对象中的属性)
function setCar(){
// 这么写直接覆盖了 reactive 对象,car 不再是响应式的了
// car = {name:'Giant',price:3500}
// 这么写 car 是一个新的 reactive 对象,而渲染时引用的是最早初始化的 reactive 对象
// car = reactive({name:'Giant',price:3500})
// 得这么写 不要覆盖原 reactive 对象,Object.assign: 使用后面对象复制到目标对象
Object.assign(car,{name:'Giant',price:3500})
}
:::alert-warning
注意 如果是 ref
定义的是响应式对象,可以直接通过 xxx.value = 新对象
去赋值,因为 ref.value
是响应式的,所以修改 .value
不会破坏响应式关系。
:::
使用原则:
- 若需要一个基本类型的响应式数据,必须使用
ref
。 - 若需要一个响应式对象,层级不深,
ref
、reactive
都可以。 - 若需要一个响应式对象,且层级较深,推荐使用
reactive
。
:::alert-info
例如一个表单,有很多表单项,如果表单值是 ref
,那就需要在代码中写 很多 .value
,增加代码复杂度,reactive
更合适
:::
toRefs 和 toRef
- 作用:将响应式对象中的属性 转为
ref
对象 - 说明:
toRefs
与toRef
功能一致,但toRefs
可以批量转换
<template>
<div class="person">
<h2 @click="setName">姓名:{{ person.name }}</h2>
<h2 @click="setAge">年龄:{{ person.age }}</h2>
</div>
</template>
<script lang="ts" setup>
defineOptions({name:"Person"})
import { reactive,toRefs,toRef } from "vue";
let person = reactive({
name:'张三',
age:20
})
// 通过toRefs将person对象中的n个属性批量取出,且依然保持响应式的能力
let {name,age} = toRefs(person)
// 通过toRef将person对象中的 age 属性取出,且依然保持响应式的能力
let age2 = toRef(person,'age')
function setName(){
name.value += '!'
}
function setAge(){
age.value += 1
}
</script>
computed 计算属性
说明:根据已有的数据计算出新的数据 (与 Vue2 中计算属性功能一致),计算属性在多次调用、参与计算的值没有发生变化时,不会重复计算。
<template>
<div class="person">
姓:<input type="text" v-model="firstName"> <br>
名:<input type="text" v-model="lastName"> <br>
全:<span>{{ fullName }}</span> <br>
全:<span>{{ fullName }}</span> <br>
全:<span>{{ fullName }}</span> <br>
全:<span>{{ fullName }}</span> <br>
<button @click="setName">Set Name</button>
</div>
</template>
<script lang="ts" setup>
defineOptions({ name: "Person" })
import { ref,computed } from "vue";
let firstName = ref('张')
let lastName = ref('三')
// 定义的 fullName 是一个计算属性,且是只读的
let fullName = computed(() => {
return firstName.value.slice(0,1).toUpperCase() + firstName.value.slice(1) + '-' + lastName.value
})
// 这样定义的是可以修改的
let fullName2 = computed({
get(){
return firstName.value.slice(0,1).toUpperCase() + firstName.value.slice(1) + '-' + lastName.value
},
set(val){
[firstName.value,lastName.value] = val.split('-')
}
})
function setName(){
// 修改值会触发 computed 中的 set 函数
fullName2.value = 'li-si'
}
</script>
watch 监控
监视 ref 定义的基本类型
监视ref定义的【基本类型】数据:直接写数据名即可,监视的是其value值的改变。
<template>
<div class="person">
<h2>求和 Sum 为: {{ sum }}</h2>
<button @click="setSum">+1</button>
</div>
</template>
<script lang="ts" setup>
defineOptions({ name: "Person" })
import { ref,watch } from "vue";
let sum = ref(0)
function setSum(){
sum.value++
}
// 场景1,监控 ref 定义的基本类型数据
const stopWatch = watch(sum,(newVal,oldVal) => {
console.log('sum 变化了',newVal,oldVal)
// 满足条件后 停止监控
if(newVal >= 10) {stopWatch()}
})
</script>
监视 ref 定义的对象类型
监视 ref
定义的【对象类型】数据:直接写数据名,监视的是对象的【地址值】,若想监视对象内部的数据,要手动开启深度监视。
:::alert-warning
- 若修改的是
ref
定义的对象中的属性,newValue
和oldValue
都是新值,因为它们是同一个对象。 - 若修改整个 ref 定义的对象,
newValue
是新值,oldValue
是旧值,因为不是同一个对象了。
:::
<template>
<div class="person">
<h2 @click="setName">姓名:{{ person.name }}</h2>
<h2 @click="setAge">年龄:{{ person.age }}</h2>
<button @click="setPerson">全改</button>
</div>
</template>
<script lang="ts" setup>
defineOptions({ name: "Person" })
import { ref,watch } from "vue";
// 数据
let person = ref({name:'张三',age:18})
function setName(){
person.value.name += '~'
}
function setAge(){
person.value.age ++
}
function setPerson(){
person.value = {name:'李四',age:28}
}
// 这个直接监控 对象的变化,不能监控对象属性的变化
watch(person,(newVal,oldVal) => {
console.log("person 变化了",newVal,oldVal)
})
// 开启了 deep (深度监控) 对象属性也可以监控
watch(person,(newVal,oldVal) => {
console.log("person 变化了",newVal,oldVal)
},{deep:true})
</script>
监视 reactive 定义的对象类型
监视 reactive
定义的对象类型数据,默认开启深度监视
<template>
<div class="person">
<h2 @click="setName">姓名:{{ person.name }}</h2>
<h2 @click="setAge">年龄:{{ person.age }}</h2>
<button @click="setPerson">全改</button>
</div>
</template>
<script lang="ts" setup>
defineOptions({ name: "Person" })
import { reactive,watch } from "vue";
// 数据
let person = reactive({name:'张三',age:18})
function setName(){
person.name += '~'
}
function setAge(){
person.age ++
}
function setPerson(){
Object.assign(person,{name:'李四',age:28})
}
// 监视 reactive 定义的 对象类型数据,默认开启深度监视
watch(person,(newVal,oldVal)=> {
console.log('person 变化了',newVal,oldVal)
})
</script>
监视某个属性
监视 ref
或 reactive
定义的对象类型数据中的某个属性时,注意如下:
- 若该属性值不是“对象类型”需要写成函数形式
- 若该属性值依然是 “对象类型”,可以直接写,也可以写函数,这里建议写函数
结论: 监视的要是对象里的属性,那么最好写函数式,注意点:若是对象监视的是地址值,需要关注对象内部,需要手动开启深度监视。
<template>
<div class="person">
<h2>姓名:{{ person.name }}</h2>
<h2>年龄:{{ person.age }}</h2>
<h2>车:{{ person.car.c1 }} 、{{ person.car.c2 }}</h2>
<button @click="setName">修改名称</button>
<button @click="setAge">修改年龄</button>
<button @click="setCar1">修改车</button>
<button @click="setCar">修改所有车</button>
</div>
</template>
<script lang="ts" setup>
defineOptions({ name: "Person" })
import { reactive,watch } from "vue";
let person = reactive({
name:"张三",
age:18,
car:{
c1:"奥托",
c2:"五菱"
}
})
function setName(){
person.name = '李四'
}
function setAge(){
person.age++
}
function setCar1(){
person.car.c1 = '大众'
}
function setCar(){
person.car = {c1:"青桔",c2:"美团"}
}
// 监控多个属性
watch(() => person.name,() => {
console.log('person.name变化了')
})
/**
* 监控响应式对象的某个属性时,且该属性是对象类型的有以下两种情况
* 直接写属性,可以监视属性中子属性的变化,例如 person.car.c1 ,但无法监视整个 car 的变化
* 写函数返回,可以监视 person.car 的变化,但不能监视 person.car.c1 的变化。如果需要监控对象内部,这时可以开启深度监视。
*/
watch(() => person.car,() => {
console.log('person.car变化了')
},{deep:true})
</script>
监听多个属性
监控属性时可以传递一个数组,同时 newVal 也会是一个数组
<template>
<div class="person">
<h2>姓名:{{ person.name }}</h2>
<h2>年龄:{{ person.age }}</h2>
<h2>车:{{ person.car.c1 }} 、{{ person.car.c2 }}</h2>
<button @click="setName">修改名称</button>
<button @click="setAge">修改年龄</button>
<button @click="setCar1">修改车1</button>
<button @click="setCar">修改所有车</button>
</div>
</template>
<script lang="ts" setup>
defineOptions({ name: "Person" })
import { reactive,watch } from "vue";
let person = reactive({
name:"张三",
age:18,
car:{
c1:"奥托",
c2:"五菱"
}
})
function setName(){
person.name = '李四'
}
function setAge(){
person.age++
}
function setCar1(){
person.car.c1 = '大众'
}
function setCar(){
person.car = {c1:"青桔",c2:"美团"}
}
// 监控响应式对象的某个属性时,且该属性是基本类型的,需要写成函数式
watch([() => person.name,()=> person.car.c1],(newVal) => {
// 注意 这里的 newVal 是数组 ['张三', '大众']
console.log('person.name or person.car.c1 变化了',newVal)
})
</script>
watchEffect
官网:立即运行一个函数,同时响应式的追踪其依赖,并在依赖更改时重新执行该函数
人话:更智能的 watch,不需要显式指定监控的响应式变量,直接写逻辑即可。在加载时会先执行一次。
watch 和 watchEffect 对比
- 都能监听响应式数据的变化,不同的是监听数据变化的方式不同
watch
要明确指出监视的数据watchEffect
不用明确指出监视的数据(函数中用到哪些属性,就自动监视哪些属性)。
<template>
<div class="person">
<h2 @click="changeSum">当前求和为:{{ sum }}</h2>
</div>
</template>
<script lang="ts" setup>
defineOptions({ name: "Person" })
import { ref,watchEffect } from "vue";
let sum = ref(0)
function changeSum(){
sum.value+= 5
}
// 无需指定监控变量,vue 自动监控,直接写逻辑就行
// 如果需要监控多个参数,一个一个指定太麻烦了,建议用这个
watchEffect(()=>{
if(sum.value > 30) {
console.log('达到30+ 了')
}
})
</script>
标签 Ref 属性
作用:用于注册模板引用,功能类似于带命名空间的 ID ,用于设置组件内的唯一标识
- 用在普通DOM标签上,获取的是DOM节点。
- 用在组件标签上,获取的是组件实例对象。
用在 DOM 标签上
<template>
<div class="person">
<h2 ref="t1">666</h2>
<h2 ref="t2">999</h2>
<button @click="ShowLog">ShowLog</button>
</div>
</template>
<script lang="ts" setup>
import { ref, useTemplateRef,defineExpose } from 'vue';
defineOptions({ name: "Person" })
// Vue3.5+ 可以通过 useTemplateRef 拿到 ref 标识的元素
let title = useTemplateRef('t1')
// Vue3 默认通过 template 中同名的ref 绑定元素
let t2 = ref()
function ShowLog(){
console.log(title.value,t2.value)
}
// 可以在组件的最后指定 那些数据可以在外部获取
defineExpose({title})
</script>
<!-- scoped:局部样式,加上之后尽在当前组件中的标签生效 -->
<style scoped>
h2{
color: red;
}
.person {
background-color: #007acc;
padding: 20px;
}
</style>
用在组件上 (组件部分参考上方代码)
<template>
<Person ref="ren" />
<button @click="showCom">输出组件</button>
</template>
<script lang="ts" setup>
defineOptions({name:"App"})
import {ref} from 'vue'
import Person from "./components/Person.vue"
let ren = ref()
function showCom(){
// 输出组件中 title 变量的内容 <h2 data-v-4cadc14e="">666</h2>
console.log(ren.value.title)
// 输出 undefined 因为组件不允许外部获取这个变量
console.log(ren.value.t2)
}
</script>
Props 父传子
在一个父组件中引入子组件,需要向子组件传递一些数据,例如传递一个 list 让子组件进行展示,这里引入了 TS 语法 在传参时规定了类型。
src\types\index.ts
ts 结构定义
// 定义一个接口(类似 go 的结构体)
export interface User {
id:string
name:string
age:number
}
// 定义一个自定义类型的 Users
export type Users = Array<User>
src\App.vue
父组件
<template>
<!-- 这里参数前需要加 ":" 否则只会将 "users" 作为字符串传递 -->
<Person :list="users" />
</template>
<script lang="ts" setup>
defineOptions({name:"App"})
import {reactive, ref} from 'vue'
import {type Users} from '@/types/index'
import Person from "./components/Person.vue"
let users = reactive<Users>([
{id:"1",name:"666",age:18},
{id:"2",name:"999",age:15},
])
</script>
src\components\Person.vue
子组件
<template>
<div class="person">
<h2 v-for="item in list">{{item.id}} -- {{ item.name }}</h2>
</div>
</template>
<script lang="ts" setup>
import { defineProps } from 'vue';
import {type Users} from '@/types'
defineOptions({ name: "Person" })
// 第一种用法 只接收 然后在模板中使用
// defineProps(['list'])
// 第二种用法 限制接受类型 括号里面不用写内容
// defineProps<{list:Users}>()
// 第三种用法 限制类型+默认值+必要性 若父组件不传 list 就使用默认值
let props = withDefaults(defineProps<{list?:Users}>(),{
list : () => [{id:"1",name:"888",age:18}]
})
console.log(props.list)
</script>
<!-- scoped:局部样式,加上之后尽在当前组件中的标签生效 -->
<style scoped>
.person {
background-color: #007acc;
padding: 20px;
}
</style>
声明周期
概念:Vue
组件实例在创建时要经历一系列的初始化步骤,在此过程中Vue
会在合适的时机,调用特定的函数,从而让开发者有机会在特定阶段运行自己的代码,这些特定的函数统称为:生命周期钩子
- 创建阶段:
setup
- 挂载阶段:
onBeforeMount
、onMounted
- 更新阶段:
onBeforeUpdate
、onUpdated
- 卸载阶段:
onBeforeUnmount
、onUnmounted
- 常用的钩子:
onMounted
(挂载完毕)、onUpdated
(更新完毕)、onBeforeUnmount
(卸载之前)
:::alert-info
生命周期整体分为四个阶段,分别是:创建、挂载、更新、销毁,每个阶段都有两个钩子,一前一后。
父子组件的顺序类似于深度有限算法,顺序如下:
父组件
挂载前
- 子组件1
挂载前
- 子组件1
挂载后
- 子组件2
挂载前
- 子组件2
挂载后
- 子组件1
- 父组件
挂载后
:::
<template>
<div class="person">
<h2 @click="sum++">{{ sum }}</h2>
</div>
</template>
<script lang="ts" setup>
import { defineProps, onBeforeMount, onBeforeUnmount, onBeforeUpdate, onMounted, onUnmounted, onUpdated, ref } from 'vue';
defineOptions({ name: "Person" })
let sum = ref(0)
// 创建
console.log('创建')
// 挂载前
onBeforeMount(() => {
console.log('挂载前')
})
// 挂载后
onMounted(() => {
console.log('挂载后')
})
// 更新前
onBeforeUpdate(() => {
console.log('更新前')
})
// 更新后
onUpdated(() => {
console.log('更新后')
})
// 卸载前
onBeforeUnmount(() => {
console.log('卸载前')
})
onUnmounted(() => {
console.log('卸载完毕')
})
</script>
自定义 Hook
什么是 Hook?
本质上是一个函数,把 setup
函数中使用的 Composition API
进行了封装,把同一功能的逻辑代码、业务数据汇总在了一起,方便后期功能维护。
自定义hook的优势:复用代码, 让setup中的逻辑更清楚易懂。
业务代码:
src\hooks\useDog.ts
将 加载dog的方法、doglist 响应式变量 都放入 useDog 这个文件中
注意:文件名称要按照功能来,比如 Dog 相关的功能就叫 useDog.ts
,订单相关的东西 就叫 useOrder.ts
import { defineProps, onMounted,reactive } from 'vue';
import axios from 'axios';
export default () => {
// 存储狗的列表
let dogList = reactive<string[]>([])
// 加载一张新图片
async function getDog() {
let result = await axios.get('https://dog.ceo/api/breed/pembroke/images/random')
dogList.push(result.data.message)
}
// hooks 中也能写生命周期函数, 挂载后直接加载一只狗
onMounted(() => {
getDog()
})
// 将工具和方法暴露出去
return {dogList,getDog}
}
src\components\Person.vue
Person 组件,引入并使用 useDog
<template>
<div class="person">
<img v-for="dog in dogList" :src="dog" alt="">
<button @click="getDog">加载一只狗</button>
</div>
</template>
<script lang="ts" setup>
defineOptions({ name: "Person" })
import useDog from '@/hooks/useDog';
const {dogList,getDog} = useDog()
</script>
路由 Router
vue3 中使用路由 需要先安装 npm i vue-router
基本切换效果
src\router\index.ts
路由组件基本代码:
// 创建一个路由
// 引入路由
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
}
]
})
// 暴露出去
export default router
src\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')
src\App.vue
代码如下:
<template>
<div class="app">
<!-- 标题 -->
<h2>Vue Router</h2>
<div class="nav">
<!--
RouterLink 标签定义 路由跳转连接
to 定义跳转地址 不能写 href
active-class 定义选中后的 class
-->
<RouterLink to="/home" active-class="active">首页</RouterLink>
<RouterLink to="/news" active-class="active">新闻</RouterLink>
<RouterLink to="/about" active-class="active">关于</RouterLink>
</div>
<div class="main">
<RouterView></RouterView>
</div>
</div>
</template>
<script lang="ts" setup>
defineOptions({name:"App"})
</script>
<style scoped>
.app {width: 800px; margin: 0 auto;}
h2{background-color: bisque;border:1px solid blue;text-align: center;}
.nav{display: flex;justify-content: space-around;width: 100%;}
.nav a{display: flex;justify-content: center;width: 200px;text-align: center;line-height: 50px;background-color: rosybrown;}
.nav a.active{background-color: blueviolet;}
.main{margin-top: 20px;;border: 1px solid #ccc;width: 800px;height: 300px;}
</style>
路由 两个注意点
路由组件通常放在
/src/pages
或/src/views
文件夹,一般组件通常放在components
文件夹- 路由组件:靠路由规则渲染出来的
routes:[{path:'/demo',component:Demo}]
- 一般组件:写标签实现一个功能的组件
<Demo/>
- 路由组件:靠路由规则渲染出来的
- 通过路由切换的组件默认是被卸载掉了,需要的时候再去加载。
路由工作模式
History 模式
优点:路径中不带 #
直接写路径,从用户角度看 URL
更美观
缺点:生产环境中需要 Web 服务器处理路径问题,否则会出现 404 错误。(注:就是后端说的路由重新)
const router = createRouter({
history:createWebHistory(), //history模式
/******/
})
Nginx 配置:
location / {
root /root/project';
index index.html
try_files $uri $uri/ /index.html
}
Hash 模式
特点:兼容性更好,不需要服务器做特殊处理
缺点:URL
中带有 #
不太美观,且 SEO
优化方面相对较差
const router = createRouter({
history:createWebHashHistory(), //hash模式
/******/
})
to 的两种写法
字符串的写法
<!-- 第一种:to的字符串写法 -->
<router-link active-class="active" to="/home">主页</router-link>
对象写法
<!-- 第二种:to的对象写法,这种方式方便传参 -->
<router-link active-class="active" :to="{path:'/home'}">Home</router-link>
命名路由
作用:可以简化路由跳转及 params 传参需要用到
路由规则:
routes:[
{
name:'zhuye',
path:'/home',
component:Home
},
{
name:'xinwen',
path:'/news',
component:News,
},
{
name:'guanyu',
path:'/about',
component:About
}
]
跳转路由:
<!--简化后:直接通过名字跳转(to的对象写法配合name属性) -->
<router-link :to="{name:'guanyu'}">跳转</router-link>
嵌套路由
场景:例如系统配置页面,路由是 /setting
,但这个页面中需要根据页面内的菜单 再嵌套更具体的配置页面,这时就需要 嵌套路由了(注:嵌套路由本质是 在一个路由组件中嵌套另一个路由组件,如果两个组件之间没有嵌套关系 则不需要使用嵌套路由)。
这里以新闻页面为例,嵌套显示新闻详情
路由规则配置
const router = createRouter({
history: createWebHistory(), // 指定工作模式
routes:[
...
{
path:'/news',
component: News,
children:[
// 子路由 访问路径: /news/detail
{path: 'detail',component:Detail}
]
},
...
]
})
src\pages\News.vue
新闻组件
<template>
<div class="news">
<ul>
<li v-for="news in newsList" :key="news.id">
<RouterLink :to="{path:'/news/detail'}">{{ news.title }}</RouterLink>
</li>
</ul>
<div class="news-conent">
<RouterView></RouterView>
</div>
</div>
</template>
<script lang="ts" setup>
defineOptions({ name: "News" });
import { reactive, ref } from "vue";
const newsList = reactive([
{id:"1",title:"新闻1",content:"111"},
{id:"2",title:"新闻2",content:"222"},
{id:"3",title:"新闻3",content:"333"},
]);
</script>
路由传参
Query 传参
通过 Query
的方式从URL种传参
传递参数
<template>
<div class="news">
<ul>
<li v-for="news in newsList" :key="news.id">
<!-- 第一种写法 模板字符串 -->
<!-- <RouterLink :to="`/news/detail?id=${news.id}`">{{ news.title }}</RouterLink> -->
<!-- 第二种写法 -->
<RouterLink
:to="{
path:'/news/detail',
query:{
id:news.id,
title:news.title,
content:news.content
}
}">{{ news.title }}</RouterLink>
</li>
</ul>
<div class="news-conent">
<RouterView></RouterView>
</div>,
</div>
</template>
<script lang="ts" setup>
defineOptions({ name: "News" });
import { reactive, ref } from "vue";
const newsList = reactive([
{id:"1",title:"新闻1",content:"111"},
{id:"2",title:"新闻2",content:"222"},
{id:"3",title:"新闻3",content:"333"},
]);
</script>
获取参数
<template>
<ul class="news-list">
<li>编号:{{query.id}}</li>
<li>标题:{{query.title}}</li>
<li>内容:{{query.content}}</li>
</ul>
</template>
<script lang="ts" setup>
import { toRefs } from 'vue';
import { useRoute } from 'vue-router';
defineOptions({name:"Detail"})
let route = useRoute()
let {query} = toRefs(route)
</script>
Params 参数
在路由种使用 占位符省略 url传参时的 Key
:::alert-warning
- 传递
params
参数时,若使用to
的对象写法,必须使用name
配置项,不能使用path
- 传递
params
参数时,需要提前在路由规则中占位,若参数为可选参数,需要使用?
结尾,例如:/:xxx?
:::
路由规则
// 创建一个路由
// 引入路由
import {createRouter,createWebHistory} 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:[
{
path:'/home',
component: Home
},
{
path:'/news',
component: News,
children:[
// 子路由
{
// params 传参对象形式必须用 name
name:'xq',
// content 加 "?" 表示可选参数
path: 'detail/:id/:title/:content?',
component:Detail
}
]
},
{
path:'/about',
component: About
}
]
})
// 暴露出去
export default router
传递参数
<template>
<div class="news">
<ul>
<li v-for="news in newsList" :key="news.id">
<!-- 第一种写法 模板字符串 -->
<!-- <RouterLink :to="`/news/detail/${news.id}/${news.title}/${news.content}`">{{ news.title }}</RouterLink> -->
<!-- 第二种写法 -->
<RouterLink :to="{
name:'xq',
params:news
}">{{ news.title }}</RouterLink>
</li>
</ul>
<div class="news-conent">
<RouterView></RouterView>
</div>
</div>
</template>
<script lang="ts" setup>
defineOptions({ name: "News" });
import { reactive, ref } from "vue";
const newsList = reactive([
{id:"1",title:"新闻1",content:"111"},
{id:"2",title:"新闻2",content:"222"},
{id:"3",title:"新闻3",content:"333"},
]);
</script>
接收参数
<template>
<ul class="news-list">
<li>编号:{{route.params.id}}</li>
<li>标题:{{route.params.title}}</li>
<li>内容:{{route.params.content}}</li>
</ul>
</template>
<script lang="ts" setup>
import { toRefs } from 'vue';
import { useRoute } from 'vue-router';
defineOptions({name:"Detail"})
let route = useRoute()
console.log(route.params)
</script>
<style scoped>
</style>
Props 参数
作用:让路由组件可以通过 Porps 的方式获取参数
路由写法
{
// params 传参对象形式必须用 name
name:'xq',
// content 加 "?" 表示可选参数
path: 'detail/:id/:title/:content?',
component:Detail,
// props 布尔值写法,把收到的每一组params参数,作为 props 传递给 Detail 组件
// props:true,
// props 函数写法,把函数返回的每一组 Key-Value 作为 props 传递给 Detail 组件
props: (route) => route.query,
// props 对象写法,把对象的每一组 Key-Value 作为 props 传递给 Detail 组件
//props: {a:1,b:2}
}
接收参数
<template>
<ul class="news-list">
<li>编号:{{id}}</li>
<li>标题:{{title}}</li>
<li>内容:{{content}}</li>
</ul>
</template>
<script lang="ts" setup>
import { toRefs } from 'vue';
import { useRoute } from 'vue-router';
defineOptions({name:"Detail"})
defineProps(['id','title','content'])
</script>
replace 属性
作用:控制路由跳转时操作浏览器历史记录的模式。
浏览器的历史记录有两种写入方式:分别为push和replace:
push
是追加历史记录(默认值)。replace
是替换当前记录。
开启 replace 模式
<RouterLink replace .......>News</RouterLink>
函数式编程导航
大白话:通过 JS 实现 RouterLink 的功能,完成路由跳转
场景
- 只能通过 JS 跳转的,例如只有到时间了才能跳转到某个页面,
RouterLink
做不到。 - 其他事件触发跳转的,例如鼠标滑过触发跳转。
不使用
RouterLink
也想跳转<template> <div class="news"> <ul> <li v-for="news in newsList" :key="news.id"> <button @click="showDetail(news)">查看</button> <RouterLink :to="{ name:'xq', params:news }">{{ news.title }}</RouterLink> </li> </ul> <div class="news-conent"> <RouterView></RouterView> </div> </div> </template> <script lang="ts" setup> defineOptions({ name: "News" }); import { reactive } from "vue"; import { useRouter } from "vue-router"; const newsList = reactive([ {id:"1",title:"新闻1",content:"111"}, {id:"2",title:"新闻2",content:"222"}, {id:"3",title:"新闻3",content:"333"}, ]); // 注意 这里是路由器 不是路由 const router = useRouter() // 通过函数跳转路由 function showDetail(news:any) { router.push({ name:'xq', params:news }) } </script>
路由重定向
作用:当匹配到 path 时,重定向到指定路由
{
path:'/',
redirect:'/home'
}
Pinia(状态管理)
大白话说法:就是组件之间数据的全局共享,例如 登录组件用户登录之后,获取到的用户信息 可以交给 状态管理工具来维护,(类似后端的 Session)
安装 Pinia
安装库
npm i pinia
src\main.ts
引入
import {createApp} from 'vue'
import App from './App.vue'
// 引入 pinia
import { createPinia } from 'pinia'
const app = createApp(App)
// 创建 pinia
const pinia =createPinia()
app.use(pinia)
app.mount('#app')
存储加读取数据
- 在 src 目录中添加
store
目录,是用来保存 状态、业务逻辑 的实体,每个组件都可以 读取、写入 它。 - 它有三个概念:
state
、getter
、action
,相当于组件中的:data
(数据)、computed
(计算属性)、methods
(方法)。
pinia 编码
文件:src/store/count.ts
// 引入defineStore用于创建store
import {defineStore} from 'pinia'
// 定义并暴露一个store
export const useCountStore = defineStore('count',{
// 动作
actions:{},
// 状态
state(){
return {
sum:6
}
},
// 计算
getters:{}
})
组件中使用
文件:src\components\Count.vue
<template>
<div class="count">
<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="sub()">减</button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
defineOptions({name:"Count"})
import {useCountStore} from '@/store/count'
const countStore = useCountStore()
let n = ref(1) // 用户选择的步长
function add(){
countStore.sum+=n.value
}
function sub(){
countStore.sum-=n.value
}
</script>
修改数据的三种方式
直接修改
countStore.sum = 666
批量修改
修改多条数据可以用这个,只会触发一次修改操作,性能更优
countStore.$patch({
sum:666,
text:"Hello,dbkuaizi"
})
借助 Action 修改
在 store 中可以写一些逻辑,例如 在多个页面有用户注销、下单 这些功能,可以通过 action 维护
import { defineStore } from 'pinia'
export const useCountStore = defineStore('count', {
/*************/
actions: {
//加
increment(value:number) {
if (this.sum < 10) {
//操作countStore中的sum
this.sum += value
}
},
//减
decrement(value:number){
if(this.sum > 1){
this.sum -= value
}
}
},
/*************/
})
在组件中使用:
// 使用countStore
const countStore = useCountStore()
// 调用对应action
countStore.increment(n.value)
storeToRefs
- 借助storeToRefs将store中的数据转为ref对象,方便在模板中使用。
- 注意:pinia提供的storeToRefs只会将数据做转换,而Vue的toRefs会转换store中数据。
<template>
<div class="count">
<h2>当前求和为:{{ sum }} </h2>
<h3>{{ text }}</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="sub()">减</button>
</div>
</template>
<script setup lang="ts">
defineOptions({name:"Count"})
import { ref } from 'vue';
import {useCountStore} from '@/store/count'
import { storeToRefs } from 'pinia';
const countStore = useCountStore()
// storeToRefs 只会关注 store 中的 state 数据部分,不会堆 方法进行 ref 包裹
const {sum,text} = storeToRefs(countStore)
let n = ref(1) // 用户选择的步长
function add(){
countStore.increment(n.value)
}
function sub(){
countStore.sum-=n.value
}
</script>
Getters
作用:当 state
中的数据需要经过处理后再使用,可以通过 getters
配置
import {defineStore} from 'pinia'
export const useCountStore = defineStore('count',{
state(){
return {
sum:6,
text:"hello word"
}
},
// 类似于计算属性
getters: {
// 使用箭头函数
bigSum:state => state.sum * 10,
// 使用 普通函数写法 需要指定返回值类型
upperText():string{
return this.text.toUpperCase()
}
}
})
$subscribe 监听数据变化
// 数据
const talkStore = useTalkStore()
// 通过 store 的 $subscribe() 方法侦听 state 及其变化
talkStore.$subscribe((mutate,state)=>{
console.log(mutate,state)
localStorage.setItem('talkList',JSON.stringify(state.talkList))
})
Store 组合式写法
import {defineStore} from 'pinia'
import axios from 'axios';
import { nanoid } from 'nanoid';
// 选项式
export const useTalkStore = defineStore('talk',{
state(){
return {
talkList:JSON.parse(localStorage.getItem('talkList') as string) || []
}
},
actions:{
// 方法
async getTalk(){
// 发送请求 拿到响应值通过连续解构 赋值+重命名给 title
let {data:{content:title}} = await axios.get("https://api.uomg.com/api/rand.qinghua?format=json")
// 放入数组中
this.talkList.push({id: nanoid(),title})
}
}
})
// 组合式
import { reactive } from 'vue';
export const useTalkStore2 = defineStore('talk',()=>{
const talkList = reactive(JSON.parse(localStorage.getItem('talkList') as string) || [] )
async function getTalk(){
// 发送请求 拿到响应值通过连续解构 赋值+重命名给 title
let {data:{content:title}} = await axios.get("https://api.uomg.com/api/rand.qinghua?format=json")
// 放入数组中
talkList.push({id: nanoid(),title})
}
return {talkList,getTalk}
});
组件通信
组件关系 | 传递方式 |
---|---|
父传子 | 1.props 2. v-model 3. $refs 4.默认插槽、具名插槽 |
子传父 | 1. props 2.自定义事件 3. v-model 4. $parent 5.作用域插槽 |
祖传孙、孙传祖 | 1. $attrs 2. provide 、inject |
兄弟间、任意组件间 | 1.mitt 2. pinia |
props
props
是使用频率最高的父子组件通讯方式,常用于:父 <=> 子
- 父传子,属性值是非函数
- 子传父,属性值是函数
父组件
<template>
<div class="father">
<h3>父组件,</h3>
<h4>我的车:{{ car }}</h4>
<h4>儿子给的玩具:{{ toy }}</h4>
<Child :car="car" :getToy="getToy"/>
</div>
</template>
<script setup lang="ts" name="Father">
import Child from './Child.vue'
import { ref } from "vue";
// 数据
const car = ref('奔驰')
const toy = ref()
// 方法
function getToy(value:string){
toy.value = value
}
</script>
子组件
<template>
<div class="child">
<h3>子组件</h3>
<h4>我的玩具:{{ toy }}</h4>
<h4>父给我的车:{{ car }}</h4>
<button @click="getToy(toy)">玩具给父亲</button>
</div>
</template>
<script setup lang="ts" name="Child">
import { ref } from "vue";
const toy = ref('奥特曼')
defineProps(['car','getToy'])
</script>
自定义事件
概述:自定义事件通常用于子传父,简单来说就是子组件定义一个事件,并在需要的时候触发这个事件。接着在父组件使用子组件时,注册对应的自定义事件,并通过回调函数获取子组件的传参。
:::alert-info
事件传参的本质
- 接收端: 定义一个函数,这个函数接受一个或多个参数,这些参数就是函数调用时发送方发送的参数,
- 发送端:调用接收端定义的函数,并在调用时将需要传递的数据通过传参发送给对方
:::
注意:需要区分原生事件和自定义事件
原生事件:
- 事件名是特定的(
click
、mosueenter
等等) - 事件对象
$event
: 是包含事件相关信息的对象(pageX
、pageY
、target
、keyCode
)
- 事件名是特定的(
自定义事件:
- 事件名是任意名称
- 事件对象
$event
: 是调用emit
时所提供的数据,可以是任意类型!!!
示例
<!--在父组件中,给子组件绑定自定义事件:-->
<Child @send-toy="toy = $event"/>
<!--注意区分原生事件与自定义事件中的$event-->
<button @click="toy = $event">测试</button>
//子组件中,触发事件:
this.$emit('send-toy', 具体数据)
父组件
<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){
console.log('saveToy',value)
toy.value = value
}
</script>
子组件
<template>
<div class="child">
<h3>子组件</h3>
<h4>玩具:{{ toy }}</h4>
<!-- 点击时,通过 emit 调用自定义事件 send-toy,并传递 toy 参数 -->
<button @click="emit('send-toy',toy)">测试</button>
</div>
</template>
<script setup lang="ts" name="Child">
import { ref } from "vue";
// 数据
let toy = ref('奥特曼')
// 声明事件
const emit = defineEmits(['send-toy'])
</script>
Mitt
与 Vue2 的 $bus
、事件发布与订阅(pubsub
)功能类似,可以实现任意组件间的通信。
mitt 和自定义函数玩法很像,只是通过一个第三方库实现事件的订阅和触发,进而完成数据的交互工作。区别在于自定义组件,只能实现父子之间的数据传递,而第三方库只需要响应的组件引入,就可是实现事件的订阅与触发。
安装 mitt
npm i mitt
新建文件:src\utils\emitter.ts
用于存放 mitt 初始化 全局变量
// 引入mitt
import mitt from "mitt";
// 创建emitter
const emitter = mitt()
/*
// 绑定事件
emitter.on('abc',(value)=>{
console.log('abc事件被触发',value)
})
emitter.on('xyz',(value)=>{
console.log('xyz事件被触发',value)
})
setInterval(() => {
// 触发事件
emitter.emit('abc',666)
emitter.emit('xyz',777)
}, 1000);
setTimeout(() => {
// 清理事件
emitter.all.clear()
}, 3000);
*/
// 创建并暴露mitt
export default emitter
数据接收端
绑定 send-toy
事件,
import emitter from "@/utils/emitter";
import { onUnmounted } from "vue";
// 绑定事件
emitter.on('send-toy',(value)=>{
console.log('send-toy事件被触发',value)
})
onUnmounted(()=>{
// 解绑事件
emitter.off('send-toy')
})
发送端触发
import emitter from "@/utils/emitter";
function sendToy(){
// 触发事件
emitter.emit('send-toy',toy.value)
}
v-model 传参
:::alert-warning
这一节讲的是 UI 组件库,如何通过 v-model
实现父组件中响应式变量的双向绑定的底层原理,实际开发中如果不写底层组件 很少会用到这个。
但是这个知识点很重要,需要理解底层原理。
:::
概述:实现父子组件间的双向通信
v-model 的本质
<!-- 使用 v-model 指令 -->
<input type="text" v-model="userName">
<!-- v-model的本质是下面这行代码
本质上是两个操作,一个是数据展示时 通过 :value 绑定,然后在更新时通过 @input 触发
-->
<input
type="text"
:value="userName"
@input="userName =(<HTMLInputElement>$event.target).value"
>
组件上的 v-model
本质::modelValue
+ @update:modelValue
事件
<!-- 组件标签上使用v-model指令 -->
<AtguiguInput v-model="userName"/>
<!-- 组件标签上v-model的本质 -->
<AtguiguInput :modelValue="userName" @update:modelValue="userName = $event"/>
AtguiguInput 组件中
<template>
<div class="box">
<!--将接收的value值赋给input元素的value属性,目的是:为了呈现数据 -->
<!--给input元素绑定原生input事件,触发input事件时,进而触发update:model-value事件-->
<input
type="text"
:value="modelValue"
@input="emit('update:model-value',$event.target.value)"
>
</div>
</template>
<script setup lang="ts" name="AtguiguInput">
// 接收props
defineProps(['modelValue'])
// 声明事件
const emit = defineEmits(['update:modelValue'])
</script>
指定 value 的名称
如果指定 value 的名称,则可以传递多个 双向绑定变量
<AtguiguInput v-model:ming="username" v-model:mima="password"/>
AtguiguInput
组件中 可以这样写
<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>
:::alert-info
$event 是啥? 什么时候使用 $event.target ?
- 对于原生事件(@input 这种),
$event
时事件对象,可以.target
- 对于自定义事件,
$event
是触发时 所传递的数据,不能.target
:::
defineModel 传参
defineModel
是 Vue 3.4+ 开始支持且官方推荐的一种组件双向绑定的写法,可以粗暴的理解为是原始 v-model
的语法糖写法。
我们以上面的 v-model
为例 使用 defineModel 可以大幅简化组件内部响应值的写法,Vue 替我们完成了 :modelValue
+ @update:modelValue
事件。
<template>
<input type="text" v-model="ming">
<br>
<input type="text" v-model="mima">
</template>
<script setup lang="ts" name="AtguiguInput">
// 如果使用的元素中没有指定 value 名称 就可以直接获取
let val = defineModel()
// 如果指定了 value 的名称 或者需要传递多个,则传递一个 绑定的value名称即可
let ming = defineModel('ming')
let mima = defineModel('mima')
</script>
attrs 祖传孙
说明:$attrs
是一个对象,包含了所有父组件传递的标签属性
:::alert-warning
注意:$attrs
会自动排除 props
中声明的属性(可以认为声明过的 props
被子组件自己“消费”了)
:::
// 父组件
<template>
<div class="father">
<h3>父组件</h3>
<Child :a="a" :b="b" :c="c" :d="d" v-bind="{x:100,y:200}" :updateA="updateA"/>
</div>
</template>
<script setup lang="ts" name="Father">
import Child from './Child.vue'
import { ref } from "vue";
let a = ref(1)
let b = ref(2)
let c = ref(3)
let d = ref(4)
function updateA(value){
a.value = value
}
</script>
// 子组件 =============
<template>
<div class="child">
<h3>子组件</h3>
<h4> {{ a }} </h4>
<!-- 继续向下传递父组件传递过来的、在当前组件中没有 defineProps 接收的参数 -->
<GrandChild v-bind="$attrs"/>
</div>
</template>
<script setup lang="ts" name="Child">
import GrandChild from './GrandChild.vue'
defineProps(['a'])
</script>
// 孙组件 =============
<template>
<div class="grand-child">
<h3>孙组件</h3>
<h4>b:{{ b }}</h4>
<h4>c:{{ c }}</h4>
<h4>d:{{ d }}</h4>
<h4>x:{{ x }}</h4>
<h4>y:{{ y }}</h4>
<button @click="updateA(666)">点我更新A</button>
</div>
</template>
<script setup lang="ts" name="GrandChild">
defineProps(['b','c','d','x','y','updateA'])
</script>
$refs 与 $parent
概述: 前面提到 $ref
是组件的唯一标识,作用类似于传统 DOM 中的 ID,所以我们也可以通过 $ref
的方式来 操作对应的组件数据、事件。
$refs
值为对象,包含所有被ref
属性标识的DOM元素或组件实例。$parent
值为对象,当前组件的父组件实例对象。
:::alert-info
让外部获取数据的前提时通过 defineExpose
向外暴露
:::
父组件
<template>
<div class="father">
<h3>父组件</h3>
<h4>房产:{{ house }}</h4>
<button @click="getAllChild($refs)">让所有孩子的书变多</button>
<Child1 ref="c1"/>
</div>
</template>
<script setup lang="ts" name="Father">
import Child1 from './Child1.vue'
import { ref,reactive } from "vue";
// 数据
let house = ref(4)
// 遍历所有被 ref 标记的标签
function getAllChild(refs:{[key:string]:any}){
console.log(refs)
for (let key in refs){
refs[key].book += 3
}
}
// 向外部提供数据
defineExpose({house})
</script>
子组件
<template>
<div class="child1">
<h3>子组件1</h3>
<h4>玩具:{{ toy }}</h4>
<h4>书籍:{{ book }} 本</h4>
<button @click="minusHouse($parent)">干掉父亲的一套房产</button>
</div>
</template>
<script setup lang="ts" name="Child1">
import { ref } from "vue";
// 数据
let toy = ref('奥特曼')
let book = ref(3)
// 方法
function minusHouse(parent:any){
parent.house -= 1
}
// 把数据交给外部
defineExpose({toy,book})
</script>
provide 与 inject
概述:实现父组件与后代组件的直接通信,而不用操作中间组件
- 在祖先组件中通过
provide
配置向后代组件提供数据 - 在后代组件中通过
inject
配置来声明接收数据
父组件 使用 provide
提供数据
<template>
<div class="father">
<h3>父组件</h3>
<h4>资产:{{ money }}</h4>
<h4>汽车:{{ car }}</h4>
<button @click="money += 1">资产+1</button>
<button @click="car.price += 1">汽车价格+1</button>
<Child/>
</div>
</template>
<script setup lang="ts" name="Father">
import Child from './Child.vue'
import { ref,reactive,provide } from "vue";
// 数据
let money = ref(100)
let car = reactive({
brand:'奔驰',
price:100
})
// 用于更新money的方法
function updateMoney(value:number){
money.value += value
}
// 提供数据
provide('moneyContext',{money,updateMoney})
provide('car',car)
</script>
后代组件 通过 inject
获取祖辈组件提供的数据
<template>
<div class="grand-child">
<h3>我是孙组件</h3>
<h4>资产:{{ money }}</h4>
<h4>汽车:{{ car }}</h4>
<button @click="updateMoney(6)">点我</button>
</div>
</template>
<script setup lang="ts" name="GrandChild">
import { inject } from 'vue';
// 注入数据 注: inject 第二个参数是默认值,当注入的参数不存在时,使用该默认值
let {money,updateMoney} = inject('moneyContext',{money:0,updateMoney:(x:number)=>{}})
let car = inject('car')
slot 插槽
在父组件中使用子组件,可以将不确定、由父组件决定内容的部分定义为插槽,可以简单的理解为预览内容的占位符。
默认插槽
父组件中:
<Category title="今日热门游戏">
<ul>
<li v-for="g in games" :key="g.id">{{ g.name }}</li>
</ul>
</Category>
子组件中:
<template>
<div class="item">
<h3>{{ title }}</h3>
<!-- 默认插槽 -->
<slot>默认内容</slot>
</div>
</template>
具名插槽
说明:默认插槽只能有一个,具名插槽可以有多个
父组件中: 通过 v-slot
或 #插槽名
指定插槽名称
<Category title="今日热门游戏">
<template v-slot:s1>
<ul>
<li v-for="g in games" :key="g.id">{{ g.name }}</li>
</ul>
</template>
<template #s2>
<a href="">更多</a>
</template>
</Category>
子组件中: 通过 name
设置插槽名称
<template>
<div class="item">
<h3>{{ title }}</h3>
<slot name="s1"></slot>
<slot name="s2"></slot>
</div>
</template>
作用域插槽
概念:数据在组件的自身,但根据数据生成的结构需要组件的使用者来决定。
大白话:数据在子组件中,但根据数据生成的结构,却由父亲决定。
场景:
- 前端 UI 库的数据表格组件,数据表格的渲染是组件内部实现的,但使用时想自定义列的单元格内容,就可以通过作用域插槽实现。
- 前端 UI 库的弹窗组件,弹窗的内容可以通过作用域插槽实现
父组件:
<Game v-slot="params">
<!-- <Game v-slot:default="params"> -->
<!-- <Game #default="params"> -->
<ul>
<li v-for="g in params.games" :key="g.id">{{ g.name }}</li>
</ul>
</Game>
子组件:
<template>
<div class="category">
<h2>今日游戏榜单</h2>
<slot :games="games" a="哈哈"></slot>
</div>
</template>
<script setup lang="ts" name="Category">
import {reactive} from 'vue'
let games = reactive([
{id:'asgdytsa01',name:'英雄联盟'},
{id:'asgdytsa02',name:'王者荣耀'},
{id:'asgdytsa03',name:'红色警戒'},
{id:'asgdytsa04',name:'斗罗大陆'}
])
</script>
其他 API
shallowRef 与 shallowReactive
shallowRef
和 shallowReactive
的作用是创建一个响应式的数据、或响应式的对象,然后只对最顶层的数据做响应式的处理,对更深层的数据不处理。
用法:
let myVar = shallowRef(initialValue);
const myObj = shallowReactive({ ... });
:::alert-info
总结
通过使用 shallowRef() 和 shallowReactive() 来绕开深度响应。浅层式 API 创建的状态只在其顶层是响应式的,对所有深层的对象不会做任何处理,避免了对每一个内部属性做响应式所带来的性能成本,这使得属性的访问变得更快,可提升性能。
:::
readDonly 与 shallowReadonly
说明:创建响应式数据的深只读副本,效果类似于数据库的视图 (可以读,原变量修改,这个数据可以跟着变,但不能修改这个数据)。
特点:
- 对象的所有嵌套属性都将变为只读。
- 任何尝试修改这个对象的操作都会被阻止(在开发模式下,还会在控制台中发出警告)。
应用场景:
- 创建不可变的状态快照。
- 保护全局状态或配置不被修改。
let sum1 = ref(0)
let sum2 = readonly(sum1)
const original = reactive({ ... });
const readOnlyCopy = readonly(original);
shallowReadonly
与 readDonly 效果类似,但只作用于对象的顶层属性,对象内部的嵌套属性仍然是可变的。
特点:
- 只将对象的顶层属性设置为只读,对象内部的嵌套属性仍然是可变的。
- 适用于只需保护对象顶层属性的场景。
const original = reactive({ ... });
const shallowReadOnlyCopy = shallowReadonly(original);
toRaw 与 markRaw
toRaw
作用:用于获取一个响应式对象的原始对象。(toRaw
返回的对象不再是响应式的,对其进行修改也不会触发视图更新)
:::alert-info
官网描述:这是一个可以用于临时读取而不影响代理访问/跟踪开销,或是写入而不触发更改的特殊方法。 不建议保持对原始对象的持久引用,需要谨慎使用。
:::
:::alert-info
使用场景:在需要讲数据提供给非 Vue
库或外部系统时,使用 toRaw
可以确保它们收到的是普通对象。
- axios 向服务端传递表单数据时
- 向外部提供数据时(例如下面代码中的
showPerson
方法) - lodash 处理函数
:::
<template>
<h2>{{person.name}}</h2>
<h2>{{ person.age }}</h2>
</template>
<script setup lang="ts">
import { reactive, toRaw } from 'vue';
defineOptions({name:"App"})
// 响应式对象
let person = reactive({
name:'tony',
age:18
})
// 原始对象
let rawPerson = toRaw(person)
// 通过 toRaw 可以向外部提供一个非响应式的原始类型
// 不用担心 showPerson 对 persion 的操作影响到
showPerson(toRaw(person))
function showPerson(p:any) {
p.age += 1 // 如何修改也不影响 person
console.log(p)
}
</script>
markRaw
作用:标记一个对象,使其永远不会变成响应式的。
:::alert-info
例如使用 mockjs
时,为了防止误把 mockjs
变为响应式对象,可以使用 markRaw
去标记 mockjs
:::
<script setup lang="ts">
import { markRaw, reactive } from 'vue';
defineOptions({name:"App"})
// 用markRow 标记一下
let person = markRaw({
name:'tony',
age:18
})
let person2 = reactive(person)
console.log(person)
// person2 依旧是原始对象
console.log(person2)
</script>
customRef 自定义 Ref
作用:创建一个自定义的 ref
,并对其依赖项跟踪和更新触发进行逻辑控制。
人话:自己实现 Ref 响应式数据的效果,因为是自己实现的所以可以在数据的获取时、修改时执行一些逻辑
概念梳理:
track
: 向 Vue 标记这个数据需要持续关注,当数据发生变化时(调用trigger()
) 需要重新通过 get 获取数据trigger
: 通知Vue
Set 方法已经对数据修改完成。Ref
和customRef
的区别:Ref
是自动挡,你只管踩油门,别的不需要管。customRef
是手动挡,离合、换挡都需要自己控制,麻烦一些,但可以在这个过程中实现更多需求
自定义 Ref 实现代码:
<template>
<h2>{{ msg }}</h2>
<input type="text" v-model="msg">
</template>
<script setup lang="ts">
import { customRef, markRaw, reactive, ref } from 'vue';
defineOptions({name:"App"})
// 使用 vue 提供的默认的 ref 定义响应式数据,数据修改 页面自动更新 (自动挡)
// let msg = ref("你好")
// 使用 Vue 提供的 customRef 定义响应式数据(手动挡)
let initMsg = '你好'
// track(跟踪、订阅)、trigger(触发、发布)
let msg = customRef((track,trigger) => {
return {
// get 在 msg 读取时调用
get(){
// 向 Vue 标记 msg 数据很重要,需要持续关注,一旦数据发生变化,就去更新
track()
return initMsg
},
// set 在 msg 修改时调用
set(val){
initMsg = val
// 通知 vue 数据发生了变化
trigger()
},
}
})
</script>
通过 Hooks 封装
useMsgRef.ts
代码
import {customRef} from 'vue'
// 封装成 hooks 传入默认值,延迟时间
export default function(initValue:string,delay:number) {
// 定义一个定时器,实现防抖的效果
let timer:number
// track(跟踪、订阅)、trigger(触发、发布)
let msg = customRef((track,trigger) => {
return {
// get 在 msg 读取时调用
get(){
// 向 Vue 标记 msg 数据很重要,需要持续关注,一旦数据发生变化,就去更新
track()
return initValue
},
// set 在 msg 修改时调用
set(val){
// 使用定时器 延时指定时间触发
clearTimeout(timer)
timer = setTimeout(() => {
initValue = val
// 通知 vue 数据发生了变化
trigger()
}, delay);
},
}
})
return {msg}
}
App.Vue
代码:
<template>
<h2>{{ msg }}</h2>
<input type="text" v-model="msg">
</template>
<script setup lang="ts">
import {ref } from 'vue';
import useMsgRef from './useMsgRef';
defineOptions({name:"App"})
// 使用 vue 提供的默认的 ref 定义响应式数据,数据修改 页面自动更新 (自动挡)
// let msg = ref("你好")
// 使用 useMsgRef 定义响应式数据,且有延迟更新的效果
let {msg} = useMsgRef('hello',1000)
</script>
Vue3 新组件
Teleport 传送门
将子组件的 HTML 结构,移动到指定位置。
例如下方代码 默认是在 #app
元素中渲染, 使用 to='body'
将元素渲染到 Body 标签中,脱离了 #app
根元素,且不影响交互逻辑
<teleport to='body' >
<div class="modal" v-show="isShow">
<h2>我是一个弹窗</h2>
<p>我是弹窗中的一些内容</p>
<button @click="isShow = false">关闭弹窗</button>
</div>
</teleport>
Suspense
官方文档:https://cn.vuejs.org/guide/built-ins/suspense.html#suspense
当子组件中包含了异步任务(例如网络请求), setup
又是在组件未挂在前执行,若在 setup 中加载异步任务,如果异步任务执行较慢(例如网络请求加载用户信息)。就会造成异步加载时组件无法渲染,空着一块用户体验不好的问题。
通过 Suspense
可以实现一个 load 的效果,子组件阻塞时展示一部分内容,加载完成时展示子组件内容。
:::alert-warning
<Suspense>
是一项实验性功能。- 骨架屏有专门的解决方案,不要用这个代替骨架屏
:::
<template>
<Suspense>
<!-- 加载完成显示组件内容 -->
<template #default>
<!-- 子组件有个异步操作,需要等请求完了之后 才能完成 Setup 的调用 -->
<Child/>
</template>
<!-- 加载时显示 -->
<template #fallback>
加载中
</template>
</Suspense>
</template>
<script setup lang="ts">
import { Suspense } from 'vue';
import Child from './Child.vue';
defineOptions({name:"App"})
</script>
Commponent 全局组件
main.ts
入口文件中注册全局组件
import {createApp} from 'vue'
import App from './App.vue'
import Child from './Child.vue'
// 创建应用
const app = createApp(App)
// 注册全局组件
app.component("Child",Child)
app.mount('#app')
App.vue
中不用引入也能用 可以直接使用这个标签
<template>
<Child/>
</template>
<script setup lang="ts">
defineOptions({name:"App"})
</script>
笔记由 两双筷子 整理,来源:https://www.bilibili.com/video/BV1Za4y1r7KE/