一、 props
1、 typescript环境 props自定义类型propType
//子组件的props类型是复杂的类型的时候,可以用propType进行强制类型转换
//eg 复杂函数,对象数组,对象的类型检查
props: {
callback: {
type: Function as PropType<() => void>,
},
tableHead: {
type: Array as PropType<getTableHeadRes[]>,
default:
() => [],
},
person: {
type: Object as PropType<Person>,
required:true,
},
}
二、ref 标签
1、Vue3中使用ref标签对组件进行操作方法
在Vue2中 一般用 this.$ref.xxxx 进行获取组件对象
Vue3中就不使用这个方法了: 例如:
// 如:
<el-upload class="upload-demo" action="" :http-request="handleUpload"
:on-change="handleChange"
:before-upload="handleBeforeUpload" :show-file-list="false" :auto-upload="false" :limit="1" ref="uploadRef">
<el-button type="primary" icon="upload" slot="trigger">导入</el-button>
</el-upload>
//想要获取el-upload组件对象,先创建
const uploadRef = ref()
//使用的话需要xxx.value.xxx ,清除上传列表
uploadRef.value.clearFiles()
Vue3 中 ref 标记组件使用 : 在 Vue3 中我们还可以使用 ref 标记组件来进行获取父子组件获取属性和方法的操作。
// 父组件
<template>
<hello-world ref='son'></hello-world>
<button @click='getSon()'>获取</button>
</template>
<script setup>
// 首先还是先引入子组件
import HelloWorld from './components/HelloWorld.vue'
// 然后引入 ref ,并声明 son
import {ref} from 'vue'
const son = ref()
const getSon = () => {
console.log(son.value.title)
// 子组件的方法
son.value.sonMethod()
}
</script>
//子组件
<template>
<div> {{ title }} </div>
</template>
<script setup>
import {ref} from 'vue'
const title = ref('我是子组件的title')
const sonMethod = () => {
console.log('我是子组件的方法')
}
// 最要的一步,这里我们需要把父组件需要的属性和方法暴露出去,这样父组件才能获取的到
defineExpose({sonMethod, title})
</script>
2、vue3 获取 ref 元素的几种方式
(1)、静态绑定
// 获取单个 dom 元素
<template>
<button ref="domRef">dom</button>
</template>
<script setup>
import { ref, onMounted } from 'vue';
// 声明一个跟 ref 同名的变量即可,来保存元素
const domRef = ref(null);
onMounted(() => {
domRef.value.style.background = "red";
})
</script>
// v-for 中使用
<template>
<ul>
<li v-for="item in list" ref="domRef">
{{ item }}
</li>
</ul>
</template>
<script setup>
import { ref, onMounted } from 'vue';
const list = ref([
/* ... */
])
const domRef = ref([]);
onMounted(() => {
console.log(domRef.value)
})
</script>
//但需要注意的是,itemRefs 元素数组并不保证与 list 数组为相同的顺序
//需要注意的是,访问的时候,要确保 ref 引用值已经成功绑定上元素,我们可以使用以下几种方式确保获取
// onMounted
onMounted(() => {
domRef.value.style.background = "red";
})
// nextTick
nextTick(() => {
domRef.value.style.background = "red";
})
// watchEffect
watchEffect(() => {
if (domRef.value) {
domRef.value.style.background = "red";
}
})
// watch
watch(domRef, val => {
domRef.value.style.background = "red";
})
(2)、动态绑定
动态绑定中,分为两种方式,一种是通过将 ref 设置为函数,第二种则是通过 getCurrentInstance 方法访问当前组件实例上的 $refs
ref 设置函数,获取单个 dom 元素
<template>
<button :ref="handleRef">动态 Ref</button>
</template>
<script setup>
import { ref } from 'vue'
const btnRef = ref(null);
// 赋值动态ref到变量
const handleRef = el => {
if (el) {
btnRef.value = el;
}
}
</script>
// ref 的函数回调中,我们能够接受到元素返回值,再动态设置到响应式变量即可
ref 设置函数,v-for 中使用循环设置ref并获取
-
使用v-for循环出来的dom,ref通过index下标来命名
// ref通过index下标来命名
<Pie :id="`riskSpread${index}`" :ref="el => getRiskSpreadRef(el, index)" :title="item.title" :data="item.data" emptyText="暂无风险" />// 此时riskSpreadRefList里面放的就是所有ref
const riskSpreadRefList = ref<HTMLElement[]>([]);
//
const getRiskSpreadRef = (el, index) => {
if (el) {
riskSpreadRefList.value[index] = el;
}
};
// 使用:循环去取就行了,item就是通过ref拿到的dom元素。可以操作上面定义的变量或方法
riskSpreadRefList.value?.forEach((item: any) => {
console.log(item)
});m)
}); -
还有一种获取ref的方法,与上面略相似,记录一下,但是用push可能会造成ref还没渲染完得到null的情况,所以最好还是上面那样写
// <div class="chart"> <Pie :id="`risk${index}`" :ref="getRiskRef" :data="item.data" @clickPie="queryRiskList" /> </div> // let riskRefList = ref<HTMLElement[]>([]); const getRiskRef = (el) => { if (el) { riskRefList.value.push(el); } }; // riskRefList.value?.forEach((item: any) => { console.log(item) });
(3)、通过 getCurrentInstance 方法
这种方式,与 vue2 的 this.$refs 一般无二,只是我们用了 getCurrentInstance 函数在 setup 中获取了当前组件实例以替代 this
<template>
<ul>
<li v-for="item in list" :key="item.id" :ref="item.id">
<button>{{ item.id }}</button>
</li>
</ul>
</template>
<script setup>
import { getCurrentInstance, onMounted, ref } from "vue";
const { proxy } = getCurrentInstance();
const list = ref([{ id: "111" }, { id: "222" }, { id: "333" }]);
onMounted(() => {
console.log(proxy.$refs["111"]);
})
</script>
(4)、获取 vue 实例
需要注意的是,无论通过以上哪种方式获取元素,如果元素为 vue 组件,则需要在子组件中使用 defineExpose 进行暴露。
在父组件中,我们静态绑定 childRef
<template>
<Test ref="childRef"></Test>
</template>
<script setup lang="ts">
import Test from "./components/test.vue";
import { onMounted, ref } from "vue";
const childRef = ref(null);
onMounted(() => {
console.log(childRef.value.btnRef)
})
</script>
在子组件中,我们需要通过defineExpose函数,手动暴露出来ref引用值,该值指向了button元素
<template>
<button ref="btnRef">子组件</button>
</template>
<script setup>
import { ref } from "vue";
const btnRef = ref(null)
defineExpose({
btnRef
})
</script>
三、vue3数据类型ref,Reactive,shallowRef,shallowReactive基本用法
- ref 用于创建基础类型的响应式,也可以创建引用类型的响应式.
- ref 对于引用类型,底层也是转换为 reactive 来进行响应式处理
- ref 创建的响应式数据在脚本中需要通过 .value, 模板中会自动添加上 .value,所以模板中不需要通过 .value 访问
- ref 创建出来的响应式就是 RefImpl 实例对象
- Ref 与 Reactive 创建的都是递归响应的,将每一层的 json 数据解析成一个 proxy 对象
- shallowRef 与 shallowReactive 创建的是非递归的响应对象
- shallowReactive 创建的数据第一层数据改变会重新渲染 dom
- shallowRef 创建的响应式对象,需要修改整个 value 才能重新渲染 dom
- 想更新 shallowRef 的某一层数据,并且想触发渲染,可以使用 triggerRef
- reactive 不支持对基本类型数据响应式
- ref 是在 reactive 上在进行了封装进行了增强, 全部使用ref可以
- ref(1) 就等价于 reactive({value: 1})
- 可以all in reactive,把页面的响应式数据都集中在一个reactive中,类似vue2的data
四、Vue 路由使用
1、路由的跳转方式
- 声明式导航:router-link,要有to属性
- 编程式导航 :$router.push | replace实现,可以处理一些业务
2、路由传参,参数有几种呢?
- params参数
- query参数
params参数
1、动态路由时传参
-
属于路径当中的一部分,在配置路由的时候,需要占位。地址栏表现为 /search/v1
-
刷新页面,参数还在
//占位:
path:'/search/:keyword'
2、非动态路由时传参
-
不属于路径当中的一部分,类似 post请求,地址栏地址不变
-
但是刷新参数数据会消失
-
params参数,跳转路由需要用:name
this.$router.push({
name:'search',
params:{
keyword:this.keyword
}
});
query参数
-
不属于路径当中的一部分,类似于get请求,地址栏表现为 /search?k1=v1&k2=v2,不需要占位
-
跳转路由需要用:name或path都可以。网上说要用path,但是自己测试都可以。
this.$router.push({
path:'/search',
query:{
k1:this.v1,
k2:this.v2
}
});
3、路由传参
3.1.声明式导航传参
<router-link :to="'/search/'+keyword">搜索</router-link>
<!-- http://localhost:8080/search/hhh -->
<router-link :to="{path: '/search', name: 'search', params: { keyword: hhh } }">搜索</router-link>
3.2.编程式导航
//1、字符串形式 this.$router.push('/search/'+this.keyword+'?k='+this.keyword.toUpperCase());
//2、模板字符串
this.$router.push(`/search/${this.keyword}?k=${this.keyword.toUpperCase()}`);
//3、对象(常用),传的是对象,路由跳转传参params参数,需要路由配置文件给路由命个名 ==>name:'search'
this.$router.push({
name:'search',
params:{
keyword:this.keyword
},
query:{
k:this.keyword.toUpperCase()
}
});
// http://localhost:8080/search/hh?k=HH
- 对象(常用):this.$router.push({name:"路由名字",params:{传参},query:{传参})
- 以对象方式传参时,如果我们传参中使用了params,只能使用name,不能使用path,如果只是使用query传参,可以使用path
3.3. props:路由组件可以传递props
//router/inex.js
{
name: "search",
path: "/search/:keyword?",
component: Search,
meta: {
show: true,
},
//1、布尔值写法,props只能传params
props:true
//2、对象写法,额外给路由自己传递一些props
props:{
a:1,
b:2
}
//3、函数写法,可以params参数、query参数,通过prop传递给路由组件(很少用)
props:($route)=>{
return{
keyword:$route.params.keyword,
k:$route.query.k
}
}
props: ($route) => ({keyword: $route.params.keyword,k: $route.query.k,}),
},
路由组件可以传递props
//pages/Search/index.vue
//路由组件可以传递props
props:['keyword','a','b','k'],
4、路由传参相关问题
1、路由传递参数(对象写法)path是否可以结合params参数一起使用?
-
路由跳转传参的时候,对象的写法可以是name、path形式,但是path的写法不可以和params参数一起使用。
-
params 参数: 已不建议使用 params 方式传参 具体查看
-
(路径参数缺失是无法匹配path里面的占位符)
-
因此,不能,传params-->对象写法,是要name的写法
this.$router.push({
path:'/search',
params:{keyword:this.keyword},
query:{k:this.keyword.toUpperCase()}
});
//这个写法,真实的是: http://localhost:8080/#/search?k=HH
//正确路径,应该的是: http://localhost:8080/#/search/hh?k=HH
2、如何指定params参数可以不传?
问题场景:配置路由的时候,已经占位了(params参数),但是路由跳转的时候就不传递。
- 这个写法,真实的是:http://localhost:8080/#/?k=HHH
- 正确路径,应该的是:http://localhost:8080/#/search/?k=HHH
- 如果路由要求传params参数,但是你没传params参数,发现url会有问题
如何指定params可以传递或者不传递?
解决:
-
配置路由时,可以在占位符后面加个?【?代表可传或不传,正则的问号一次或多次】
//router/inex.js
{
name:'search',
path:'/search/:keyword?',
component:Search,
}this.$router.push({
name:'search',
// params:{keyword:this.keyword},
query:{k:this.keyword.toUpperCase()}
});
//真实路径:http://localhost:8080/#/search?k=HHH
3、params参数可以传递可以不传递,但是如果传递为空字符串,如何解决?
-
这个写法,真实的是:http://localhost:8080/#/?k=, 路径没有/search
-
正确路径,应该的是:http://localhost:8080/#/search?k=
//使用undefined解决:params参数可以传递,或不传递(传空字符串)
this.$router.push({
name: "search",
params: { keyword: ""||undefined },
query: { k: this.keyword.toUpperCase() },
});
4、路由组件能不能传递props数据
- 见 3.3. props:路由组件可以传递props
五、数据通讯传输:9 种组件通信机制
1、概述
我们将涵盖以下关键概念:
- Props / Emit: 用于父组件向子组件传递数据以及子组件向父组件发出事件通知。
- Provide / Inject: 用于父组件向所有后代组件提供数据,实现依赖注入。
- Pinia: 一个现代化的 Vue 状态管理库,替代了传统的 Vuex。
- Expose / Ref: 用于父组件直接访问子组件的实例或元素。
- Attrs: 用于获取父组件传递给子组件的非 prop 属性。
- v-Model: 用于实现双向数据绑定。
- mitt.js: 一个事件总线库,用于跨组件通信。
- Slots: 用于父组件控制子组件部分内容,实现组件模板的灵活性和可重用性。
2、Props / Emit
2.1 父组件向子组件传递数据
父组件通过 props 属性将数据传递给子组件。子组件可以通过 defineProps 方法获取这些数据。
<!-- 父组件 Parent.vue -->
<template>
<Child :msg2="msg2" />
</template>
<script setup lang="ts">
import Child from './Child.vue';
import { ref, reactive } from 'vue';
const msg2 = ref<string>('This is the message 2 sent to the child component');
// 或对于复杂类型
const msg2 = reactive<string>(['This is the message 2 for the descendant component']);
</script>
<!-- 子组件 Child.vue -->
<template>
<!-- 使用 props -->
<div>子组件收到消息:{{ props.msg2 }}</div>
</template>
<script setup lang="ts">
// 无需导入,直接使用
// import { defineProps } from "vue"
interface Props {
msg1: string;
msg2: string;
}
const props = withDefaults(defineProps<Props>(), {
msg1: '',
msg2: '',
});
console.log(props); // { msg2: "This is the message 2 for the descendant component" }
</script>
2.2 子组件向父组件传递数据
子组件通过 emit 方法向父组件发送事件,并传递数据。父组件可以通过 v-on 指令监听事件,并接收数据。
<!-- 子组件 Child.vue -->
<template>
<button @click="emit('myClick')">Button</button>
<!-- 方法二 -->
<button @click="handleClick">Button</button>
</template>
<script setup lang="ts">
// 方法一
// import { defineEmits } from "vue"
// 对应方法一
const emit = defineEmits(['myClick', 'myClick2']);
// 对应方法二
const handleClick = () => {
emit('myClick', 'This is the message sent to the parent component');
};
// 方法二,不适合 Vue3.2 版本,useContext() 已过时
// import { useContext } from "vue"
// const { emit } = useContext()
// const handleClick = () => {
// emit("myClick", "This is the message sent to the parent component")
// }
</script>
<!-- 父组件 Parent.vue -->
<template>
<Child @myClick="onMyClick" />
</template>
<script setup lang="ts">
import Child from './Child.vue';
const onMyClick = (msg: string) => {
console.log(msg); // This is the message received by the parent component
};
</script>
注意:
-
如果父组件使用 setup() 方法,子组件使用脚本设置语法,子组件将无法从父组件的数据中接收属性,只能接收父组件 setup() 函数中传递的属性。
-
如果父组件使用脚本设置语法糖,子组件使用 setup() 方法,子组件可以从父组件的数据和 setup() 函数中通过 props 接收属性。但是,如果子组件想要在它的 setup 中接收属性,它只能接收父组件 setup() 函数中的属性,不能接收数据属性。
2.3 组合式API和选项式AP混用时进行通信(父组件使用语法糖方式,子组件使用函数方式时互相通信,且可以使用v-model 更新属性值)
在 Vue3 中,$emit('update:visible', false) 是一种用于子组件向父组件传递事件的方式,通常用于实现 双向绑定 或通知父组件更新某个属性的值。
以下是一个简单的示例,展示如何使用 $emit 更新父组件的 visible 属性:
<!-- 子组件 -->
<template>
<div>
<button @click="closeDialog">关闭弹窗</button>
</div>
</template>
<script setup>
import { defineEmits } from 'vue';
// 定义事件
const emit = defineEmits(['update:visible']);
// 触发事件
const closeDialog = () => {
emit('update:visible', false);
};
</script>
<!-- 父组件 -->
<template>
<div>
<Child :visible="isVisible" @update:visible="isVisible = $event" />
<p>弹窗状态:{{ isVisible }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue';
import Child from './Child.vue';
const isVisible = ref(true);
</script>
工作原理:子组件通过 emit('update:visible', false) 通知父组件更新 visible 属性。父组件通过监听 update:visible 事件,动态更新其 isVisible 值。 注意事项:双向绑定:Vue3 推荐使用 v-model 的新语法(如 v-model:visible),它是 update:propName 的语法糖。单向数据流:子组件不能直接修改父组件传递的 props,而是通过 emit 通知父组件更新。
替代方案:如果需要更简洁的写法,可以使用 v-model:
<Child v-model:visible="isVisible" />
//这会自动绑定 update:visible 事件,无需手动监听
3、Provide / Inject
provide 和 inject 机制用于在父组件和其所有后代组件之间共享数据,即使它们不是直接的父子关系。
-
provide: 用于在父组件中定义要共享的数据。
-
inject: 用于在后代组件中获取共享数据。
<script setup> import { provide } from 'vue';provide('name', 'Jhon');
<script setup> import { inject } from 'vue';
</script>const name = inject('name');
console.log(name); // Jhon
</script>
4、Pinia
Pinia 是一个现代化的 Vue 状态管理库,旨在取代传统的 Vuex。
// main.ts
import { createPinia } from 'pinia';
createApp(App).use(createPinia()).mount('#app');
// /store/user.ts
import { defineStore } from 'pinia';
export const userStore = defineStore('user', {
state: () => {
return {
count: 1,
arr: [],
};
},
getters: {
// ...
},
actions: {
// ...
},
});
// Page.vue
<template>
<div>{{ store.count }}</div>
</template>
<script lang="ts" setup>
import { userStore } from '../store';
const store = userStore();
// 解构
// const { count } = userStore()
</script>
5、Expose / Ref
expose 和 ref 机制用于父组件直接访问子组件的实例或元素。
<!-- 子组件 Child.vue -->
<script setup>
// 方法一,不适合 Vue 3.2 版本,useContext() 在此版本中已过时
// import { useContext } from "vue"
// const ctx = useContext()
// 暴露属性和方法等
// ctx.expose({
// childName: "This is a property of the child component",
// someMethod(){
// console.log("This is a method of the child component")
// }
// })
// 方法二,适合 Vue 3.2 版本,无需导入
// import { defineExpose } from "vue"
defineExpose({
childName: 'This is a property of the child component',
someMethod() {
console.log('This is a method of the child component');
},
});
</script>
<!-- 父组件 Parent.vue -->
<template>
<Child ref="comp" />
<button @click="handlerClick">Button</button>
</template>
<script setup>
import Child from './Child.vue';
import { ref } from 'vue';
const comp = ref(null);
const handlerClick = () => {
console.log(comp.value.childName); // 获取子组件暴露的属性
comp.value.someMethod(); // 调用子组件暴露的方法
};
</script>
6、Attrs
attrs 对象包含从父级作用域传递给子组件的非 prop 属性,不包括 class 和 style
<!-- 父组件 Parent.vue -->
<template>
<Child :msg1="msg1" :msg2="msg2" title="3333" />
</template>
<script setup>
import Child from './Child.vue';
import { ref, reactive } from 'vue';
const msg1 = ref('1111');
const msg2 = ref('2222');
</script>
<!-- 子组件 Child.vue -->
<script setup>
import { defineProps, useContext, useAttrs } from 'vue';
// 无需在 3.2 版本中导入 defineProps,直接使用
const props = defineProps({
msg1: String,
});
// 方法一,不适合 Vue3.2 版本,因为 useContext() 已过时
// const ctx = useContext()
// 如果 msg1 没有作为 prop 接收,它将是 { msg1: "1111", msg2:"2222", title: "3333" }
// console.log(ctx.attrs) // { msg2:"2222", title: "3333" }
// 方法二,适合 Vue3.2 版本
const attrs = useAttrs();
console.log(attrs); // { msg2:"2222", title: "3333" }
</script>
7、v-Model
v-model 指令用于实现双向数据绑定。
<!-- 父组件 Parent.vue -->
<template>
<Child v-model:key="key" v-model:value="value" />
</template>
<script setup>
import Child from './Child.vue';
import { ref, reactive } from 'vue';
const key = ref('1111');
const value = ref('2222');
</script>
<!-- 子组件 Child.vue -->
<template>
<button @click="handlerClick">Button</button>
</template>
<script setup>
// 方法一,不适合 Vue 3.2 版本,因为 useContext() 已过时
// import { useContext } from "vue"
// const { emit } = useContext()
// 方法二,适合 Vue 3.2 版本,无需导入
// import { defineEmits } from "vue"
const emit = defineEmits(['key', 'value']);
// 使用
const handlerClick = () => {
emit('update:key', 'New key');
emit('update:value', 'New value');
};
</script>
8、mitt.js
在 Vue3 中,事件总线不再可用,但现在可以使用 mitt.js 来替代,它基于与事件总线相同的原理。
// mitt.js
import mitt from 'mitt';
const mitt = mitt();
export default mitt;
// 组件 A
<script setup>
import mitt from './mitt';
const handleClick = () => {
mitt.emit('handleChange');
};
</script>
// 组件 B
<script setup>
import mitt from './mitt';
import { onUnmounted } from 'vue';
const someMethed = () => {
// ...
};
mitt.on('handleChange', someMethed);
onUnmounted(() => {
mitt.off('handleChange', someMethed);
});
</script>
9、Slots
Slots 允许父组件控制子组件部分内容,从而实现组件模板的灵活性和可重用性。
9.1 默认插槽
<!-- 父组件 Parent.vue -->
<template>
<FancyButton>Click me!</FancyButton>
</template>
<!-- 子组件 Child.vue -->
<template>
<button class="fancy-btn">
<slot></slot>
</button>
</template>
Click me!
9.2 具名插槽
具名插槽是基于默认插槽的一种分类,可以理解为将内容匹配到对应的占位符。
<!-- 父组件 Parent.vue -->
<template>
<Child>
<template v-slot:monkey>
<div>monkey</div>
</template>
<button>Click me!</button>
</Child>
</template>
<!-- 子组件 Child.vue -->
<template>
<div>
<!-- 默认插槽 -->
<slot></slot>
<!-- 具名插槽 -->
<slot name="monkey"></slot>
</div>
</template>
monkey
Click me!
9.3 作用域插槽
插槽的内容无法访问子组件的状态。然而,在某些情况下,插槽的内容可能想要使用父组件和子组件作用域的数据。为了实现这一点,我们需要一种方法让子组件在渲染时向插槽提供一些数据。
<!-- 父组件 Parent.vue -->
<template>
<!-- v-slot="{scope}" 用于接收从子组件传递上来的数据 -->
<!-- :list="list" 将列表传递给子组件 -->
<Child v-slot="{scope}" :list="list">
<div>
<div>Name: {{ scope.name }}</div>
<div>Occupation: {{ scope.occupation }}</div>
<hr>
</div>
</Child>
</template>
<script setup>
import { ref } from 'vue';
import Child from './components/Child.vue';
const list = ref([
{ name: 'Jhon', occupation: 'Thundering' },
// ...
]);
</script>
<!-- 子组件 Child.vue -->
<template>
<div>
<!-- 使用 :scope="item" 返回每个项目 -->
<slot v-for="item in list" :scope="item" />
</div>
</template>
<script setup>
const props = defineProps({
list: {
type: Array,
default: () => [],
},
});
</script>