四、vue3的v-model、组件传值、异步组件、teleport组件

1.v-model

Vue3可以通过v-model指令实现对多个数据的双向绑定

<!-- 父级 -->
<template>
 <Five v-model:bg="bg" v-model:mb="mb" v-model:nn="nn" @click="appClick"></Five>
</template>
<script>
import { ref } from 'vue'
import Five from './components/Five.vue'
export default {
  name: 'App',
  components: {
    Five
  },
  setup() {
    //饼干
    let bg = ref(5)
    //面包
    let mb = ref(8)
    //牛奶
    let nn = ref(3)
    let appClick = (e)=>{
      alert(e)
    }
    return{
      bg,
      mb,
      nn,
      appClick
    }
  }
}
</script>
<!-- 子级 -->
<template>
  <div class="five">
    <h3>Five</h3>
    <div class="item">
      饼干:<button @click="bg--">-</button><input readonly type="text" :value="bg"><button @click="bg++">+</button>
    </div>
    <div class="item">
      面包:<button @click="mb--">-</button><input readonly type="text" :value="mb"><button @click="mb++">+</button>
    </div>
    <div class="item">
      牛奶:<button @click="nn--">-</button><input readonly type="text" :value="nn"><button @click="nn++">+</button>
    </div>
    <button @click="fiveClick">点击</button>
  </div>
</template>
<script>import { ref, watch } from "vue";
export default {
  // 接收属性
  props:['bg','mb','nn'],
  // 放在自定义事件名称跟原生事件名称同名
  // 这里统一注册自定义事件的名称,
  // 这样父组件只会触发子组件自定义的事件,不会触发原生事件
  emits:['click','update:bg','update:mb','update:nn'],
  setup(props,{emit}) {
    //获取父组件的传值
    let bg = ref(props.bg)
    let mb = ref(props.mb)
    let nn = ref(props.nn)
    watch(bg,(val)=>{
      // 注意:事件名称是update:属性名称
      emit('update:bg',val)
    })
    watch(mb,(val)=>{
      emit('update:mb',val)
    })
    watch(nn,(val)=>{
      emit('update:nn',val)
    })
    let fiveClick = ()=>{
      // 当我们的自定义事件,跟原生事件重名时,会执行两次
      emit('click','哈哈!我被点击了!')
    }
    return {
      bg,
      mb,
      nn,
      fiveClick
    }
  }
};

2.组件传值

1.父子组件传值

父级

<template>
  <div class="app">
    <div class="box">
      <div>汽车名称:{{carName}}</div>
      <div>汽车价格:{{carPrice}}</div>
      <div>汽车颜色:{{carColor}}</div>
      <div>汽车产地:{{carAddress}}</div>
      <!-- 通过props给子组件传值 -->
      <Three :car-name="carName" :car-price="carPrice" 
      :car-color="carColor" :car-address="carAddress"
      @change-name="changeName" @change-price="changePrice"
      @change-color="changeColor" @change-address="changeAddress"></Three>
    </div>
  </div>
</template>
<script>
import Three from './components/Three.vue'
import { ref } from 'vue'
export default {
  name: 'App',
  components: {
    Three
  },
  setup() {
    let carName = ref('奔驰')
    let carPrice = ref(20)
    let carColor = ref('红色')
    let carAddress = ref('德国')

    let changeName = (e)=>{
      carName.value = e
    }
    let changePrice = (e)=>{
      carPrice.value = e
    }
    let changeColor = (e)=>{
      carColor.value = e
    }
    let changeAddress = (e)=>{
      carAddress.value = e
    }
    return{
      carName,
      carPrice,
      carColor,
      carAddress,
      changeName,
      changePrice,
      changeColor,
      changeAddress
    }
  }
}
</script>

子级

<template>
  <div class="three">
    <h3>子组件</h3>
    <!-- 注意:props接过来的属性,在模板中可以直接修改 -->
    <div>汽车名称:{{ myCarName }}<button @click="setCarName">修改车名</button></div>
    <div>汽车价格:{{ myCarPrice }}<button @click="setCarPrice">修改价格</button></div>
    <div>汽车颜色:{{ myCarColor }}<button @click="setCarColor">修改颜色</button></div>
    <div>汽车产地:{{ myCarAddress }}<button @click="setCarAddress">修改地址</button></div>
  </div>
