Vue3 study
Vue3 工程
创建
-
还是能像 vue2 一样通过 vue-cli 创建,即
vue create projectName
-
但是官方更推荐 vite 创建,即
npm create vue@latest
,然后从项目名开始配置 -
总结:入口在 index.html,它会引入 main.ts,在 main.ts 中引入 App 组件并挂在到 index.html 中
Vue3 核心语法
CompositionAPI
- 可以用函数的形式将相关功能的代码组织在一起
setup
- 是 Vue3 中一个新配置项,值为函数
- 返回值可以为对象,将数据和方法返回出去
- 返回值也可以为一个函数,此时该函数返回的内容会直接渲染在页面上,并且无视 template 标签中的内容(很少使用)
细节
- setup 的生命周期是最先的,所以 data 中可以通过 this.xx 读取 setup 中的数据,即原来的 data,methods 写法中可以读取 setup 中的数据,但是 setup 中不能读取 data,methods 中的数据
- setup 中无法使用 this
语法糖
-
如果正常写 setup 比较麻烦,总是需要 return 数据或方法,例如
-
setup(){ let name:string = 'John' return {name} }
-
我们可以直接用一个带有 setup 属性的 script 标签来代替:
<script setup>...</script>
-
但是 script 标签语法默认为 js,由于 script 标签需要统一语法,所以需要再加一个值为 ts 的 lang 属性,即
<script lang="ts" setup>...</script>
-
如此一来我们的 vue 文件就总需要两个 script 标签,一个定义组件名,一个写 setup,我们可以安装插件 vite-plugin-vue-setup-extend,此时在 script 标签中用一个 name 属性就能定义组件名即
<script lang="ts" setup name="Person1">...</script>
-
但是用于组件名默认为文件名,我们一般也是使用文件名作为组件名,所以也可以不用特意定义组件名
ref
基本数据类型的响应式
import {ref} from 'vue'
后修改定义 name 为let name = ref('张三')
,此时打印会发现 name 已经为一个 refImpl 对象,他的 value 属性为张三,所以我们对 name 的使用理应改为 name.value,但是插值表达式中的使用不需要,帮我们自动解决了,而方法中还是需要的- ref 也能定义对象类型的响应式数据,但是实际上 value 是 Proxy,即底层还是 reactive
reactive
对象数据类型的响应式
import {reactive} from 'vue'
后可以定义比如let car = reactive({brand:'奔驰',price:100,})
,此时会被定义为一个 Proxy 对象,这是 js 中的原生代码 window 对象的一个方法,随后正常使用即可- reactive 支持多层嵌套的对象的响应式
- 但是 reactive 只能定义对象类型的响应式数据
ref 对比 reactive
宏观角度
- Ref 定义:基本类型数据,对象类型数据
- reactive 定义:对象类型数据
区别
- ref 创建的变量必须使用
.value
(可以使用 volar 插件自动添加.value
)- volar 插件如今为 Vue-official,开启 Auto Insert: Dot value 即可,会自动填充
.value
- volar 插件如今为 Vue-official,开启 Auto Insert: Dot value 即可,会自动填充
- reactive 重新分配一个新对象,会失去响应式(可以使用
Object.assign
去整体替换)- 比如修改 car 不能直接
car = {brand:'宝马', price:100}
,分配一个新的 reactive 更不行,这样就更改了整个对象的引用,可以Object.assign(car,{brand:'宝马', price:100})
- 比如修改 car 不能直接
使用原则
- 基本类型用 ref
- 层级不深的对象 用 ref 和 reactive 都可
- 层级较深的对象用 reactive
toRefs 与 toRef
-
如果我们定义一个响应式对象,然后用解构写法去定义几个变量,那么这些变量并不是响应式的,但是用 toRefs 可以将其变为响应式的
-
let person = reactive({name:'张三',age:18}) let {name,person} = person //非响应式 let {name,person} = toRefs(person) // 响应式,相当于定义了两个 ref 的变量,并且与 person 的属性关联了
-
-
toRef 可以单独将某个响应式对象的属性定义为一个变量比如:
let v1 = toRef(person, 'name')
,不过用的比较少
总结
- toRefs 和 toRef 都可以用来结构响应式对象,并且仍为响应式的
Computed 计算属性
-
当需要频繁地根据一些值计算出一个新的值时可以使用 computed 属性
-
例如根据 firstName 和 lastName 计算出 fullname:
let fullName = computed(()=>{return firstName + '-' + lastName})
-
但是以上的写法是只读的,首先 funllName 最终为一个 computedRefImpl,所以修改方式为
fullName.value = xx
,如果要可读可写的 computed 属性应该定义为-
let fullName computed({ get(){ return ... } set(newFullName){ ... } })
-
然后在 set 方法中根据 newFullName 修改 firstName 和 lastName 即可
-
watch
监视数据变化(和 vue2 中作用一致)
用法:watch(监视对象,回调函数,配置选项)
可监听数据
- ref 定义的数据
- reactive 定义的数据
- 函数返回一个值(getter 函数)
- 包含上述内容的数组
实际应用情况
1. 监视 ref 定义的基本数据类型
import {ref, watch} from 'vue'
let sum = ref(0);
function addSum(){
sum.value += 1
}
// 为什么这里不是 sum.value ?
// 因为只能监视 ref 定义的数据,.value 则为一个基本数据类型的数据了
watch(sum,(newSum,oldSum)=>{
console.log('sum changed!',newSum, oldSum);
})
// 如何结束监视
const stopWatch = watch(sum,(newSum,oldSum)=>{
if(newSum > 10){
stopWatch()
}
console.log('sum changed!',newSum, oldSum);
})
2. 监视 ref 定义的对象类型的数据
- 如果直接写对象名,监视的是对象的地址值,若想监视对象内部数据,要手动开启深度监视
细节:如果修改对象属性,你会发现 newVal 和 oldVal 的值相同,因为监视的对象并未改变,修改完属性后才进入 watch 函数读取的新旧对象是同一个;但是如果修改了整个对象,newVal 和 oldVal 的值就不相同了,修改对象后进入 watch 函数会发现旧的值为旧的对象,新的值为新的对象,所以开发时我们大多数在回调函数中写一个参数即可,它会对应新值,而且我们基本也只关心新值
import {ref, watch} from 'vue'
let person = ref({
name: 'john',
age: 18,
})
function changeName() {
person.value.name += '~'
}
function changeAge() {
person.value.age += 1
}
function changePerson() {
person.value = { name: 'jack', age: 10 }
}
// 没开启 deep=true 时只有 changePerson 才会执行该 watch 函数
// 开启后 changeName 和 changeAge 也会触发
watch(person, (newVal, oldVal) => {
console.log('value changed!', newVal, oldVal);
},{deep:true})
3. 监视 reactive 定义的对象类型数据,且自动开始深度监听并无法关闭(隐式开启深层监听)
import { reactive, watch } from 'vue'
let person = reactive({
name: 'john',
age: 18,
})
function changeName() {
person.name += '~'
}
function changeAge() {
person.age += 1
}
function changePerson() {
Object.assign(person,{ name: 'jack', age: 10 })
}
watch(person, (newVal, oldVal) => {
console.log('value changed!', newVal, oldVal);
}
4. 监视 ref 或 reacctive 定义的对象类型数据中的某个属性
- 若该属性不是对象类型,需写成函数形式
let person = reactive({
name:'john',
age:18,
car:{
c1:'c1',
c2:'c2',
}
})
watch(()=>person.name, (newVal,oldVal)=>{
console.log('name changed!', newVal,oldVal);
})
- 若该属性时对象类型,可直接编写,也可写成函数形式,建议写成函数形式
let person = reactive({
name:'john',
age:18,
car:{
c1:'c1',
c2:'c2',
}
})
function changeC1(){
person.car.c1 = 'cc1'
}
function changeC2(){
person.car.c2 = 'cc2'
}
function changeCar(){
person.car = {
c1:'ccc1',
c2:'ccc2',
}
}
// 由于监视的是 person.car 这个对象,所以改变它的属性时会进入 watch,但是直接更改整个 car 时相当于监视对象都没了,就会进入 watch 了
watch(person.car, (newVal,oldVal)=>{
console.log('name changed!', newVal,oldVal);
})
// 由于监视的是 person.car 这个对象的地址值,所以改变它的属性都不会进入 watch,只有更改整个 car 时因为地址改变了才会进入 watch
watch(()=>person.car, (newVal,oldVal)=>{
console.log('name changed!', newVal,oldVal);
})
// 这样就能监视属性和整个对象的改变
watch(()=>person.car, (newVal,oldVal)=>{
console.log('name changed!', newVal,oldVal);
},{deep:true})
5. 监视上述多个数据
watch([()=>person.name,()=>person.car], (newVal,oldVal)=>{
console.log('name changed!', newVal,oldVal);
},{deep:true})
watchEffect
立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行该函数
- 对比 watch 相当于能自动明确需要监视地属性
let sum = ref(0)
function changeSum(){
sum.value += 1
}
watchEffect(()=>{
if(sum.value > 5){
console.log('sum > 5');
}
})
标签的 ref 属性
ref 还能用于存储 ref 标记的内容,用于 html 标签时能获取该标签
但是用于组件时会获得该组件实例,并且只能读取子组件 defineExpose 出的内容
// 标记 html 标签
<template>
<h2 ref="test">test</h2>
</template>
let test = ref()
console.log(test.value) //会输出 <h2>test</h2>
// 标记组件
// 子组件内容:
let a = ref(10)
let b = ref(20)
defineExpose({a})
// 父组件内容
<子组件 ref="son"/>
<button @click="printSon">print</button>
let son = ref()
function printSon(){
console.log(son.value) // 能获取 a,不能获取 b
}
回顾 TS 中的接口、泛型、自定义类型
- 一般会在 src 下新建 types 目录,然后创建 index.ts 文件在其中编写类型
- 例如一个拥有 name 和 age 的接口 PersonInteFace,以及用自定义类型表示数组形式的该接口
export interface PersonInterFace {
name:string,
age:number,
}
export type Persons = Array<PersonInterFace>
// 使用示例
import {type PersonInterFace, type Persons} from '@/types'
let person:PersonInterFace = {name:'john',age:18}
let persons:Persons = [{name:"jack",age:19}]
props
-
首先我们可以用 defineProps 在子组件接受父组件的传值
-
//父组件: <Son x="123"/> //子组件: defineProps(['x']) //然后就可以在 template 中使用 //如果子组件想在 script 标签中使用 let something = defineProps(['x']) console.log(something.x)
-
但是这样的定义下,比如你预期接受父组件传入的数组,结果父组件传错成了一个数字,那么你的使用肯定会出问题,所以可以限制父组件传值类型
-
// 此时 x 只能传入 number 否则父组件传值处会报错 let something = defineProps<{x:number}>()
-
我们也可以在限制传入类型的同时限制是否必须传入以及设定默认值
-
// 例如在父组件定义了 PersonInterFace 的数组 // 如果用 reactive 定义,这里最好写成该形式而不是 let persons:Person = reactive(...) let persons = reactive<Persons>([ {name:"jack",age:19}, {name:'john',age:88}, ]) // 子组件: // 熟悉 ts 就知道 ? 表示是否 need import {defineProps,withDefaults} from 'vue' withDefaults(defineProps<{persons?:Persons}>(),{ // 默认值 persons: ()=>[{name:'mike', age:20}] })
-
其实
defineXXX
这种形式都是宏函数,所以不用显示引用也能使用,即 import 中可以不写
生命周期
回顾 Vue2 生命周期
-
主要有四个生命周期,以及对应的八个 [钩子](生命周期函数)
-
创建(beforeCreate,created),组件的创建前以及创建完毕
-
挂载(beforeMount,mounted),将组件挂载到页面显示出来前以及挂载后
-
更新(beforeUpdate,updated),组件中的数据更新前以及更新完毕
-
销毁(beforeDestroy,destroyed),组件的销毁前以及销毁后
Vue3 生命周期
-
同样也是四个生命周期,但是 create 不分创建前后了,合并成了 setup
-
挂载前和完毕则是 onBeforeMount 以及 onMounted,例如挂载前的调用方式为
onBeforeMount (()=>{挂载前的处理})
,挂载完毕同理 -
更新也是同理,为 onBeforeUpdate 以及 onUpdated,调用也是同理
-
Vue3 中的销毁和挂载对应了,不再为销毁而是卸载,所以为 onBeforeUnmount 以及 onUnmounted,调用方式还是同理
自定义 hooks
当我们把多个功能的数据和函数都写在 setup 中时,其实还是和 Vue2 一样混乱,所以可以使用自定义 hooks 实现模块化开发
-
命名规范为 useXXX,例如有一个关于狗的功能,在页面上有一个按钮,按一下按钮在页面上增加一张狗的图片,我们可以在 src 中新建 hooks 目录,在其中创建 useDog.ts,里面的代码就是我们用到的关于该功能的所有代码,然后 return 出去,让别人使用即可
-
import { reactive } from 'vue'import axios from 'axios'export default function(){ let dogs = reactive([ 'https://images.dog.ceo/breeds/pembroke/n02113023_4373.jpg', ]) async function addDog(){ try { let result = await axios.get('https://dog.ceo/api/breed/pembroke/images/random') dogs.push(result.data.message) } catch (error) { alert(error) } } return {dogs, addDog}}// 其他组件使用示例<img v-for="(d,index) in dogs" :src="d" alt="" :key="index"/><br><button @click="addDog">add dog</button><script lang="ts" setup name="Person"> import useDog from '@/hooks/useDog' const {dogs, addDog} = useDog();</script>
-
并且在自定义 hooks 中也能使用生命周期钩子和计算属性
基本使用
-
准备工作:
- 一个展示的页面(包含导航区,展示区)
- 路由器
- 制定路由器规则
- 路径对应的组件
-
安装路由器 npm i vue-router
-
src 下创建 router 目录,其中创建 index.ts
-
import { createRouter, createWebHistory } from "vue-router";
// 引入要呈现的组件
import Home from '@/pages/Home.vue'
import About from '@/pages/About.vue'
import News from '@/pages/News.vue'
// 创建路由器
const router = createRouter({
history: createWebHistory(), // 路由器的工作模式
routes:[ // 一个个路由规则
{
path:'/home',
component: Home,
},
{
path:'/news',
component: News,
},
{
path:'/about',
component: About,
},
],
})
// 暴露出去
export default router
-
创建并暴露完以后 vueApp 需要使用它,所以修改 main.ts
-
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(router)
app.mount('#app')
-
在 App.vue 中就可以具体的使用路由了,需要注意的是 to 还有另一种写法::to="{path:'/home'}"
-
<template>
<div class="app"></div>
<h2 class="title">vue3 路由</h2>
<!-- 导航区 -->
<div class="navigate">
<!--
使用 a 标签使用 vue-router 的路由切换组件,to:对应的配置的路径
active-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-content">
<!-- 根据路径自动对应路由中配置的组件 -->
<RouterView/>
</div>
</template>
<script lang="ts" setup name="App">
import { RouterView, RouterLink } from 'vue-router';
</script>
注意点
- 路由组件一般放在 pages 或 views 目录下,一般组件放在 components 目录下
- 点击导航后,视觉效果上消失的路由组件默认被卸载了,需要时再挂载
路由器工作模式
history 模式
- 优点:URL 更美观,不带有 #,更接近传统网站的 URL
- 缺点:后期项目上线,需要服务端配合处理路径问题,否则刷新会有 404 错误
const router = createRouter({
history: createWebHistory(), // history 模式
}
hash 模式
- 优点:兼容性更好,不需要服务器端处理路径
- 缺点:URL 带有 # 不太美观,且在 SEO 优化方面较差
const router = createRouter({
history: createWebHashHistory(),
}
命名路由
配置路由规则时可以为路由命名,相对应的跳转时也可以根据路由名字跳转
// router/index.ts
routes:[
{
name: 'main',
path:'/home',
component: Home,
},
}
// 使用
<RouterLink :to="{name:'main'}" active-class="active">首页</RouterLink>
嵌套路由
当一个子组件中同样存在类似导航区和展示区的内容时,可以使用嵌套路由,例如新闻区中根据不同的新闻标题展示不同的新闻
-
随意编写一个展示新闻内容的组件,例如 NewsDetail.vue
-
然后将其编写入路由配置中
-
{
path:'/news',
component: News,
children:[
{
path: 'detail', // 注意此处无斜杠了
component: NewsDetail,
}
]
},
-
配置后需要在新闻组件中使用 detail 组件
-
<template>
<div class="news">
<ul>
<li v-for="n in news" :key="n.id">
<RouterLink to="/news/detail">{{ n.title }}</RouterLink>
</li>
</ul>
<div class="news-content">
<RouterView/>
</div>
</div>
</template>
-
此时 /news/detail
的路径下就会呈现 detail 组件了,但是我们还没能让 detail 显示不同的新闻内容
-
那我们就需要设法在跳转路由时传参
路由 query 参数
query 是直接写在 url 中传递的参数,
-
例如 news/detail?a=123&b=test
,问号后的 a 与 b 就传递了出去
-
detail 接收方式:
-
// 内容都在 route.query 中,为一个对象
const route = useRoute()
console.log(route.query.a)
-
由于路径是写在 RouterLink 的 to 属性中,所以也可以用对象形式
-
<RouterLink :to="{
path: '/news/detail',
query: {
a: 123,
}
}">test</RouterLink>
路由 params 参数
-
params 需要在 route 配置中以在路径中占位的形式配置
-
{
path:'/news',
component: News,
children:[
{
path: 'detail/:id',
component: NewsDetail,
}
]
},
-
此时路径中就能直接写参数
-
<RouterLink :to="`/news/detail/${123}`">test</RouterLink>
-
对应组件中会根据配置的规则认为 123 是一个名为 id 的参数
-
import { useRoute } from "vue-router";
const route = useRoute()
console.log( route.params.id)
注意
- 当使用对象形式在 to 属性中传参时,就不能使用 path 属性了,因为你使用了命名的路由,所以此时的路径不是简单的
/news/detail
了,所以需要使用 name 属性来对应路由了 - 还需要注意的是使用 params 传参时不能传递对象和数组,而且如果想要配置某个参数的必要性可以加问号,例如
path: 'detail/:id?',
,否则不传 id 这个参数就会报错
路由 props 配置
第一种
-
直接在路由配置中开启 props
-
{
path:'/news',
component: News,
children:[
{
name: 'newsDetail',
path: 'detail/:id',
component: NewsDetail,
props: true,
}
]
},
-
此时相当于将路由收到的所有 params 参数作为 props 传给路由组件
-
所以路由组件使用 defineProps 接收即可 defineProps(['id','title','content'])
第二种
-
将路由中 props 定义为函数,自己决定将什么作为 props 传给路由组件
-
例如当你使用 query 传参时
-
// props 函数有默认传参 route 可以使用
props(route){
return route.query
},
第三种
- props 也可以为对象类型,不过此时的传参就写死了
路由 replace 属性
-
控制路由跳转时浏览器历史记录有 push 模式和 replace 模式,一般默认为 push 模式。
-
当使用 push 时,从一个页面到另一个页面后能后退回前一个页面;而如果是 replace 模式则无法进行页面回退
-
设置某个页面为 replace 的方式为在 RouterLink 中添加 replace 属性
-
<RouterLink replace to="/news" active-class="active">新闻</RouterLink>
编程式路由导航
-
以上代码实现路由导航都是使用了 RouterLink 组件,它最终使用 a 标签实现的。但是如果需要手动切换路由而不是通过点击某处来实现,那就只能用编程式路由导航。例如在某个页面挂载三秒后跳转到另一个页面
-
// 这里是获取路由器而不是某个路由组件的路由了
const router = useRouter()
onMounted(()=>{
setTimeout(()=>{
// 使用 push 模式跳转
router.push('/news')
},3000)
})
-
router 的使用其实和 RouterLink 的 to 属性一致,例如 push 时不通过字符串形式而是对象形式
-
const router = useRouter()
// 同理也可以 router.replace()
router.push({
name:'xx',
params: {
...
}
})
路由重定向
-
我们可以让一个执行路径重新定位到另一个路径,例如首页我们默认要打开 home 页面
-
{
path: '/',
redirect: '/home',
}
pinia
当某些数据需要多个组件共享时,我们就需要集中式状态管理库,pinia 就是这样的一种库
搭建环境
-
npm i pinia
-
下一步肯定就是在 app 中使用它,直接编写 main.ts
-
import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.mount('#app')
存取数据
-
一般会在 src 下创建 store 目录,然后比如有一个组件名为 Count.vue,那么我们就会创建一个 count.ts 来使用 pinia
-
//count.ts
import { defineStore } from "pinia";
// 创建了一个名为 count 的仓库,里面存放了一个名为 sum 的变量
// 注意命名规则为 useXXXStore
export const useCountStore = defineStore('count', {
state(){ // state 需要写成函数形式
return {
sum: 6
}
}
})
-
使用示例
-
import {useCountStore} from '@/store/count'
const countStore = useCountStore()
// 以下两种都能获取 sum
console.log(countStore.sum)
console.log(countStore.$state.sum)
-
小细节:sum 是存储在类型为 Proxy 对象的 countStore 下的一个 ref 类型的数据,按道理我们读取时应该是 countStore.sum.value
,但是这里不需要,因为 reactive 定义的数据会帮我们自动拆包
-
let a = ref(0)
console.log(a.value)
let b = reactive({
a:ref(0)
})
console.log(b.a)
修改数据
第一种
- 直接修改
countStore.sum += 1
第二种
-
当大量数据需要同时修改时可以使用 $patch,对于组件事件(component events)来说只需要进行一次 patch 就能修改完成,而第一种则是修改几个数据就发生几次修改数据(mutation)的事件
![请添加图片描述](https://i-blog.csdnimg.cn/direct/08e19096ea8847728dfd078745e883d0.png)
-
countStore.$patch({
sum: countStore.sum + 1,
...
})
第三种
-
学过 vue2 中 vuex 会最先想到的 actions,它存放着一个个方法用来相应组件中的动作
-
先在 countStore 中定义
-
export const useCountStore = defineStore('count', {
actions:{
// this 就是这个 store
increment(val){
this.sum += val
}
},
state(){
return {
sum:6,
}
}
})
-
组件中使用时例如 countStore.increment(123)
storeToRefs
当使用 store 中的多个数据时,我们都写成 store.xx 就显得不够优雅,那么我们就会想到解构
-
注意直接解构会丢失数据的响应式:let {sum} = countStore
-
所以需要解构成响应式的例如 let {sum} = toRefs(countStore)
-
但是这样做的代价很大,它会把 store 中的一切转成 ref 引用,包括数据和方法
-
所以我们需要使用 storeToRefs:let {sum} = storeToRefs(countStore)
,他只会把数据转成响应式的
getters
当 state 中的数据需要经过处理再使用时可以使用 getters,类似于组件的计算属性
-
例如将 sum 放大十倍
-
// store/count.ts
export const useCountStore = defineStore('count', {
actions:{
...
},
state(){
sum: 1,
},
getters: {
// this 就是 store,所以可以 this.sum
bigSum(){
return this.sum * 10
},
// 也可以接收传参 state,所以可以用 state.sum
bigSum2(state){
return state.sum * 100
},
}
})
$subscribe
-
$subscribe能够监听 state 中数据的变化
-
使用示例
-
// 某个组件
countStore.$subscribe((mutation,state)=>{
console.log('@@32',mutation,state);
})
-
回调函数中我们一般不关心第一个传参,state 则是更新后的 state
-
我们可以以此实现例如防止数据的功能,每次 state 更新就将其存入 localStorage,那么 state 中存放的就是 从 localStorage 中取出来的数据,这样无论页面刷新还是关闭页面再打开都不会丢失数据
-
// 某个组件
countStore.$subscribe((mutation,state)=>{
localStorage.setItem('sum',state.sum)
})
// store/count.ts
export const useCountStore = defineStore('count', {
state(){
return {
// 不用 JSON.parse 取出来就是 string
// 还有最开始 sum 为 null 的话就让他默认为 1
sum: JSON.parse(localStorage.getItem('sum')) || 1,
}
},
})
组合式写法
-
以上定义的 store 其实还选项式的,是以对象的形式定义的,我们可以改为函数形式的组合式写法
-
// store/count.ts
export const useCountStore = defineStore('count', ()=>{
let sum = ref(JSON.parse(localStorage.getItem('sum')) || 1)
function increment(val){
sum.value += val
}
function bigSum(){
return sum.value * 10
}
function bigSum2(){
return sum.value * 100
}
// 别忘了 return,否则无法使用
return {sum,increment,bigSum,bigSum2}
})
组件通信
props
- 一般用于父子组件互传
- 父传子:直接传一个非函数
- 子传父:父先传给子一个函数,子在某个时刻调用函数把东西以参数形式传给父亲
//father.vue
<template>
<div class="father">
<h2>father</h2>
<h3 v-show="toy" >子传给父的:{{ toy }}</h3>
<Son :car="car" :sendToy="getToy"/>
</div>
</template>
<script setup lang="ts" name="">
import Son from "@/pages/props/Son.vue";
import { ref } from "vue";
let car = ref("爸爸的车");
let toy = ref("")
// 获取儿子的玩具
function getToy(val){
toy.value = val
}
</script>
//son.vue
<template>
<div class="son">
<h2>son</h2>
<h3>父传给子的:{{ car }}</h3>
<button @click="sendToy(toy)">传给父亲玩具</button>
</div>
</template>
<script setup lang="ts" name="Son">
import { ref } from "vue";
let toy = ref("奥特曼");
defineProps(["car","sendToy"]);
</script>
自定义事件
-
我们都知道 <button @click="handleClick"></button>
中 @click 的含义是对这个按钮进行点击事件时触发 handleClick 函数的调用,但是我们也可以自定义事件
-
自定义事件一般用来子传父例如 <h2 @print-ok="printOk">
,我们就定义了一个名为 print-ok 的自定义事件,当该事件触发后会进行 pringOk 函数的回调
-
触发自定义事件的方式就是使用 defineEmits
-
// 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 { ref } from "vue";
import Child from "./Child.vue";
let toy = ref("");
function saveToy(val){
toy.value = val
}
</script>
// child.vue
<template>
<div class="child">
<h3>子组件</h3>
<h4>玩具:{{ toy }}</h4>
<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>
注意
-
注意自定义事件的命名形式为 kebab-case,即短横线连接的命名法,例如 send-toy
-
我们一般定义 emit 来接收自定义事件,然后调用时就是 emit('自定义事件名', 传参)
mitt
-
能实现任意组件间的通信
-
安装:npm i mitt
-
在 src 的 utils 目录下创建 emitter.ts
-
// 引入mitt
import mitt from 'mitt'
// 调用mitt得到emitter,emitter能:绑定事件、触发事件
const emitter = mitt()
// 暴露emitter
export default emitter
-
他的使用也很简单,有获取全部事件,绑定事件,解绑事件,触发事件
-
// 绑定事件
emitter.on('test1',()=>{
console.log('test1被调用了')
})
emitter.on('test2',()=>{
console.log('test2被调用了')
})
// 每秒触发一次事件 test1 和 test2
setInterval(() => {
emitter.emit('test1')
emitter.emit('test2')
}, 1000);
// 三秒后执行
setTimeout(() => {
// 解绑某个事件
emitter.off('test1')
emitter.off('test2')
// 解绑全部事件
emitter.all.clear()
}, 3000);
-
一般流程:接收数据方会绑定事件,提供数据方触发事件,提供方触发事件时把数据传过去,接收方对传过来的数据进行操作
-
// 接收方
<template>
<div class="child2">
<h3>子组件2</h3>
<h4 v-show="toy">哥哥给的玩具:{{ toy }}</h4>
</div>
</template>
<script setup lang="ts" name="Child2">
import { ref } from "vue";
import emitter from '@/utils/emitter';
let toy = ref("");
// 绑定事件 send-toy,设置回调函数,如果有玩具传过来就赋值给自己定义的 toy
emitter.on('send-toy',(val:string)=>{
toy.value = val
})
</script>
// 传递方
<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>
-
以上代码实际上就是关于 mitt 的事件的绑定与触发的使用示例
注意
-
绑定事件的组件销毁时最好解绑事件
-
// 绑定方
onUnmounted(()=>{
emitter.off('send-toy')
})
v-model
一般开发中不会使用 v-model 进行组件通信,但是 UI 库中底层源码会经常使用,所以理解这个对于阅读 UI 库组件源码有帮助
- 能用于父子组件互传
用在 html 标签
-
先说一下当 v-model 用在 html 标签上实现双向绑定的原理
-
例如 <input type="text" v-model="username">
,我们定义的变量 username 的值会显示在页面中,并且改变输入框的内容也会改变这个变量的值
-
底层实现:
- 变量的值 -> 页面:底层实现就是通过 value 属性
- 页面输入 -> 变量的值:通过监听 input 事件更改变量的值
-
即 <input type="text" :value="username" @input="username = $event.target.value">
-
但是由于我们可以手动 new 一个 event,所以 ts 语法检查会报错,认为可能不存在 $event.target.value,所以我们需要断言
-
@input="username = ($event.target as HTMLInputElement).value"
或者 @input="username = (<HTMLInputElement>$event.target).value"
用在组件
-
例如自定义一个 input 组件 MyInput
-
我们直接使用 <MyInput v-model="username"/>
肯定不是双向绑定的,所以我们需要编写 MyInput 代码
-
// Father.vue
<template>
<div class="father">
<h3>父组件</h3>
<h2>username: {{ username }}</h2>
<MyInput :modelValue="username" @update:modelValue="username = $event"/>
</div>
</template>
<script setup lang="ts" name="Father">
import { ref } from "vue"
import MyInput from './MyInput.vue'
let username = ref("john")
</script>
// MyInput.vue
<template>
<input type="text"
:value="modelValue"
@input="emit('update:modelValue', ($event.target as HTMLInputElement).value)"
>
</template>
<script setup lang="ts" name="MyInput">
defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>
-
实现原理
- 父传子:直接传一个值
- 子传父:传一个自定义事件
-
我们自然可以自己命名传的值和自定义事件,但是通过以上这样的命名形式,我们可以直接将父组件中的 <MyInput :modelValue="username" @update:modelValue="username = $event"/>
改为 <MyInput v-model="username"/>
-
也就是说我们在使用 UI 库中的 input 时,之所以能直接 v-model,就是因为组件底层是这样编写的
细节
- 对于原生事件:$event 是事件对象,所以可以 $event.target
- 对于自定义事件:$event 是触发事件时传递的参数,所以不能 $event.target
v-model 升级写法
- 如果觉得不喜欢 modelValue,我们也可以自定义这个名字,例如
<MyInput v-model:myValue="username"/>
- 以上写法就相当于我们把 modelValue 改名成了 myValue,那么 MyInput 就得修改成
// MyInput.vue
<template>
<input type="text"
:value="myValue"
@input="emit('update:myValue', ($event.target as HTMLInputElement).value)"
>
</template>
<script setup lang="ts" name="MyInput">
defineProps(['myValue'])
const emit = defineEmits(['update:myValue'])
</script>
-
可以自定义命名,也就意味着命名不唯一,那么我们可以在使用自定义组件时写多个 v-model:自定义名="xx"
-
例如 <MyInput v-model:myValue="username" v-model:myValue2="password"/>
-
那我们在 MyInput 中如果处理 myValue2,也就意味着可以再写一个 input,也就是说使用一次 MyInput 组件可以显示两个 input
$attrs
-
专门适用于 祖 <-> 孙
-
首先需要知道一点,当父组件传东西给子组件时,子组件如果不通过 defineProps 接收,这些数据将会被存储在子组件的 $attrs 中
-
// Father.vue
<template>
<div class="father">
<h3>父组件</h3>
<Child :a="1"/>
</div>
</template>
<script setup lang="ts" name="Father">
import Child from './Child.vue'
</script>
//Child.vue
<template>
<div class="child">
<h3>子组件</h3>
<h3>未接收的{{ $attrs }}</h3>
</div>
</template>
-
最终效果:
![请添加图片描述](https://i-blog.csdnimg.cn/direct/6f3ab6243c14420f93deaba6b5a49a79.png)
-
那么子组件只要选择不接收父组件的数据,把 $attrs 都给孙组件,也就实现了祖传值给孙,、
-
其实孙传祖我们也能想到,只需要祖传给孙一个函数,孙调用时把要给祖的传过去就可以了
-
祖传孙时一次性把 $attrs 中全部内容传给孙组件可以通过 v-bind
<A :a="1">
<==> <A v-bind="{a:1}">
-
祖孙互传示例:
-
// 父
<template>
<div class="father">
<h3>父组件</h3>
<h4>a: {{ a }}</h4>
<Child :a="a" v-bind="{ b: 2 }" :updateA="changeA" />
</div>
</template>
<script setup lang="ts" name="Father">
import Child from "./Child.vue"
import { ref } from "vue"
let a = ref(1)
function changeA(val:number) {
a.value += val
}
</script>
// 子
<template>
<div class="child">
<h3>子组件</h3>
<GrandChild v-bind="$attrs"/>
</div>
</template>
<script setup lang="ts" name="Child">
import GrandChild from './GrandChild.vue'
</script>
//孙
<template>
<div class="grand-child">
<h3>孙组件</h3>
<h4>a:{{ a }}</h4>
<h4>b:{{ b }}</h4>
<button @click="updateA(6)">点我将爷爷的a更新</button>
</div>
</template>
<script setup lang="ts" name="GrandChild">
defineProps(['a','b','updateA'])
</script>
$refs
-
用于父传子
-
存放了所有被 ref 标记的 DOM 元素或组件实例
-
最开始定义响应式数据时提到过 ref 不仅可以定义基本类型的数据,同时可以标记组件,然后获取该组件实例,但是出于数据保护的考虑,无法获取被标记的组件的数据,如果被标记组件愿意暴露数据可以使用 defineExpose
-
// Child1.vue
<template>
<div class="child1">
<h3>子组件1</h3>
<h4>玩具:{{ toy }}</h4>
</div>
</template>
<script setup lang="ts" name="Child1">
import { ref } from "vue";
let toy = ref('奥特曼')
defineExpose({toy})
</script>
// Father.vue
<template>
<div class="father">
<h3>父组件</h3>
<Child1 ref="c1"/>
<button @click="changeC1Toy">修改c1玩具</button>
</div>
</template>
<script setup lang="ts" name="Father">
import Child1 from './Child1.vue'
import { ref } from "vue";
let c1 = ref()
function changeC1Toy(){
c1.value.toy = '小猪佩奇'
}
</script>
-
这样就实现了父传子,同理定义一个 Child2 也能以同样的方式传值给 Child2
-
但是如果需要一次性修改多个子组件,就需要使用 $refs
-
// 两个子组件都定义一个 toy 并通过 defineExpose 暴露出去就不写了
// Father.vue
// $refs 结构为
// {
// c1:child1组件实例,
// c2:child2组件实例,
// }
<template>
<div class="father">
<h3>父组件</h3>
<Child1 ref="c1"/>
<Child2 ref="c2"/>
<button @click="changeAllToy($refs)">修改所有子组件的玩具</button>
</div>
</template>
<script setup lang="ts" name="Father">
import Child1 from './Child1.vue'
import Child2 from './Child2.vue'
import { ref } from "vue";
let c1 = ref()
let c2 = ref()
function changeAllToy(refs:{[key:string]:any}){
for(let key in refs){
refs[key].toy = '小猪佩奇'
}
}
</script>
$parent
-
用于子传父
-
存放了当前组件的父组件实例对象
-
子组件可以通过 $parent 给父组件传值,并且使用方式也符合直觉
-
//父组件定义一个数并暴露出去,否则子组件读取不到
// Father.vue
let house = ref(4)
defineExpose({house})
//Child1.vue
<button @click="minusHouse($parent)">减少父亲一套房</button>
function minusHouse(parent:any){
parent.house -= 1
}
provide-inject
-
使用规则为提供数据的父辈组件(爷组件,父组件都可以)使用 provide 提供数据,接收数据放使用 inject 获取数据
-
// Father.vue
<template>
<div class="father">
<h3>父组件</h3>
<h4>我拥有{{ money }}万</h4>
<h4>我拥有价值{{ car.price }}万的{{ car.brand }}</h4>
<Child/>
</div>
</template>
<script setup lang="ts" name="Father">
import Child from './Child.vue'
import {reactive, ref, provide} from 'vue';
let money = ref(100)
let car = reactive({
brand: '宝马',
price: 100,
})
function updateMoney(val:number){
money.value += val
}
provide('car',car)
// 不能写 money.value,否则相当于只传递了一个值,会失去响应式
provide('moneyContext',{money,updateMoney})
</script>
// GrandChild.vue
<template>
<div class="grand-child">
<h3>孙组件</h3>
<h4>我爷爷拥有{{ money }}万</h4>
<h4>我爷爷拥有价值{{ car.price }}万的{{ car.brand }}</h4>
<button @click="updateMoney(-5)">花爷爷5万</button>
</div>
</template>
<script setup lang="ts" name="GrandChild">
import { inject } from 'vue';
// inject 的第二个参数,可以设置默认值
// 而且因为 ts 没法推断这里的 car 能 .price, .brand,这样的默认值设定还顺便通过 ts 的类型推断
let car = inject('car', {brand:'',price:0})
let {money, updateMoney} = inject('moneyContext', {money:0,updateMoney:(x:number)=>{}})
</script>
pinia
- 参考之前的 pinia 的部分,定义 useXXStore 存储公共数据,然后需要使用的地方拿到 store 直接用即可
slot
当需要显示多种结构布局类似,只有内容稍微不同的页面时,为了提高代码复用率,我们肯定不可能一个页面写一个 .vue 文件,所以我们可以使用插槽
默认插槽
-
含有插槽组件的使用 slot 标签进行占位,slot 标签中可以编写默认内容;使用该组件时在组件标签中编写的内容就会替代 slot 标签,例如下面代码中 <Game>...</Game>
Game 标签中的内容会替换 Game 组件中的 slot,否则就显示默认内容
-
// 插槽组件 Game.vue
<template>
<div class="game">
<h2>{{ title }}</h2>
<slot>我是默认内容</slot>
</div>
</template>
<script setup lang="ts" name="Game">
defineProps(['title'])
</script>
// Father.vue
<template>
<div class="father">
<h3>父组件</h3>
<div class="content">
<Game title="热门游戏列表">
<ul>
<li v-for="g in games" :key="g.id">
{{ g.name }}
</li>
</ul>
</Game>
<Game title="今日美食城市">
<img :src="imgUrl" />
</Game>
</div>
</div>
</template>
<script setup lang="ts" name="Father">
import Game from "./Game.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')
</script>
-
![请添加图片描述](https://i-blog.csdnimg.cn/direct/76a234658fc34f8b845091f275c197d3.png)
具名插槽
当一个组件含有多个插槽时,使用该组件时你需要手段向指定的插槽填充内容,这就需要具名插槽,也就是有名字插槽
-
我们可以把上述 Game 组件中的 title 部分也设置成一个插槽,并分别为插槽取名,Father 组件使用它时也根据 v-slot 设置要填充的插槽。
- v-slot 能被设置在组件和 template 上,我们一般都设置在 template 标签中
- 其实默认插槽也有名字,叫 default,不过我们一般不用
-
// Game.vue
<template>
<div class="game">
<slot name="title">默认标题</slot>
<slot name="content">默认内容</slot>
</div>
</template>
// Father.vue
<template>
<div class="father">
<h3>父组件</h3>
<div class="content">
<Game title="">
<template v-slot:content>
<ul>
<li v-for="g in games" :key="g.id">
{{ g.name }}
</li>
</ul>
</template>
<template v-slot:title>
<h2>热门游戏列表</h2>
</template>
</Game>
<Game>
<template v-slot:title>
<h2>今日美食城市</h2>
</template>
<template v-slot:content>
<img :src="imgUrl" />
</template>
</Game>
</div>
</div>
</template>
<script setup lang="ts" name="Father">
import Game from "./Game.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");
</script>
-
可以注意到上面的 h2 热门游戏列表虽然写在内容 ul 下面,但是因为具名插槽,会被填充到对应插槽的位置
作用域插槽
-
当数据在子组件,但是根据数据生成的结构,却由父亲决定,就需要使用作用域插槽
-
// 子组件
<template>
<div class="game">
<h2>游戏列表</h2>
<slot name="content" :games="games"></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>
// 父组件
<template>
<div class="father">
<h3>父组件</h3>
<div class="content">
<Game>
<template v-slot:content="{games}">
<ul>
<li v-for="g in games" :key="g.id">
{{ g.name }}
</li>
</ul>
</template>
</Game>
</div>
</div>
</template>
-
因为子组件可能传递多个数据,所以父组件接收到的是对象类型的数据
-
如果不使用解构方式接收子组件的数据,一般会命名为 params,即 <template v-slot="params">
-
v-slot 还有语法糖,例如以上代码可以改为 <template #content="{games}">
其他主要 API
shallowRef 和 shallowReactive
类似于 ref,但是只处理第一层的响应式
总结:通过使用 shallowRef() 和 shallowReactive() 来绕开深度响应。浅层式 API 创建的状态只在其顶层是响应式的,对所有深层的对象不会做任何处理,避免了对每一个内部属性做响应式所带来的性能成本,使得属性的访问变得更快,可提升性能
-
// 以下两个 function 都能正常使用
let sum = ref(1);
let sum2 = shallowRef(10)
function addSum() {
sum.value += 1;
}
function addSum2() {
sum2.value += 10;
}
// 但是以下 function 中涉及到修改第二层值的 changePrice2 就无法正常使用了
// 也可以简单理解为 shallowRef 只能修改到 xx.value,无法继续往后 value.xx
let car = ref({
price: 10,
});
let car2 = ref({
price: 100
})
function changePrice() {
car.value.price += 1;
}
function changeCar() {
car.value = { price: 20 };
}
function changePrice2() {
car2.value.price += 10;
}
function changeCar2() {
car2.value = { price: 200 };
}
// 使用 shallowReactive 也同理,以下修改 price 能修改
// 但是修改 engine 时使用 shallowReactive 定义的 car2 就无法修改
let car = reactive({
price: 10,
options: {
engine: "v8",
},
});
let car2 = shallowReactive({
price: 100,
options: {
engine: "v8",
},
});
function changePrice() {
car.price += 1;
}
function changeEngine() {
car.options.engine = 'v9'
}
function changePrice2() {
car2.price += 10;
}
function changeEngine2() {
car2.options.engine = 'v9'
}
readonly 和 shallowReadonly
主要用于数据保护,防止数据被拿去展示时被修改
-
readonly 能创建一个响应式数据的深只读副本,例如以下代码中只有关于 car 的 function 能修改数据,并且由于 car2 是 car 的副本,car 的修改结果会同步给 car2,但是有关 car2 的 function 都无法起作用
-
let car = reactive({
price: 10,
options: {
engine: "v8",
},
});
let car2 = readonly(car);
function changePrice() {
car.price += 1;
}
function changeEngine() {
car.options.engine = 'v9'
}
function changePrice2() {
car2.price += 10;
}
function changeEngine2() {
car2.options.engine = 'v9'
}
-
shallowReadonly 顾名思义就是浅层的 readonly,唯一不同的是只可以修改深层次的数据,例如以下代码中关于 car2 的function 中,只有修改深层次数据的 changeEngine2 能起作用
-
let car = reactive({
price: 10,
options: {
engine: "v8",
},
});
let car2 = shallowReadonly(car);
function changePrice() {
car.price += 1;
}
function changeEngine() {
car.options.engine = 'v9'
}
function changePrice2() {
car2.price += 10;
}
function changeEngine2() {
car2.options.engine = 'v9'
}
toRaw 和 markRaw
toRaw
- 作用:用于获取一个响应式对象的原始对象,即失去响应式的对象
- 使用场景:在需要将响应式对象传给非 Vue 的库或外部系统时,使用 toRaw 可以确保他们收到的是普通对象
markRaw
-
作用:标记一个对象,使其永远不会变成响应式
-
使用场景:例如使用第三方库时,可以防止错误地把第三方库暴露的对象定义成响应式的
-
// person2 就是响应式的
let person = {name: 'john'}
let person2 = reactive(person)
-------------------------------------------
// person2 不是响应式的
let person = markRaw{name: 'john'}
let person2 = reactive(person)
customRef
使用 ref 时,由于数据是响应式的,所以当你改变数据时,显示它的地方会立刻跟随着改变,但有些时候我们会有一些自定义的需求,比如更改数据后,显示的地方经过一秒后再同步更新,这时 ref 是无法满足需求的,就需要自定义的 ref 即 customRef
// 例如定义一个响应式的数字 1,基本结构如下
let a = customRef(() => {
return {
get(){
},
set(){
},
};
});
// get 函数很容易想到是返回 1,set 函数按道理也会有一个传参,是修改后的新值 val,
// 但是不知道把 val 给谁,所以我们需要一个变量来辅助我们
let initVal = 1
let a = customRef(() => {
return {
get(){
return initVal
},
set(val){
initVal = val
},
};
});
// 但是光这样还是不行的,我们的修改不会被触发
// 同时就算修改成功了,最新的值也不会被追踪观察,即不会在页面上同步更新
// 所以我们需要两个函数通知 vue 追踪 get 函数中 a 的数据变化,同时触发 set 函数中 a 的数据更新
let initVal = 1
let a = customRef((track, trigger) => {
return {
get(){
track() // 告诉 vue 数据 a 很重要,需要持续关注,一旦 a 变化就去更新
return initVal
},
set(val){
initVal = val
trigger() // 通知 vue 触发 a 的数据更新
},
};
});
// 这时加上需求例子中的一秒后同步更新页面的功能可以使用 setTimeout 包裹 set 函数部分
set(val) {
setTimeout(() => {
initVal = val;
trigger();
}, 1000);
},
// 但是当输入过快时,由于瞬间开启多个定时器,所以会出现问题,所以可以定义 timer
let initVal = 1;
let timer: number;
let a = customRef((track, trigger) => {
return {
get() {
track();
return initVal;
},
set(val) {
clearTimeout(timer);
timer = setTimeout(() => {
initVal = val;
trigger();
}, 1000);
},
};
});
// 当我们需要多次使用这种类似的 customRef 时可以将其定义为 hook,可以传入初始值和延迟时长
// useA.ts
import { customRef } from "vue";
export default function (initVal: number, delay: number) {
let timer: number;
let a = customRef((track, trigger) => {
return {
get() {
track();
return initVal;
},
set(val) {
clearTimeout(timer);
timer = setTimeout(() => {
initVal = val;
trigger();
}, delay);
},
};
});
return { a };
}
// 使用时
let { a } = useA(1, 2000);
Teleport
类似该单词的意思,能够实现远距离传送的功能
- 使用场景示例:例如手动写一个 Modal,我们一般会使用 position: fixed,设置定位为相对于整个窗口,但是当某个图片使用滤镜 filter 调整饱和度 saturate 时,可能会影响 Modal 窗口的定位,这时我们就可以使用 Teleport 标签,它可以更改包裹内容的位置例如 :
<Teleport to="body">...</Teleport>
,此时被包裹的内容会直接处于 body 标签内第一层,就一定会相对于整个窗口定位
Suspense
作用:等待异步组件时渲染一些额外内容,让应用有更好的用户体验
-
例如子组件 setup 中数据定义部分有直接的网络请求,此时子组件会无法显示,想要正常使用,父组件就需要用 Suspense 标签包裹子组件
- Suspense 标签中有两个插槽,一个显示异步组件,一个显示网络请求时暂时显示的内容
-
<Suspense>
<template v-slot:default>
<异步组件 />
</template>
<template v-slot:fallback>
<h2>加载中...</h2>
</template>
</Suspense>
全局 API 转移到应用对象
其实就是 vue2 中原本 vue.xx 的代码转移到了 app.xx
app.component
定义全局组件
- 例如 main.ts 中编写
app.component("Modal", Modal);
会定义一个名为 Modal 的对应 Modal 组件的全局组件
app.config
定义 vue 全局配置
-
例如编写一个全局变量 x 可以 app.config.globalProperties.x = 99
-
但是直接使用时虽然可以显示,但编译器会报错,根据官方文档可以在 main.ts 中定义
-
declare module "vue" {
interface ComponentCustomProperties {
x: number
}
}
app.directive
自定义指令
-
例如定义一个可以美化内容的指令 v-beauty
-
app.directive("beauty", (element, { value }) => {
element.innerText += value;
element.style.backgroundColor = 'yellow'
});
-
使用时 <h3 v-beauty="123">test</h3>
app.mount
挂载 app
app.unmount
卸载 app
app.use
利用插件
非兼容性改变
- v3 中有很多与 v2 不兼容的内容 非兼容性改变
准备工作:
- 一个展示的页面(包含导航区,展示区)
- 路由器
- 制定路由器规则
- 路径对应的组件
安装路由器 npm i vue-router
src 下创建 router 目录,其中创建 index.ts
import { createRouter, createWebHistory } from "vue-router";
// 引入要呈现的组件
import Home from '@/pages/Home.vue'
import About from '@/pages/About.vue'
import News from '@/pages/News.vue'
// 创建路由器
const router = createRouter({
history: createWebHistory(), // 路由器的工作模式
routes:[ // 一个个路由规则
{
path:'/home',
component: Home,
},
{
path:'/news',
component: News,
},
{
path:'/about',
component: About,
},
],
})
// 暴露出去
export default router
创建并暴露完以后 vueApp 需要使用它,所以修改 main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(router)
app.mount('#app')
在 App.vue 中就可以具体的使用路由了,需要注意的是 to 还有另一种写法::to="{path:'/home'}"
<template>
<div class="app"></div>
<h2 class="title">vue3 路由</h2>
<!-- 导航区 -->
<div class="navigate">
<!--
使用 a 标签使用 vue-router 的路由切换组件,to:对应的配置的路径
active-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-content">
<!-- 根据路径自动对应路由中配置的组件 -->
<RouterView/>
</div>
</template>
<script lang="ts" setup name="App">
import { RouterView, RouterLink } from 'vue-router';
</script>
const router = createRouter({
history: createWebHistory(), // history 模式
}
const router = createRouter({
history: createWebHashHistory(),
}
配置路由规则时可以为路由命名,相对应的跳转时也可以根据路由名字跳转
// router/index.ts
routes:[
{
name: 'main',
path:'/home',
component: Home,
},
}
// 使用
<RouterLink :to="{name:'main'}" active-class="active">首页</RouterLink>
当一个子组件中同样存在类似导航区和展示区的内容时,可以使用嵌套路由,例如新闻区中根据不同的新闻标题展示不同的新闻
随意编写一个展示新闻内容的组件,例如 NewsDetail.vue
然后将其编写入路由配置中
{
path:'/news',
component: News,
children:[
{
path: 'detail', // 注意此处无斜杠了
component: NewsDetail,
}
]
},
配置后需要在新闻组件中使用 detail 组件
<template>
<div class="news">
<ul>
<li v-for="n in news" :key="n.id">
<RouterLink to="/news/detail">{{ n.title }}</RouterLink>
</li>
</ul>
<div class="news-content">
<RouterView/>
</div>
</div>
</template>
此时 /news/detail
的路径下就会呈现 detail 组件了,但是我们还没能让 detail 显示不同的新闻内容
那我们就需要设法在跳转路由时传参
query 是直接写在 url 中传递的参数,
例如 news/detail?a=123&b=test
,问号后的 a 与 b 就传递了出去
detail 接收方式:
// 内容都在 route.query 中,为一个对象
const route = useRoute()
console.log(route.query.a)
由于路径是写在 RouterLink 的 to 属性中,所以也可以用对象形式
<RouterLink :to="{
path: '/news/detail',
query: {
a: 123,
}
}">test</RouterLink>
params 需要在 route 配置中以在路径中占位的形式配置
{
path:'/news',
component: News,
children:[
{
path: 'detail/:id',
component: NewsDetail,
}
]
},
此时路径中就能直接写参数
<RouterLink :to="`/news/detail/${123}`">test</RouterLink>
对应组件中会根据配置的规则认为 123 是一个名为 id 的参数
import { useRoute } from "vue-router";
const route = useRoute()
console.log( route.params.id)
/news/detail
了,所以需要使用 name 属性来对应路由了path: 'detail/:id?',
,否则不传 id 这个参数就会报错直接在路由配置中开启 props
{
path:'/news',
component: News,
children:[
{
name: 'newsDetail',
path: 'detail/:id',
component: NewsDetail,
props: true,
}
]
},
此时相当于将路由收到的所有 params 参数作为 props 传给路由组件
所以路由组件使用 defineProps 接收即可 defineProps(['id','title','content'])
将路由中 props 定义为函数,自己决定将什么作为 props 传给路由组件
例如当你使用 query 传参时
// props 函数有默认传参 route 可以使用
props(route){
return route.query
},
控制路由跳转时浏览器历史记录有 push 模式和 replace 模式,一般默认为 push 模式。
当使用 push 时,从一个页面到另一个页面后能后退回前一个页面;而如果是 replace 模式则无法进行页面回退
设置某个页面为 replace 的方式为在 RouterLink 中添加 replace 属性
<RouterLink replace to="/news" active-class="active">新闻</RouterLink>
以上代码实现路由导航都是使用了 RouterLink 组件,它最终使用 a 标签实现的。但是如果需要手动切换路由而不是通过点击某处来实现,那就只能用编程式路由导航。例如在某个页面挂载三秒后跳转到另一个页面
// 这里是获取路由器而不是某个路由组件的路由了
const router = useRouter()
onMounted(()=>{
setTimeout(()=>{
// 使用 push 模式跳转
router.push('/news')
},3000)
})
router 的使用其实和 RouterLink 的 to 属性一致,例如 push 时不通过字符串形式而是对象形式
const router = useRouter()
// 同理也可以 router.replace()
router.push({
name:'xx',
params: {
...
}
})
我们可以让一个执行路径重新定位到另一个路径,例如首页我们默认要打开 home 页面
{
path: '/',
redirect: '/home',
}
当某些数据需要多个组件共享时,我们就需要集中式状态管理库,pinia 就是这样的一种库
npm i pinia
下一步肯定就是在 app 中使用它,直接编写 main.ts
import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.mount('#app')
一般会在 src 下创建 store 目录,然后比如有一个组件名为 Count.vue,那么我们就会创建一个 count.ts 来使用 pinia
//count.ts
import { defineStore } from "pinia";
// 创建了一个名为 count 的仓库,里面存放了一个名为 sum 的变量
// 注意命名规则为 useXXXStore
export const useCountStore = defineStore('count', {
state(){ // state 需要写成函数形式
return {
sum: 6
}
}
})
使用示例
import {useCountStore} from '@/store/count'
const countStore = useCountStore()
// 以下两种都能获取 sum
console.log(countStore.sum)
console.log(countStore.$state.sum)
小细节:sum 是存储在类型为 Proxy 对象的 countStore 下的一个 ref 类型的数据,按道理我们读取时应该是 countStore.sum.value
,但是这里不需要,因为 reactive 定义的数据会帮我们自动拆包
let a = ref(0)
console.log(a.value)
let b = reactive({
a:ref(0)
})
console.log(b.a)
countStore.sum += 1
当大量数据需要同时修改时可以使用 $patch,对于组件事件(component events)来说只需要进行一次 patch 就能修改完成,而第一种则是修改几个数据就发生几次修改数据(mutation)的事件
countStore.$patch({
sum: countStore.sum + 1,
...
})
学过 vue2 中 vuex 会最先想到的 actions,它存放着一个个方法用来相应组件中的动作
先在 countStore 中定义
export const useCountStore = defineStore('count', {
actions:{
// this 就是这个 store
increment(val){
this.sum += val
}
},
state(){
return {
sum:6,
}
}
})
组件中使用时例如 countStore.increment(123)
当使用 store 中的多个数据时,我们都写成 store.xx 就显得不够优雅,那么我们就会想到解构
注意直接解构会丢失数据的响应式:let {sum} = countStore
所以需要解构成响应式的例如 let {sum} = toRefs(countStore)
但是这样做的代价很大,它会把 store 中的一切转成 ref 引用,包括数据和方法
所以我们需要使用 storeToRefs:let {sum} = storeToRefs(countStore)
,他只会把数据转成响应式的
当 state 中的数据需要经过处理再使用时可以使用 getters,类似于组件的计算属性
例如将 sum 放大十倍
// store/count.ts
export const useCountStore = defineStore('count', {
actions:{
...
},
state(){
sum: 1,
},
getters: {
// this 就是 store,所以可以 this.sum
bigSum(){
return this.sum * 10
},
// 也可以接收传参 state,所以可以用 state.sum
bigSum2(state){
return state.sum * 100
},
}
})
$subscribe能够监听 state 中数据的变化
使用示例
// 某个组件
countStore.$subscribe((mutation,state)=>{
console.log('@@32',mutation,state);
})
回调函数中我们一般不关心第一个传参,state 则是更新后的 state
我们可以以此实现例如防止数据的功能,每次 state 更新就将其存入 localStorage,那么 state 中存放的就是 从 localStorage 中取出来的数据,这样无论页面刷新还是关闭页面再打开都不会丢失数据
// 某个组件
countStore.$subscribe((mutation,state)=>{
localStorage.setItem('sum',state.sum)
})
// store/count.ts
export const useCountStore = defineStore('count', {
state(){
return {
// 不用 JSON.parse 取出来就是 string
// 还有最开始 sum 为 null 的话就让他默认为 1
sum: JSON.parse(localStorage.getItem('sum')) || 1,
}
},
})
以上定义的 store 其实还选项式的,是以对象的形式定义的,我们可以改为函数形式的组合式写法
// store/count.ts
export const useCountStore = defineStore('count', ()=>{
let sum = ref(JSON.parse(localStorage.getItem('sum')) || 1)
function increment(val){
sum.value += val
}
function bigSum(){
return sum.value * 10
}
function bigSum2(){
return sum.value * 100
}
// 别忘了 return,否则无法使用
return {sum,increment,bigSum,bigSum2}
})
//father.vue
<template>
<div class="father">
<h2>father</h2>
<h3 v-show="toy" >子传给父的:{{ toy }}</h3>
<Son :car="car" :sendToy="getToy"/>
</div>
</template>
<script setup lang="ts" name="">
import Son from "@/pages/props/Son.vue";
import { ref } from "vue";
let car = ref("爸爸的车");
let toy = ref("")
// 获取儿子的玩具
function getToy(val){
toy.value = val
}
</script>
//son.vue
<template>
<div class="son">
<h2>son</h2>
<h3>父传给子的:{{ car }}</h3>
<button @click="sendToy(toy)">传给父亲玩具</button>
</div>
</template>
<script setup lang="ts" name="Son">
import { ref } from "vue";
let toy = ref("奥特曼");
defineProps(["car","sendToy"]);
</script>
我们都知道 <button @click="handleClick"></button>
中 @click 的含义是对这个按钮进行点击事件时触发 handleClick 函数的调用,但是我们也可以自定义事件
自定义事件一般用来子传父例如 <h2 @print-ok="printOk">
,我们就定义了一个名为 print-ok 的自定义事件,当该事件触发后会进行 pringOk 函数的回调
触发自定义事件的方式就是使用 defineEmits
// 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 { ref } from "vue";
import Child from "./Child.vue";
let toy = ref("");
function saveToy(val){
toy.value = val
}
</script>
// child.vue
<template>
<div class="child">
<h3>子组件</h3>
<h4>玩具:{{ toy }}</h4>
<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>
注意自定义事件的命名形式为 kebab-case,即短横线连接的命名法,例如 send-toy
我们一般定义 emit 来接收自定义事件,然后调用时就是 emit('自定义事件名', 传参)
能实现任意组件间的通信
安装:npm i mitt
在 src 的 utils 目录下创建 emitter.ts
// 引入mitt
import mitt from 'mitt'
// 调用mitt得到emitter,emitter能:绑定事件、触发事件
const emitter = mitt()
// 暴露emitter
export default emitter
他的使用也很简单,有获取全部事件,绑定事件,解绑事件,触发事件
// 绑定事件
emitter.on('test1',()=>{
console.log('test1被调用了')
})
emitter.on('test2',()=>{
console.log('test2被调用了')
})
// 每秒触发一次事件 test1 和 test2
setInterval(() => {
emitter.emit('test1')
emitter.emit('test2')
}, 1000);
// 三秒后执行
setTimeout(() => {
// 解绑某个事件
emitter.off('test1')
emitter.off('test2')
// 解绑全部事件
emitter.all.clear()
}, 3000);
一般流程:接收数据方会绑定事件,提供数据方触发事件,提供方触发事件时把数据传过去,接收方对传过来的数据进行操作
// 接收方
<template>
<div class="child2">
<h3>子组件2</h3>
<h4 v-show="toy">哥哥给的玩具:{{ toy }}</h4>
</div>
</template>
<script setup lang="ts" name="Child2">
import { ref } from "vue";
import emitter from '@/utils/emitter';
let toy = ref("");
// 绑定事件 send-toy,设置回调函数,如果有玩具传过来就赋值给自己定义的 toy
emitter.on('send-toy',(val:string)=>{
toy.value = val
})
</script>
// 传递方
<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>
以上代码实际上就是关于 mitt 的事件的绑定与触发的使用示例
绑定事件的组件销毁时最好解绑事件
// 绑定方
onUnmounted(()=>{
emitter.off('send-toy')
})
一般开发中不会使用 v-model 进行组件通信,但是 UI 库中底层源码会经常使用,所以理解这个对于阅读 UI 库组件源码有帮助
先说一下当 v-model 用在 html 标签上实现双向绑定的原理
例如 <input type="text" v-model="username">
,我们定义的变量 username 的值会显示在页面中,并且改变输入框的内容也会改变这个变量的值
底层实现:
- 变量的值 -> 页面:底层实现就是通过 value 属性
- 页面输入 -> 变量的值:通过监听 input 事件更改变量的值
即 <input type="text" :value="username" @input="username = $event.target.value">
但是由于我们可以手动 new 一个 event,所以 ts 语法检查会报错,认为可能不存在 $event.target.value,所以我们需要断言
@input="username = ($event.target as HTMLInputElement).value"
或者 @input="username = (<HTMLInputElement>$event.target).value"
例如自定义一个 input 组件 MyInput
我们直接使用 <MyInput v-model="username"/>
肯定不是双向绑定的,所以我们需要编写 MyInput 代码
// Father.vue
<template>
<div class="father">
<h3>父组件</h3>
<h2>username: {{ username }}</h2>
<MyInput :modelValue="username" @update:modelValue="username = $event"/>
</div>
</template>
<script setup lang="ts" name="Father">
import { ref } from "vue"
import MyInput from './MyInput.vue'
let username = ref("john")
</script>
// MyInput.vue
<template>
<input type="text"
:value="modelValue"
@input="emit('update:modelValue', ($event.target as HTMLInputElement).value)"
>
</template>
<script setup lang="ts" name="MyInput">
defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>
实现原理
- 父传子:直接传一个值
- 子传父:传一个自定义事件
我们自然可以自己命名传的值和自定义事件,但是通过以上这样的命名形式,我们可以直接将父组件中的 <MyInput :modelValue="username" @update:modelValue="username = $event"/>
改为 <MyInput v-model="username"/>
也就是说我们在使用 UI 库中的 input 时,之所以能直接 v-model,就是因为组件底层是这样编写的
<MyInput v-model:myValue="username"/>
// MyInput.vue
<template>
<input type="text"
:value="myValue"
@input="emit('update:myValue', ($event.target as HTMLInputElement).value)"
>
</template>
<script setup lang="ts" name="MyInput">
defineProps(['myValue'])
const emit = defineEmits(['update:myValue'])
</script>
可以自定义命名,也就意味着命名不唯一,那么我们可以在使用自定义组件时写多个 v-model:自定义名="xx"
例如 <MyInput v-model:myValue="username" v-model:myValue2="password"/>
那我们在 MyInput 中如果处理 myValue2,也就意味着可以再写一个 input,也就是说使用一次 MyInput 组件可以显示两个 input
专门适用于 祖 <-> 孙
首先需要知道一点,当父组件传东西给子组件时,子组件如果不通过 defineProps 接收,这些数据将会被存储在子组件的 $attrs 中
// Father.vue
<template>
<div class="father">
<h3>父组件</h3>
<Child :a="1"/>
</div>
</template>
<script setup lang="ts" name="Father">
import Child from './Child.vue'
</script>
//Child.vue
<template>
<div class="child">
<h3>子组件</h3>
<h3>未接收的{{ $attrs }}</h3>
</div>
</template>
最终效果:
那么子组件只要选择不接收父组件的数据,把 $attrs 都给孙组件,也就实现了祖传值给孙,、
-
其实孙传祖我们也能想到,只需要祖传给孙一个函数,孙调用时把要给祖的传过去就可以了
-
祖传孙时一次性把 $attrs 中全部内容传给孙组件可以通过 v-bind
<A :a="1">
<==><A v-bind="{a:1}">
祖孙互传示例:
// 父
<template>
<div class="father">
<h3>父组件</h3>
<h4>a: {{ a }}</h4>
<Child :a="a" v-bind="{ b: 2 }" :updateA="changeA" />
</div>
</template>
<script setup lang="ts" name="Father">
import Child from "./Child.vue"
import { ref } from "vue"
let a = ref(1)
function changeA(val:number) {
a.value += val
}
</script>
// 子
<template>
<div class="child">
<h3>子组件</h3>
<GrandChild v-bind="$attrs"/>
</div>
</template>
<script setup lang="ts" name="Child">
import GrandChild from './GrandChild.vue'
</script>
//孙
<template>
<div class="grand-child">
<h3>孙组件</h3>
<h4>a:{{ a }}</h4>
<h4>b:{{ b }}</h4>
<button @click="updateA(6)">点我将爷爷的a更新</button>
</div>
</template>
<script setup lang="ts" name="GrandChild">
defineProps(['a','b','updateA'])
</script>
用于父传子
存放了所有被 ref 标记的 DOM 元素或组件实例
最开始定义响应式数据时提到过 ref 不仅可以定义基本类型的数据,同时可以标记组件,然后获取该组件实例,但是出于数据保护的考虑,无法获取被标记的组件的数据,如果被标记组件愿意暴露数据可以使用 defineExpose
// Child1.vue
<template>
<div class="child1">
<h3>子组件1</h3>
<h4>玩具:{{ toy }}</h4>
</div>
</template>
<script setup lang="ts" name="Child1">
import { ref } from "vue";
let toy = ref('奥特曼')
defineExpose({toy})
</script>
// Father.vue
<template>
<div class="father">
<h3>父组件</h3>
<Child1 ref="c1"/>
<button @click="changeC1Toy">修改c1玩具</button>
</div>
</template>
<script setup lang="ts" name="Father">
import Child1 from './Child1.vue'
import { ref } from "vue";
let c1 = ref()
function changeC1Toy(){
c1.value.toy = '小猪佩奇'
}
</script>
这样就实现了父传子,同理定义一个 Child2 也能以同样的方式传值给 Child2
但是如果需要一次性修改多个子组件,就需要使用 $refs
// 两个子组件都定义一个 toy 并通过 defineExpose 暴露出去就不写了
// Father.vue
// $refs 结构为
// {
// c1:child1组件实例,
// c2:child2组件实例,
// }
<template>
<div class="father">
<h3>父组件</h3>
<Child1 ref="c1"/>
<Child2 ref="c2"/>
<button @click="changeAllToy($refs)">修改所有子组件的玩具</button>
</div>
</template>
<script setup lang="ts" name="Father">
import Child1 from './Child1.vue'
import Child2 from './Child2.vue'
import { ref } from "vue";
let c1 = ref()
let c2 = ref()
function changeAllToy(refs:{[key:string]:any}){
for(let key in refs){
refs[key].toy = '小猪佩奇'
}
}
</script>
用于子传父
存放了当前组件的父组件实例对象
子组件可以通过 $parent 给父组件传值,并且使用方式也符合直觉
//父组件定义一个数并暴露出去,否则子组件读取不到
// Father.vue
let house = ref(4)
defineExpose({house})
//Child1.vue
<button @click="minusHouse($parent)">减少父亲一套房</button>
function minusHouse(parent:any){
parent.house -= 1
}
使用规则为提供数据的父辈组件(爷组件,父组件都可以)使用 provide 提供数据,接收数据放使用 inject 获取数据
// Father.vue
<template>
<div class="father">
<h3>父组件</h3>
<h4>我拥有{{ money }}万</h4>
<h4>我拥有价值{{ car.price }}万的{{ car.brand }}</h4>
<Child/>
</div>
</template>
<script setup lang="ts" name="Father">
import Child from './Child.vue'
import {reactive, ref, provide} from 'vue';
let money = ref(100)
let car = reactive({
brand: '宝马',
price: 100,
})
function updateMoney(val:number){
money.value += val
}
provide('car',car)
// 不能写 money.value,否则相当于只传递了一个值,会失去响应式
provide('moneyContext',{money,updateMoney})
</script>
// GrandChild.vue
<template>
<div class="grand-child">
<h3>孙组件</h3>
<h4>我爷爷拥有{{ money }}万</h4>
<h4>我爷爷拥有价值{{ car.price }}万的{{ car.brand }}</h4>
<button @click="updateMoney(-5)">花爷爷5万</button>
</div>
</template>
<script setup lang="ts" name="GrandChild">
import { inject } from 'vue';
// inject 的第二个参数,可以设置默认值
// 而且因为 ts 没法推断这里的 car 能 .price, .brand,这样的默认值设定还顺便通过 ts 的类型推断
let car = inject('car', {brand:'',price:0})
let {money, updateMoney} = inject('moneyContext', {money:0,updateMoney:(x:number)=>{}})
</script>
当需要显示多种结构布局类似,只有内容稍微不同的页面时,为了提高代码复用率,我们肯定不可能一个页面写一个 .vue 文件,所以我们可以使用插槽
含有插槽组件的使用 slot 标签进行占位,slot 标签中可以编写默认内容;使用该组件时在组件标签中编写的内容就会替代 slot 标签,例如下面代码中 <Game>...</Game>
Game 标签中的内容会替换 Game 组件中的 slot,否则就显示默认内容
// 插槽组件 Game.vue
<template>
<div class="game">
<h2>{{ title }}</h2>
<slot>我是默认内容</slot>
</div>
</template>
<script setup lang="ts" name="Game">
defineProps(['title'])
</script>
// Father.vue
<template>
<div class="father">
<h3>父组件</h3>
<div class="content">
<Game title="热门游戏列表">
<ul>
<li v-for="g in games" :key="g.id">
{{ g.name }}
</li>
</ul>
</Game>
<Game title="今日美食城市">
<img :src="imgUrl" />
</Game>
</div>
</div>
</template>
<script setup lang="ts" name="Father">
import Game from "./Game.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')
</script>
当一个组件含有多个插槽时,使用该组件时你需要手段向指定的插槽填充内容,这就需要具名插槽,也就是有名字插槽
我们可以把上述 Game 组件中的 title 部分也设置成一个插槽,并分别为插槽取名,Father 组件使用它时也根据 v-slot 设置要填充的插槽。
- v-slot 能被设置在组件和 template 上,我们一般都设置在 template 标签中
- 其实默认插槽也有名字,叫 default,不过我们一般不用
// Game.vue
<template>
<div class="game">
<slot name="title">默认标题</slot>
<slot name="content">默认内容</slot>
</div>
</template>
// Father.vue
<template>
<div class="father">
<h3>父组件</h3>
<div class="content">
<Game title="">
<template v-slot:content>
<ul>
<li v-for="g in games" :key="g.id">
{{ g.name }}
</li>
</ul>
</template>
<template v-slot:title>
<h2>热门游戏列表</h2>
</template>
</Game>
<Game>
<template v-slot:title>
<h2>今日美食城市</h2>
</template>
<template v-slot:content>
<img :src="imgUrl" />
</template>
</Game>
</div>
</div>
</template>
<script setup lang="ts" name="Father">
import Game from "./Game.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");
</script>
可以注意到上面的 h2 热门游戏列表虽然写在内容 ul 下面,但是因为具名插槽,会被填充到对应插槽的位置
当数据在子组件,但是根据数据生成的结构,却由父亲决定,就需要使用作用域插槽
// 子组件
<template>
<div class="game">
<h2>游戏列表</h2>
<slot name="content" :games="games"></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>
// 父组件
<template>
<div class="father">
<h3>父组件</h3>
<div class="content">
<Game>
<template v-slot:content="{games}">
<ul>
<li v-for="g in games" :key="g.id">
{{ g.name }}
</li>
</ul>
</template>
</Game>
</div>
</div>
</template>
因为子组件可能传递多个数据,所以父组件接收到的是对象类型的数据
如果不使用解构方式接收子组件的数据,一般会命名为 params,即 <template v-slot="params">
v-slot 还有语法糖,例如以上代码可以改为 <template #content="{games}">
类似于 ref,但是只处理第一层的响应式
总结:通过使用 shallowRef() 和 shallowReactive() 来绕开深度响应。浅层式 API 创建的状态只在其顶层是响应式的,对所有深层的对象不会做任何处理,避免了对每一个内部属性做响应式所带来的性能成本,使得属性的访问变得更快,可提升性能
// 以下两个 function 都能正常使用
let sum = ref(1);
let sum2 = shallowRef(10)
function addSum() {
sum.value += 1;
}
function addSum2() {
sum2.value += 10;
}
// 但是以下 function 中涉及到修改第二层值的 changePrice2 就无法正常使用了
// 也可以简单理解为 shallowRef 只能修改到 xx.value,无法继续往后 value.xx
let car = ref({
price: 10,
});
let car2 = ref({
price: 100
})
function changePrice() {
car.value.price += 1;
}
function changeCar() {
car.value = { price: 20 };
}
function changePrice2() {
car2.value.price += 10;
}
function changeCar2() {
car2.value = { price: 200 };
}
// 使用 shallowReactive 也同理,以下修改 price 能修改
// 但是修改 engine 时使用 shallowReactive 定义的 car2 就无法修改
let car = reactive({
price: 10,
options: {
engine: "v8",
},
});
let car2 = shallowReactive({
price: 100,
options: {
engine: "v8",
},
});
function changePrice() {
car.price += 1;
}
function changeEngine() {
car.options.engine = 'v9'
}
function changePrice2() {
car2.price += 10;
}
function changeEngine2() {
car2.options.engine = 'v9'
}
主要用于数据保护,防止数据被拿去展示时被修改
readonly 能创建一个响应式数据的深只读副本,例如以下代码中只有关于 car 的 function 能修改数据,并且由于 car2 是 car 的副本,car 的修改结果会同步给 car2,但是有关 car2 的 function 都无法起作用
let car = reactive({
price: 10,
options: {
engine: "v8",
},
});
let car2 = readonly(car);
function changePrice() {
car.price += 1;
}
function changeEngine() {
car.options.engine = 'v9'
}
function changePrice2() {
car2.price += 10;
}
function changeEngine2() {
car2.options.engine = 'v9'
}
shallowReadonly 顾名思义就是浅层的 readonly,唯一不同的是只可以修改深层次的数据,例如以下代码中关于 car2 的function 中,只有修改深层次数据的 changeEngine2 能起作用
let car = reactive({
price: 10,
options: {
engine: "v8",
},
});
let car2 = shallowReadonly(car);
function changePrice() {
car.price += 1;
}
function changeEngine() {
car.options.engine = 'v9'
}
function changePrice2() {
car2.price += 10;
}
function changeEngine2() {
car2.options.engine = 'v9'
}
作用:标记一个对象,使其永远不会变成响应式
使用场景:例如使用第三方库时,可以防止错误地把第三方库暴露的对象定义成响应式的
// person2 就是响应式的
let person = {name: 'john'}
let person2 = reactive(person)
-------------------------------------------
// person2 不是响应式的
let person = markRaw{name: 'john'}
let person2 = reactive(person)
使用 ref 时,由于数据是响应式的,所以当你改变数据时,显示它的地方会立刻跟随着改变,但有些时候我们会有一些自定义的需求,比如更改数据后,显示的地方经过一秒后再同步更新,这时 ref 是无法满足需求的,就需要自定义的 ref 即 customRef
// 例如定义一个响应式的数字 1,基本结构如下
let a = customRef(() => {
return {
get(){
},
set(){
},
};
});
// get 函数很容易想到是返回 1,set 函数按道理也会有一个传参,是修改后的新值 val,
// 但是不知道把 val 给谁,所以我们需要一个变量来辅助我们
let initVal = 1
let a = customRef(() => {
return {
get(){
return initVal
},
set(val){
initVal = val
},
};
});
// 但是光这样还是不行的,我们的修改不会被触发
// 同时就算修改成功了,最新的值也不会被追踪观察,即不会在页面上同步更新
// 所以我们需要两个函数通知 vue 追踪 get 函数中 a 的数据变化,同时触发 set 函数中 a 的数据更新
let initVal = 1
let a = customRef((track, trigger) => {
return {
get(){
track() // 告诉 vue 数据 a 很重要,需要持续关注,一旦 a 变化就去更新
return initVal
},
set(val){
initVal = val
trigger() // 通知 vue 触发 a 的数据更新
},
};
});
// 这时加上需求例子中的一秒后同步更新页面的功能可以使用 setTimeout 包裹 set 函数部分
set(val) {
setTimeout(() => {
initVal = val;
trigger();
}, 1000);
},
// 但是当输入过快时,由于瞬间开启多个定时器,所以会出现问题,所以可以定义 timer
let initVal = 1;
let timer: number;
let a = customRef((track, trigger) => {
return {
get() {
track();
return initVal;
},
set(val) {
clearTimeout(timer);
timer = setTimeout(() => {
initVal = val;
trigger();
}, 1000);
},
};
});
// 当我们需要多次使用这种类似的 customRef 时可以将其定义为 hook,可以传入初始值和延迟时长
// useA.ts
import { customRef } from "vue";
export default function (initVal: number, delay: number) {
let timer: number;
let a = customRef((track, trigger) => {
return {
get() {
track();
return initVal;
},
set(val) {
clearTimeout(timer);
timer = setTimeout(() => {
initVal = val;
trigger();
}, delay);
},
};
});
return { a };
}
// 使用时
let { a } = useA(1, 2000);
类似该单词的意思,能够实现远距离传送的功能
<Teleport to="body">...</Teleport>
,此时被包裹的内容会直接处于 body 标签内第一层,就一定会相对于整个窗口定位作用:等待异步组件时渲染一些额外内容,让应用有更好的用户体验
例如子组件 setup 中数据定义部分有直接的网络请求,此时子组件会无法显示,想要正常使用,父组件就需要用 Suspense 标签包裹子组件
- Suspense 标签中有两个插槽,一个显示异步组件,一个显示网络请求时暂时显示的内容
<Suspense>
<template v-slot:default>
<异步组件 />
</template>
<template v-slot:fallback>
<h2>加载中...</h2>
</template>
</Suspense>
其实就是 vue2 中原本 vue.xx 的代码转移到了 app.xx
定义全局组件
app.component("Modal", Modal);
会定义一个名为 Modal 的对应 Modal 组件的全局组件定义 vue 全局配置
例如编写一个全局变量 x 可以 app.config.globalProperties.x = 99
但是直接使用时虽然可以显示,但编译器会报错,根据官方文档可以在 main.ts 中定义
declare module "vue" {
interface ComponentCustomProperties {
x: number
}
}
自定义指令
例如定义一个可以美化内容的指令 v-beauty
app.directive("beauty", (element, { value }) => {
element.innerText += value;
element.style.backgroundColor = 'yellow'
});
使用时 <h3 v-beauty="123">test</h3>
挂载 app
卸载 app
利用插件
原文地址:https://blog.csdn.net/m0_53256503/article/details/140673622
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!