# 前言

尤雨溪懂个锤子 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>

使用步骤:

  1. 异步引入组件
  2. 使用 Suspense 包裹组件,并配置好 defaultfallback
  3. 先渲染后备内容直到默认内容准备就绪会切换显示我们的异步组件

# 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 虚拟元素中

减少标签层级,减小内存占用

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

山河 微信支付

微信支付

山河 支付宝

支付宝

山河 贝宝

贝宝