</template>
<script>
import { toRef } from "vue";
export default {
  // props接过来的属性在方法里面是只读的
  props: ["carName", "carPrice", "carColor"],
  // 使用vue3中的组合式API写法
  setup(props,context) {
    // props返回的是一个Proxy对象,里面保存的是props选项接的属性
    // context对象里面保存了三个重要属性:attrs,emit,slots
    // attrs 相当于 this.$arrts
    // emit 相当于 this.$emit
    // slots 是插槽信息
    let myCarName = toRef(props,'carName')   //ref(props.carName)
    let myCarPrice = toRef(props,'carPrice')
    let myCarColor = toRef(props,'carColor')
    let myCarAddress = toRef(context.attrs,'car-address')
    
    let setCarName = ()=>{
      myCarName = '宝马'
      context.emit('change-name',myCarName)
    }
    let setCarPrice = ()=>{
      myCarPrice = 30
      context.emit('change-price',myCarPrice)
    }
    let setCarColor = ()=>{
      myCarColor = '蓝色'
      context.emit('change-color',myCarColor)
    }
    let setCarAddress = ()=>{
      myCarAddress = '美国'
      context.emit('change-address',myCarAddress)
    }

    return{
      myCarName,
      myCarPrice,
      myCarColor,
      myCarAddress,
      setCarName,
      setCarPrice,
      setCarColor,
      setCarAddress
    }
  }
};
</script>

2.隔代组件传值

父级

setup() {
    //定义数据
    let name = ref('张三')
    let age = ref(20)
    //汽车数据
    let car = reactive({
      name:'大众',
      color:'黑色'
    })
    //修改汽车的名称
    let updateCarName = (val)=>{
      car.name = val
    }
    //修改汽车的价格
    let updateCarColor = (val)=>{
      car.color = val
    }
    // 定义依赖数据
    // provide方法的第一个参数是key,第二个参数是value
    provide('name',name)
    provide('age',age)
    // 将两个修汽车信息的方法,作为依赖数据传出去
    provide('updateCarName',updateCarName)
    provide('updateCarColor',updateCarColor)
    return{
      name,
      age,
      car,
    }
  }

子级

<template>
  <div class="two">
    <h3>Two</h3>
    <div>姓名:{{ myName }}</div>
    <div>年龄:{{ myAge }}</div>
    <button @click="updateName">修改姓名</button>
    <button @click="updateAge">修改年龄</button>
    <button @click="updateCarName('比亚迪')">修改汽车名称</button>
    <button @click="updateCarColor('黄色')">修改汽车颜色</button>
  </div>
</template>
<script>
import { inject } from "vue";
export default {
  setup() {
    // 通过inject注入父级的依赖数据集
    let myName = inject("name");
    let myAge = inject("age");
    let updateName = () => {
      myName.value = "李四";
    };
    let updateAge = () => {
      myAge.value = "王五";
    };

    // 通过inject注入两个方法
    let updateCarName = inject("updateCarName");
    let updateCarColor = inject("updateCarColor");

    return {
      myName,
      myAge,
      updateName,
      updateAge,
      updateCarName,
      updateCarColor,
    };
  },
};
</script>

3.兄弟组件传值

1.下载mitt :npm i mitt

2.main.js中导入mitt

import { createApp } from 'vue'
import App from './App.vue'

//导入插件mitt(第三方中央事件中线库)
import mitt from "mitt"

//创建一个Vue实例
let app = createApp(App)

//挂载事务总线为全局属性
// config.globalProperties 就相当于返回Vue的原型对象
app.config.globalProperties.$bus = new mitt()

//将Vue实例挂载到#app容器中
app.mount('#app')

3. 监听事件

<script>
// getCurrentInstance方法,用于在setup函数中获取当前组件实例
import {ref,getCurrentInstance} from 'vue'
export default {
  // vue3推崇的写法
  setup() {
    // 获取当前组件实例 不等同于this
    let $this = getCurrentInstance()
    // $this.appContext 获取当前项目中的Vue实例
    let dogName = ref('琪琪')
    let dogSex = ref('女生')
    
    //监听事件
    $this.appContext.config.globalProperties.$bus.on('update-dog-name',(e)=>{
      dogName.value = e
    })
    $this.appContext.config.globalProperties.$bus.on('update-dog-sex',(e)=>{
      dogSex.value = e
    })
    return {
      dogName,
      dogSex
    }
  }
}
</script>

