Vue3 五天速成(中)https://developer.aliyun.com/article/1504166?spm=a2c6h.13148508.setting.28.36834f0eJwPRIa
28. 组件通信_mitt
类似于总线bus,收数据的:在公共区域提前绑定好事件;提供数据的:在合适的时候触发事件;
首先需要安装mitt
npm i mitt
由于mitt属于一个工具,所以在src目录下创建一个utils文件夹,创建一个emitter.ts
文件;
emitter.ts 文件内容如下:
// 引入 mitt import mitt from 'mitt' // 调用mitt得到emitter,emitter可以绑定事件和触发事件 const emitter = mitt() // 绑定事件 emitter.on('test1', ()=>{ console.log('test1被调用了') }) emitter.on('test2', ()=>{ console.log('test2被调用了') }) // 触发事件 setInterval(()=>{ emitter.emit('test1') emitter.emit('test2') }, 2000) // 解绑事件 setTimeout(()=>{ emitter.off('test1') // 全部解绑 // emitter.all.clear() }, 3000) // 暴露emitter export default emitter
同时,需要在mian.ts文件中进行引入:
import {createApp} from 'vue' import App from './App.vue' import {createPinia} from 'pinia' import router from './router' import emitter from './utils/emitter' const app = createApp(App) const pinia = createPinia() app.use(pinia) app.use(router) app.mount('#app')
注意,这里不需要app.use;
Child1.vue代码如下:
<template> <div class="child"> <h2>这是子1</h2> <h3> 玩具 {{toy}}</h3> <button @click="emitter.emit('send-toy', toy)">点击发送玩具</button> </div> </template> <script lang="ts"> export default { name: 'Child1' } </script> <script setup lang="ts"> import emitter from '@/utils/emitter'; import {ref} from 'vue' let toy = ref('atm') </script> <style scoped> div.child { background-color: gray; width: 80%; height: 70%; margin-left: 20px; border-radius: 10px; box-shadow: 0 0 10px; } </style>
Child2.vue代码如下:
<template> <div class="child"> <h2>这是子2</h2> <h3> 电脑 {{computer}}</h3> <h3 v-if="toy"> 玩具 {{toy}}</h3> </div> </template> <script lang="ts"> export default { name: 'Child2' } </script> <script setup lang="ts"> import {ref, onUnmounted} from 'vue' import emitter from '@/utils/emitter'; let computer = ref('wxr') let toy = ref('') emitter.on('send-toy', (val:any)=>{ toy.value = val }) // 防止被卸载后占用内存 onUnmounted(()=>{ emitter.off('send-toy') }) </script> <style scoped> div.child { background-color: gray; width: 80%; height: 70%; margin-left: 20px; border-radius: 10px; box-shadow: 0 0 10px; } </style>
29. 组件通信_v-model
UI组件库底层大量使用v-model进行通信;
e v e n t :对于原生事件, event: 对于原生事件, event:对于原生事件,event就是事件对象; 能==> .target
对于自定义事件,$event就是触发事件时,所传递的数据; 不能==> .target
Father.vue如下:
<template> <div> <h2>父组件</h2> <h4>value : {{ username }}</h4> <!-- v-model用在html标签上 下面两种方法等价--> <!-- <input type="text" v-model="username"> --> <!-- <input type="text" :value="username" @input="username=(<HTMLInputElement>$event.target).value"> --> <!-- v-model用在组件标签上 下面两种方法等价--> <!-- <MyCustomInput v-model="username"></MyCustomInput> --> <!-- <MyCustomInput :modelValue="username" @update:modelValue="username = $event"></MyCustomInput> --> <!-- 自定义modelValue 需要把UI组件中所有的modelValue改成qwe --> <MyCustomInput v-model:qwe="username"></MyCustomInput> </div> </template> <script lang="ts"> export default { name: 'Model' } </script> <script setup lang="ts"> import {ref} from 'vue' import MyCustomInput from './MyCustomInput.vue'; let username = ref('zhangsan') </script> <style scoped> </style>
自定义组件MyCustomInput.vue如下:
<!-- 要是用v-model,必须要modelValue和update:modelValue --> <template> <div> <input type="text" :value="modelValue" @input="emit('update:modelValue', (<HTMLInputElement>$event.target).value)" > </div> </template> <script lang="ts"> export default { name: 'MyCustomInput' } </script> <script setup lang="ts"> defineProps(['modelValue']) const emit = defineEmits(['update:modelValue']) </script> <style scoped> input { border: 2px solid black; background-image: linear-gradient(45deg, red, yellow, green); height: 30px; font-size: 20px; color: white; } </style>
30. 组件通信_$attrs
使用props,未用defineProps的数据,子组件会使用attrs接收;
Father.vue
<template> <div class="father"> <h2> 父组件 </h2> <h4> {{ data }} </h4> <!-- v-bind="{x: 100, y:200}" 相当于 :x="100" :y="200" --> <Child v-bind="data"></Child> </div> </template> <script lang="ts"> export default { name: 'AttrsListeners' } </script> <script setup lang="ts"> import Child from './Child.vue' import {reactive} from 'vue' let data = reactive({ a: 100, b: 200, c: 300 }) </script> <style scoped> div.father { margin: 30px; background-color: yellow; width: 500px; height: 300px; border-radius: 10px; box-shadow: 0 0 10px; } </style>
Child.vue
<template> <div class="child"> <h2> 子组件 </h2> <Grandson v-bind="$attrs"> </Grandson> </div> </template> <script lang="ts"> export default { name: 'Child' } </script> <script setup lang="ts"> import Grandson from './Grandson.vue' </script> <style scoped> div.child { margin: 30px; background-color: red; width: 500px; height: 300px; border-radius: 10px; box-shadow: 0 0 10px; } </style>
Grandson.vue
<template> <div class="grandson"> <h2> 孙组件 </h2> <h4> {{ $attrs }}</h4> </div> </template> <script lang="ts"> export default { name: 'Grandson' } </script> <script setup lang="ts"> </script> <style scoped> div.grandson { margin: 30px; background-color: green; width: 500px; height: 300px; border-radius: 10px; box-shadow: 0 0 10px; } </style>
31. 组件通信_$refs
与$parent
$refs
可以获取html下所有的ref
组件,$parent
可以获取父组件中的数据;都要结合defineExpose()
Father.vue 代码如下:
<template> <div> <h2> 父组件 </h2> <h4>房产:{{house}}</h4> <button @click="changeToys">改变c1的toys</button> <button @click="changeComputer">改变c2的computer</button> <button @click="getAllChild($refs)">获取所有的子组件实例对象</button> <Child1 ref="c1"/> <Child2 ref="c2"/> </div> </template> <script lang="ts"> export default { name: 'RefChildrenParent' } </script> <script setup lang="ts"> import Child1 from './Child1.vue' import Child2 from './Child2.vue' import {ref} from 'vue' let house = ref(3) let c1 = ref() let c2 = ref() function changeToys(){ c1.value.toys = '修改后的玩具' } function changeComputer(){ c2.value.computer = '修改后的电脑' } // refs:{[key:string]:any} 或者 refs:any function getAllChild(refs:{[key:string]:any}){ for(let index in refs){ refs[index].books += 3 } } defineExpose({house}) </script> <style scoped> </style>
Child1.vue 代码如下:
<template> <div> <h2> 子组件1 </h2> <h4>玩具:{{ toys }}</h4> <h4>书籍:{{ books }}</h4> <button @click="minusHouse($parent)">干掉父亲的一套房产</button> </div> </template> <script lang="ts"> export default { name: 'Child1' } </script> <script setup lang="ts"> import {ref} from 'vue' let toys = ref('asdsdsaxzczxc') let books = ref(3) function minusHouse(parent:any){ parent.house -= 1 } defineExpose({toys, books}) </script> <style scoped> </style>
Child2.vue 代码如下:
<template> <div> <h2> 子组件1 </h2> <h4>电脑:{{ computer }}</h4> <h4>书籍:{{ books }}</h4> </div> </template> <script lang="ts"> export default { name: 'Child2' } </script> <script setup lang="ts"> import {ref} from 'vue' let computer = ref('asdas') let books = ref(3) defineExpose({computer, books}) </script> <style scoped> </style>
32. 组件通信_provide-inject
常用于祖孙之间越过父亲直接通信;
祖使用provide传送,孙使用inject接收,inject第二个参数是默认值
Father.vue
<template> <div class="father"> <h2> 父组件 </h2> <h4>钱:{{ money }}</h4> <h4 v-for="car in cars">车:{{ car.brand }} 价值 :{{ car.price }}</h4> <Child></Child> </div> </template> <script lang="ts"> export default { name: 'ProvidedInject' } </script> <script setup lang="ts"> import Child from './Child.vue' import {ref, reactive, provide} from 'vue' let money = ref(100) let cars = reactive([{ brand: 'benz', price: 1000, }]) // 向后代提取数据 provide('qian', money) provide('che', cars) </script> <style scoped> div.father { margin: 30px; background-color: yellow; width: 500px; height: 300px; border-radius: 10px; box-shadow: 0 0 10px; } </style>
Child.vue
<template> <div class="grandson"> <h2> 孙组件 </h2> <h4> 钱:{{ money }}</h4> <h4 v-for="car in cars">车:{{ car.brand }} 价值 :{{ car.price }}</h4> </div> </template> <script lang="ts"> export default { name: 'Grandson' } </script> <script setup lang="ts"> import {inject} from 'vue' // 这里inject第二个参数是默认值 let money = inject('qian', '我是默认值') let cars = inject('che', {}) </script> <style scoped> div.grandson { margin: 30px; background-color: green; width: 500px; height: 300px; border-radius: 10px; box-shadow: 0 0 10px; } </style>
32. 组件通信_slot默认插槽
slot有三种:默认插槽,具名插槽,作用域插槽;
默认插槽就是在组件中间使用html标签,然后在组件中加入;
Father.vue 如下:
<template> <div class="father"> <Category> <h2> 热门游戏列表</h2> <ul> <li v-for="game in games" :key="game.id">{{ game.name }}</li> </ul> </Category> <Category> <template v-slot:default> <h2> 热门美食列表</h2> <img :src="imgUrl" alt=""> </template> </Category> <Category> <h2> 热门美食列表</h2> <video :src="videoUrl" controls> </video> </Category> </div> </template> <script lang="ts"> export default { name: 'Slot' } </script> <script setup lang="ts"> import Category from './Category.vue'; import {ref, reactive} from 'vue' let games = reactive([ {id: 'zxzczxczxc01', name: '爱谁谁大所多'}, {id: 'zxzczxczxc02', name: '爱阿萨德按所多'}, {id: 'zxzczxczxc03', name: '爱自行车在线所多'}, {id: 'zxzczxczxc04', name: '爱谁周传雄所多'}, ]) let imgUrl = ref('https://z1.ax1x.com/2023/11/19/piNxLo4.jpg') let videoUrl = ref('http://vjs.zencdn.net/v/oceans.mp4') </script> <style scoped> .father { display: flex; background-color: rosybrown; padding: 20px; border-radius: 10px; justify-content: space-evenly; width: 100%; } img, video { width: 100%; } </style>
Child.vue 如下:
<template> <div class="category"> <slot></slot> </div> </template> <script lang="ts"> export default { name: 'Category' } </script> <script setup lang="ts"> </script> <style scoped> .category { background-color: skyblue; border-radius: 10px; box-shadow: 0 0 10px; padding: 10px; width: 200px; height: 300px; } </style>
33. 组件通信_slot具名插槽
具名插槽意思就是具有名字的插槽,这里默认插槽的名字为default,多个slot可以倒序;
Category.vue
<template> <div class="category"> <slot name="s1"></slot> <slot name="s2"></slot> </div> </template> <script lang="ts"> export default { name: 'Category' } </script> <script setup lang="ts"> </script> <style scoped> .category { background-color: skyblue; border-radius: 10px; box-shadow: 0 0 10px; padding: 10px; width: 200px; height: 300px; } </style>
Father.vue
<template> <div class="father"> <Category> <template #s1> <h2> 热门游戏列表 </h2> </template> <template v-slot:s2> <ul> <li v-for="game in games" :key="game.id">{{ game.name }}</li> </ul> </template> </Category> <Category> <template #s2> <img :src="imgUrl" alt=""> </template> <template #s1> <h2> 热门美食列表 </h2> </template> </Category> <Category> <template #s2> <video :src="videoUrl" controls> </video> </template> <template #s1> <h2> 热门美食列表 </h2> </template> </Category> </div> </template> <script lang="ts"> export default { name: 'Slot' } </script> <script setup lang="ts"> import Category from './Category.vue'; import {ref, reactive} from 'vue' let games = reactive([ {id: 'zxzczxczxc01', name: '爱谁谁大所多'}, {id: 'zxzczxczxc02', name: '爱阿萨德按所多'}, {id: 'zxzczxczxc03', name: '爱自行车在线所多'}, {id: 'zxzczxczxc04', name: '爱谁周传雄所多'}, ]) let imgUrl = ref('https://z1.ax1x.com/2023/11/19/piNxLo4.jpg') let videoUrl = ref('http://vjs.zencdn.net/v/oceans.mp4') </script> <style scoped> .father { display: flex; background-color: rosybrown; padding: 20px; border-radius: 10px; justify-content: space-evenly; width: 100%; } img, video { width: 100%; } </style>
34. 组件通信_slot作用域插槽
主要用在子里面有数据,但是父组件想以不同形式进行表示;
Game.vue
<template> <div class="game"> <h2>游戏列表</h2> <slot :youxi="games"></slot> </div> </template> <script lang="ts"> export default { name: 'Category' } </script> <script setup lang="ts"> import {reactive} from 'vue' let games = reactive([ {id: 'zxzczxczxc01', name: '爱谁谁大所多'}, {id: 'zxzczxczxc02', name: '爱阿萨德按所多'}, {id: 'zxzczxczxc03', name: '爱自行车在线所多'}, {id: 'zxzczxczxc04', name: '爱谁周传雄所多'}, ]) </script> <style scoped> .game { background-color: skyblue; border-radius: 10px; box-shadow: 0 0 10px; padding: 10px; width: 200px; height: 300px; } </style>
Father.vue
<template> <div class="father"> <Game> <!-- 子组件中slot传的props中所有的数据都在a的手里--> <template v-slot="{youxi}"> <ul> <li v-for="y in youxi" :key="y.id"> {{ y.name }}</li> </ul> </template> </Game> <Game> <!-- 子组件中slot传的props中所有的数据都在a的手里--> <template v-slot="{youxi}"> <ol> <li v-for="y in youxi" :key="y.id"> {{ y.name }}</li> </ol> </template> </Game> <Game> <!-- 子组件中slot传的props中所有的数据都在a的手里--> <template v-slot="{youxi}"> <h3 v-for="y in youxi" :key="y.id"> {{ y.name }}</h3> </template> </Game> </div> </template> <script lang="ts"> export default { name: 'Slot' } </script> <script setup lang="ts"> import Game from './Game.vue'; </script> <style scoped> .father { display: flex; background-color: rosybrown; padding: 20px; border-radius: 10px; justify-content: space-evenly; width: 100%; } img, video { width: 100%; } </style>
35. shallowRef 和 shallowReactive
36. readonly 和 shallowReadonly
37. toRaw 和 markRaw
38. customRef
customRef()
预期接收一个工厂函数作为参数,这个工厂函数接受 track
和 trigger
两个函数作为参数,并返回一个带有 get
和 set
方法的对象。
一般来说,track()
应该在 get()
方法中调用,而 trigger()
应该在 set()
中调用。然而事实上,你对何时调用、是否应该调用他们有完全的控制权。
示例 创建一个防抖 ref,即只在最近一次 set 调用后的一段固定间隔后再调用:
import { customRef } from 'vue' export function useDebouncedRef(value, delay = 200) { let timeout return customRef((track, trigger) => { return { // 数据被读取时,调用get get() { track() // 告诉vue数据value很重要,一旦value变化就去更新 return value }, // 数据被修改时,调用set set(newValue) { clearTimeout(timeout) timeout = setTimeout(() => { value = newValue trigger() // 通知一下vue数据value变化了 }, delay) } } }) }
39. Teleport
这个组件中有一个 按钮来触发打开模态框,和一个 class 名为 .modal
的
,它包含了模态框的内容和一个用来关闭的按钮。
当在初始 HTML 结构中使用这个组件时,会有一些潜在的问题:
position: fixed
能够相对于浏览器窗口放置有一个条件,那就是不能有任何祖先元素设置了transform
、perspective
或者filter
样式属性。也就是说如果我们想要用 CSStransform
为祖先节点设置动画,就会不小心破坏模态框的布局!
- 这个模态框的
z-index
受限于它的容器元素。如果有其他元素与重叠并有更高的
z-index
,则它会覆盖住我们的模态框。
提供了一个更简单的方式来解决此类问题,让我们不需要再顾虑 DOM 结构的问题。让我们用 改写一下 :
<button @click="open = true">Open Modal</button> <Teleport to="body"> <div v-if="open" class="modal"> <p>Hello from the modal!</p> <button @click="open = false">Close</button> </div> </Teleport>
40. Suspense
在这个组件树中有多个嵌套组件,要渲染出它们,首先得解析一些异步资源。如果没有 ,则它们每个都需要处理自己的加载、报错和完成状态。在最坏的情况下,我们可能会在页面上看到三个旋转的加载态,在不同的时间显示出内容。
有了 组件后,我们就可以在等待整个多层级组件树中的各个异步依赖获取结果时,在顶层展示出加载中或加载失败的状态。
可以等待的异步依赖有两种:
- 带有异步
setup()
钩子的组件。这也包含了使用 时有顶层await
表达式的组件。 - 异步组件。
<Suspense> <!-- 具有深层异步依赖的组件 --> <Dashboard /> <!-- 在 #fallback 插槽中显示 “正在加载中” --> <template #fallback> Loading... </template> </Suspense>
41. 全局api转化为应用对象
import {createApp} from 'vue' import App from './App.vue' import Hello from './Hello.vue' //创建应用 const app = createApp(App) // 定义全局组件 app.component('Hello',Hello) // 定义全局属性 app.config.globalProperties.x = 99 declare module 'vue'{ interface ComponentCustomProperties{ x: number } } // 定义样式 对组件使用v-beauty改变样式 app.directive('beauty',(element,{value})=>{ element.innerText += value element.style.color = 'green' element.style.backgroundColor = 'yellow' }) // app挂载 app.mount('#app') // app卸载 setTimeout(()=>{ app.unmount() }, 3000)
42. 其他