# 前言
尤雨溪懂个锤子 vue,我要教他开发 vue4
# 兄弟关系通信
setup
是所有组合式 api 的序幕,其中 this 不再是组件的实例对象而是 undefined
$on
, $off
和 $once
实例方法已被移除,应用实例不再实现事件触发接口
我们使用消息订阅与发布仅仅只有 27kb 的第三方库 mitt
npm install mitt -S |
新建 src/untils/bus.js
暴露在组件中引用
import mitt from 'mitt' | |
export default mitt() |
或者在 main.js 全局挂载
import { createApp } from 'vue' | |
import App from './App.vue' | |
import mitt from 'mitt' | |
const app = createApp(App) | |
app.config.globalProperties.$bus = new mitt() | |
app.mount('#app') |
参考代码
兄弟组件
<button @click="send">发送消息</button> | |
<script> | |
import { getCurrentInstance } from 'vue' | |
// import event from '../untils/bus' | |
export default { | |
name: 'Index', | |
setup() { | |
let that = getCurrentInstance().proxy.$bus | |
function send() { | |
that.emit('hello', { name: '我是index传过来的对象' }) | |
} | |
return { send } | |
} | |
} | |
</script> |
<div></div> | |
<script> | |
import { getCurrentInstance, reactive, toRefs } from 'vue' | |
// import event from '../untils/bus' | |
export default { | |
name: 'Brother', | |
setup() { | |
let that = getCurrentInstance().proxy.$bus | |
let data = reactive({}) | |
that.on('hello', (p) => { | |
data.name = p | |
}) | |
return { | |
data, | |
...toRefs(data) | |
} | |
} | |
} | |
</script> |
原生内置自定义事件
/** 组件 A */ | |
document.addEventListener('自定义事件', ev => console.log(ev)); | |
// CustomEvent {isTrusted: false, detail: "我是 payload", type: "自定义事件", target: document, currentTarget: document, …} | |
/** 组件 B */ | |
document.dispatchEvent(new CustomEvent('自定义事件', { detail: '我是 payload' })); |
# 其他关系通信
准备了一个案例,使用到的知识点有父子组件、祖孙组件和子父组件的通信还有异步组件的部分使用
为了案例而案例,写法不一定很好,但表达出来了意思,类似与骨架屏的效果
参考图片
1.数据在Index组件中,通过子传父到App组件 | |
2.数据到App组件中,通过祖传孙到Son组件 | |
3.数据到Son组件中,通过父传子到Child组件 | |
│ App.vue | |
│ main.js | |
├─assets | |
├─components | |
│ Child.vue | |
│ Index.vue | |
│ Son.vue | |
└─untils | |
bus.js |
<template> | |
<Son /> | |
</template> | |
<script> | |
import Son from '../components/Son.vue' | |
import { reactive, toRefs } from '@vue/reactivity' | |
export default { | |
name: 'Index', | |
components: { Son }, | |
emits: ['sendToIndex'], | |
setup(props, context) { | |
let data = reactive({ | |
todo: [ | |
{ id: '1', name: '学习' }, | |
{ id: '2', name: '吃饭' }, | |
{ id: '3', name: '睡觉' } | |
] | |
}) | |
context.emit('sendToIndex', data.todo) | |
return { | |
...toRefs(data) | |
} | |
} | |
} | |
</script> |
<template> | |
<Index @sendToIndex="sendToIndex"></Index> | |
</template> | |
<script> | |
import { reactive, provide, watch } from 'vue' | |
import Index from './components/Index.vue' | |
export default { | |
name: 'App', | |
components: { | |
Index | |
}, | |
setup(props, context) { | |
let data = reactive({}) | |
function sendToIndex(value) { | |
data.todo = value | |
} | |
watch( | |
() => data.todo, | |
(n, o) => { | |
provide('todo', n) | |
} | |
) | |
return { | |
sendToIndex, | |
data | |
} | |
} | |
} | |
</script> | |
<style> | |
* { | |
margin: 0; | |
padding: 0; | |
} | |
html, | |
body, | |
#app { | |
height: 100%; | |
width: 100%; | |
} | |
::-webkit-scrollbar { | |
display: none; | |
} | |
</style> |
<template> | |
<div class="error" v-if="error"></div> | |
<Suspense v-else> | |
<template v-slot:default> | |
<Child :todo="todo" /> | |
</template> | |
<template v-slot:fallback> | |
<h3 class="gray">加载中.....</h3> | |
</template> | |
</Suspense> | |
</template> | |
<script> | |
import { inject, defineAsyncComponent, onErrorCaptured, ref } from 'vue' | |
const Child = defineAsyncComponent(() => import('./Child.vue')) | |
// import Child from './Child.vue' | |
export default { | |
components: { | |
Child | |
}, | |
setup() { | |
const error = ref(null) | |
onErrorCaptured((e) => { | |
error.value = e | |
return true | |
}) | |
const todo = inject('todo') | |
return { todo, error } | |
} | |
} | |
</script> | |
<style scoped> | |
.gray { | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
font-size: 70px; | |
color: skyblue; | |
height: 100%; | |
width: 100%; | |
background: gray; | |
} | |
.error { | |
background: crimson; | |
font-size: 60px; | |
background: darkkhaki; | |
height: 100%; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
color: red; | |
} | |
</style> |
<template> | |
<ul> | |
<li v-for="item in newTodo" :key="item.id">--</li> | |
</ul> | |
</template> | |
<script> | |
export default { | |
props: ['todo'], | |
async setup(props) { | |
const getData = (_) => { | |
return new Promise((resolve, reject) => { | |
setTimeout(() => { | |
resolve(_) | |
//reject (' 获取数据失败 ') | |
}, 3000) | |
}) | |
} | |
const newTodo = await getData(props.todo) | |
return { newTodo } | |
} | |
} | |
</script> | |
<style scoped> | |
ul { | |
height: 100%; | |
background: aliceblue; | |
display: FLEX; | |
flex-direction: column; | |
justify-content: center; | |
align-items: center; | |
font-size: 50px; | |
list-style: none; | |
} | |
</style> |
# model 对话框
这个案例来做一个对话框,复刻 Ant Design of Vue
参考图片
Dialog组件就是我们的对话框,在App组件中调用 | |
│ App.vue | |
│ main.js | |
├─assets | |
│ logo.png | |
├─components | |
│ Dialog.vue | |
└─untils | |
bus.js |
<template> | |
<div style="height:4000px"> | |
<button @click="visible = !visible">点我编辑</button> | |
<Dialog v-model:visible="visible" :title="title"> | |
<template v-slot:model-body> | |
<p>Some contents...</p> | |
<p>Some contents...</p> | |
<p>Some contents...</p> | |
</template> | |
</Dialog> | |
</div> | |
</template> | |
<script> | |
import { ref } from 'vue' | |
import Dialog from './components/Dialog.vue' | |
export default { | |
name: 'App', | |
components: { | |
Dialog | |
}, | |
setup() { | |
let title = ref('Basic Modal') | |
let visible = ref(false) | |
return { | |
title, | |
visible | |
} | |
} | |
} | |
</script> | |
<style> | |
::-webkit-scrollbar { | |
display: none; | |
} | |
</style> |
<template> | |
<teleport to="body"> | |
<div v-if="visible" class="mask" @click.self="close"> | |
<div class="dialog"> | |
<div class="close" @click="close"> | |
<svg viewBox="64 64 896 896" data-icon="close" width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false" class=""> | |
<path | |
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 0 0 203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z" | |
></path> | |
</svg> | |
</div> | |
<div class="head"> | |
Vue3 组件通信和新的组件 | |
</div> | |
<div class="body"> | |
<slot name="model-body"></slot> | |
</div> | |
<div class="footer"> | |
<button @click="close" class="btn">取 消</button> | |
<button @click="close" class="btn btn-primary" style="margin-left: 8px;">确 定</button> | |
</div> | |
</div> | |
</div> | |
</teleport> | |
</template> | |
<script> | |
export default { | |
name: 'Dialog', | |
props: ['visible', 'title'], | |
emits: ['edit', 'update:visible'], | |
setup(props, context) { | |
function close() { | |
context.emit('update:visible', !props.visible) | |
} | |
return { | |
close | |
} | |
} | |
} | |
</script> | |
<style> | |
.mask { | |
position: fixed; | |
top: 0; | |
right: 0; | |
bottom: 0; | |
left: 0; | |
z-index: 1000; | |
overflow: auto; | |
background-color: rgba(0, 0, 0, 0.5); | |
} | |
.dialog { | |
position: absolute; | |
left: 50%; | |
transform: translate(-50%, 0); | |
top: 100px; | |
margin: 0 auto; | |
width: 520px; | |
transform-origin: 436px 483px; | |
background-color: #fff; | |
background-clip: padding-box; | |
border: 0; | |
border-radius: 4px; | |
box-shadow: 0 4px 12px rgb(0 0 0 / 15%); | |
pointer-events: auto; | |
} | |
.close { | |
position: absolute; | |
right: 0; | |
cursor: pointer; | |
color: rgba(0, 0, 0, 0.45); | |
height: 42px; | |
display: flex; | |
align-items: center; | |
width: 42px; | |
justify-content: center; | |
transition: all 0.3s; | |
z-index: 222; | |
} | |
.close:hover { | |
color: rgba(0, 0, 0, 0.75); | |
} | |
.head { | |
text-align: left; | |
padding: 10px 0; | |
text-indent: 20px; | |
font-weight: 600; | |
border-bottom: 1px solid #e8e8e8; | |
color: rgba(0, 0, 0, 0.85); | |
} | |
.body { | |
padding: 0 20px; | |
} | |
.footer { | |
padding: 10px 16px; | |
text-align: right; | |
background: transparent; | |
border-top: 1px solid #e8e8e8; | |
border-radius: 0 0 4px 4px; | |
} | |
.btn { | |
line-height: 1.499; | |
position: relative; | |
display: inline-block; | |
font-weight: 400; | |
white-space: nowrap; | |
text-align: center; | |
background-image: none; | |
box-shadow: 0 2px 0 rgb(0 0 0 / 2%); | |
cursor: pointer; | |
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); | |
user-select: none; | |
touch-action: manipulation; | |
height: 32px; | |
padding: 0 15px; | |
font-size: 14px; | |
border-radius: 4px; | |
color: rgba(0, 0, 0, 0.65); | |
background-color: #fff; | |
border: 1px solid #d9d9d9; | |
} | |
.btn-primary { | |
color: #fff; | |
background-color: #1890ff; | |
border-color: #1890ff; | |
text-shadow: 0 -1px 0 rgb(0 0 0 / 12%); | |
box-shadow: 0 2px 0 rgb(0 0 0 / 5%); | |
} | |
.btn:focus, | |
.btn:hover { | |
color: #40a9ff; | |
background-color: #fff; | |
border-color: #40a9ff; | |
} | |
.btn-primary:focus, | |
.btn-primary:hover { | |
color: #fff; | |
background-color: #40a9ff; | |
border-color: #40a9ff; | |
} | |
</style> |
# 小结
set up 的参数
props:
值为对象,包含:组件外部传递过来,且组件内部声明接收了的属性。context:
上下文对象attrs:
值为对象,包含:组件外部传递过来,但没有在 props 配置中声明的属性,相当于this.$attrs
slots:
收到的插槽内容,相当于this.$slots
emit:
分发自定义事件的函数,相当于this.$emit
# 自定义属性
父组件
<template> | |
<Son :todo="todo" /> | |
</template> | |
<script> | |
import { reactive } from 'vue' | |
import Son from './Son.vue' | |
export default { | |
components: { | |
Son | |
}, | |
setup() { | |
const todo = reactive({ | |
todo: [ | |
{ id: '1', name: '学习' }, | |
{ id: '2', name: '吃饭' }, | |
{ id: '3', name: '睡觉' } | |
] | |
}) | |
return { todo } | |
} | |
} | |
</script> |
子组件
<template> | |
</template> | |
<script> | |
export default { | |
props: ['todo'], | |
setup(props,context) { | |
return { } | |
} | |
} | |
</script> |
# 自定义事件
子组件
<template> | |
</template> | |
<script> | |
import { reactive, toRefs } from '@vue/reactivity' | |
export default { | |
name: 'Index', | |
emits: ['sendToIndex'], | |
setup(props, context) { | |
let data = reactive({ | |
todo: [ | |
{ id: '1', name: '学习' }, | |
{ id: '2', name: '吃饭' }, | |
{ id: '3', name: '睡觉' } | |
] | |
}) | |
context.emit('sendToIndex', data.todo) | |
return { | |
...toRefs(data) | |
} | |
} | |
} | |
</script> |
父组件
<Index @sendToIndex="sendToIndex" /> | |
<script> | |
import Index from './components/Index.vue' | |
export default { | |
name: 'App', | |
components: { | |
Index | |
}, | |
setup() { | |
function sendToIndex(value) { | |
console.log(value); | |
} | |
return { | |
sendToIndex, | |
} | |
} | |
} | |
</script> |
# provide 与 inject
实现祖与后代组件间通信
父组件有一个 provide
选项来提供数据,后代组件有一个 inject
选项来开始使用这些数据
参考图片
祖组件
setup(){ | |
let car = reactive({name:'奔驰',price:'40万'}) | |
provide('car',car) | |
} |
后代组件
setup(){ | |
const car = inject('car') | |
return {car} | |
} |
# Suspense 组件
Suspense:
等待异步组件时渲染一些额外内容,让应用有更好的用户体验
<template> | |
<Suspense> | |
<template #default> | |
<!-- 多个异步组件控制台如果警告可以用个根节点包住 --> | |
<!-- Child 组件 set up 应该返回一个 Promise 对象或者使用 async/await --> | |
<Child /> | |
</template> | |
<template #fallback> | |
<h3>加载中.....</h3> | |
</template> | |
</Suspense> | |
</template> |
使用步骤:
- 异步引入组件
- 使用
Suspense
包裹组件,并配置好default
与fallback
- 先渲染后备内容直到默认内容准备就绪会切换显示我们的异步组件
# Teleport 组件
Teleport
是一种能够将我们的组件结构移动到指定位置的技术
<teleport to="移动位置"> | |
<div v-if="isShow" class="mask"> | |
<div class="dialog"> | |
<h3>我是一个弹窗</h3> | |
<button @click="isShow = false">关闭弹窗</button> | |
</div> | |
</div> | |
</teleport> |
# Fragment 组件
Vue3 组件可以没有根标签,内部会将多个标签包含在一个 Fragment 虚拟元素中
减少标签层级,减小内存占用