4.触发事件 

<template>
  <div class="four">
    <h3>Four</h3>
    <button @click="updateDogName">修改狗狗昵称</button>
    <button @click="updateDogSex">修改狗狗性别</button>
  </div>
</template>
<script>
import {getCurrentInstance} from 'vue'
export default {
  setup() {
    //获取当前组件实例
    let $this = getCurrentInstance()
    let updateDogName = ()=>{
      $this.appContext.config.globalProperties.$bus.emit('update-dog-name','天天')
    }
    let updateDogSex = ()=>{
      $this.appContext.config.globalProperties.$bus.emit('update-dog-sex','男生')
    }
    return {
      updateDogName,
      updateDogSex
    }
  }
};
</script>

3.异步组件

<template>
  <div class="home">
    <h1>首页</h1>
    <One></One>
    <!-- suspense内置组件,用于加载异步组件,添加loading效果 -->
    <suspense>
      <!-- default插槽里面放置异步组件 -->
      <template #default>
        <Two></Two>
      </template>
      <!-- fallback插槽里面制作loading效果 -->
      <template #fallback>
        Loading...
      </template>
    </suspense>
    <!-- 异步组件,可以不用suspense,
    但是如果异步组件内部的setup函数返回的是一个Promise对象,
    此时的异步组件就必须要使用suspense -->
    <suspense>
      <template #default>
        <Three></Three>
      </template>
      <template #fallback>
        Loading...
      </template>
    </suspense>
    <!-- <Three></Three> -->
  </div>
</template>

<script>
// defineAsyncComponent方法,用于异步加载组件
import {defineAsyncComponent} from 'vue'
import One from '../components/One.vue'
// import Two from './components/Two.vue'
// 异步导入Two组件和Three
let Two = defineAsyncComponent(()=>import('../components/Two.vue'))
// 注意:如果组件中setup方法返回的是Promise对象,那么组件必须要采用异步的方式加载
// import Three from './components/Three.vue'
let Three = defineAsyncComponent(()=>import('../components/Three.vue'))
export default {
  name: 'App',
  components: {
    One,
    Two,
    Three
  }
}
</script>

4.teleport组件

    <button @click="show=true">显示</button>
    <!-- teleport组件:官方起的名称:瞬移。通过to属性确定里面的元素移动到哪 -->
    <teleport to="body">
      <div v-show="show" class="box">
        <button @click="show=false">关闭</button>
        <div>{{dog}}</div>
      </div>
    </teleport>
<think>嗯,用户想了解如何在Vue3中创建全局弹窗组件。首先,我需要回忆一下Vue3的全局组件注册方法。记得在Vue3中,是通过createApp方法创建的app实例来使用component方法注册全局组件的。所以步骤应该是先创建一个弹窗组件,比如叫MyModal.vue,然后在main.js或者入口文件中导入并使用app.component注册它,这样在任何地方都可以直接使用而不需要局部注册。 不过,弹窗通常不仅仅是显示静态内容,还需要能够控制显示和隐藏。所以组件内部可能需要使用v-model或者props来接收一个visible的状态,并且在组件内部有关闭按钮来触发关闭事件。比如,组件里有一个v-if或者v-show绑定到isVisible这个变量,然后当点击关闭按钮或者遮罩层时,emit一个事件来更新这个变量。 另外,用户可能希望弹窗是全局的,可以通过方法调用,比如this.$modal.open()这样的形式来打开弹窗。这时候就需要用到Vue的插件机制或者全局属性。可以在注册组件的同时,添加一个全局方法。比如,使用app.config.globalProperties.$modal来挂载一个包含open和close方法的对象。 不过需要注意的是,在Vue3中,组合式API更推荐使用provide/inject或者直接在组件模板中使用,但全局方法仍然可以通过globalProperties添加。不过,如果使用TypeScript,可能会遇到类型问题,需要扩展ComponentCustomProperties接口。 还有一种情况,用户可能希望弹窗的内容可以动态插入,这时候可能需要用到Teleport组件,将弹窗的DOM渲染到body下面,避免父组件的样式影响。所以在弹窗组件里,可以用<Teleport to="body">包裹弹窗的内容,确保它正确挂载到body上。 另外,样式方面,弹窗通常需要固定定位,覆盖全屏的半透明背景,以及居中的内容区域。需要确保样式正确,比如position: fixed,top:0,left:0,width和height都是100%,背景用半透明的颜色,内容区域用flex或者transform居中。 测试的时候,可能会遇到z-index的问题,弹窗可能被其他元素遮挡,所以要确保弹窗的z-index足够高。还有,点击遮罩层关闭弹窗的功能,可以通过@click事件绑定到遮罩层元素,但要注意事件冒泡的问题,比如如果点击内容区域内部,不应该触发关闭,所以需要判断event.target是否是遮罩层本身。 最后,全局注册组件和使用Teleport可能需要一些注意事项,比如确保Teleport的目标元素存在,或者处理SSR的情况。不过一般情况下,在客户端渲染中,直接挂载到body是可以的。 总结步骤应该是:创建弹窗组件,处理显示隐藏的逻辑和样式,全局注册组件,添加全局方法调用,使用Teleport优化DOM结构,最后在需要的地方通过模板或者方法调用使用弹窗。</think>在 Vue 3 中创建全局弹窗组件可以通过以下步骤实现,这里以最简单的模态框为例: --- ### 一、创建弹窗组件 ```vue <!-- src/components/GlobalModal.vue --> <template> <Teleport to="body"> <div v-if="isVisible" class="modal-mask"> <div class="modal-container"> <div class="modal-header"> <slot name="header"> <h3>{{ title }}</h3> <button @click="close">×</button> </slot> </div> <div class="modal-content"> <slot></slot> </div> </div> </div> </Teleport> </template> <script setup> import { ref } from &#39;vue&#39; const isVisible = ref(false) const title = ref(&#39;默认标题&#39;) const open = (newTitle) => { title.value = newTitle || &#39;默认标题&#39; isVisible.value = true } const close = () => { isVisible.value = false } // 暴露方法给父组件 defineExpose({ open, close }) </script> <style scoped> .modal-mask { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 999; } .modal-container { background: white; padding: 20px; border-radius: 8px; min-width: 300px; } </style> ``` --- ### 二、全局注册组件 ```javascript // main.js import { createApp } from &#39;vue&#39; import App from &#39;./App.vue&#39; import GlobalModal from &#39;./components/GlobalModal.vue&#39; const app = createApp(App) // 注册全局组件 app.component(&#39;GlobalModal&#39;, GlobalModal) // 添加全局方法(可选) app.config.globalProperties.$modal = { open: (title) => { const modal = document.querySelector(&#39;#global-modal&#39;) if (modal) modal.__vue__.open(title) }, close: () => { const modal = document.querySelector(&#39;#global-modal&#39;) if (modal) modal.__vue__.close() } } app.mount(&#39;#app&#39;) ``` --- ### 三、在根组件放置挂载点 ```vue <!-- App.vue --> <template> <div id="app"> <router-view /> <GlobalModal id="global-modal" /> </div> </template> ``` -- ### 、使用方式 #### 方式 1:直接调用全局方法(需在 setup 外) ```vue <script> export default { methods: { showModal() { this.$modal.open(&#39;自定义标题&#39;) } } } </script> ``` #### 方式 2:组合式 API 调用 ```vue <script setup> import { getCurrentInstance } from &#39;vue&#39; const { proxy } = getCurrentInstance() const showModal = () => { proxy.$modal.open(&#39;组合式标题&#39;) } </script> ``` #### 方式 3:模板直接使用 ```vue <template> <GlobalModal> <template #header> <h2>自定义标题</h2> </template> 这里是弹窗内容 </GlobalModal> </template> ``` --- ### 五、关键点说明 1. **Teleport 组件**:将弹窗内容渲染到 `<body>` 末尾,避免父组件样式影响 2. **全局方法注册**:通过 `app.config.globalProperties` 添加全局调用方式 3. **组合式 API**:使用 `defineExpose` 暴露组件方法 4. **CSS 定位**:固定定位(fixed)确保全屏覆盖 5. **插槽机制**:支持内容定制化 --- ### 六、进阶优化方向 1. **动画效果**:添加 `<Transition>` 组件实现淡入淡出 2. **Promise 封装**:支持 `.open().then()` 的异步调用方式 3. **多弹窗管理**:使用服务层维护弹窗堆栈 4. **TypeScript 支持**:添加类型定义 5. **键盘事件**:ESC 键关闭支持 按需选择优化方向即可实现更强大的全局弹窗系统。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值