文章目录
前言
在做vue项目或者uniapp开发微信小程序时,经常会用到组件之间传值,因此想总结记录下。
一、父子传值(props / $emit 、ref / $refs)
1)props / $emit
父组件通过 props
向子组件传递数据,子组件通过 $emit
触发事件向父组件传递数据。
// 父组件中:
<template>
<child-component :message="message" @update-message="updateMessage"></child-component>
</template>
<script>
export default {
data() {
return {
message: 'Hello World'
}
},
methods: {
updateMessage(msg) {
this.message = msg
}
}
}
</script>
// 子组件中:
<template>
<div>
<p>{{ message }}</p>
<button @click="updateMessage">Update Message</button>
</div>
</template>
<script>
export default {
props: ['message'],
methods: {
updateMessage() {
this.$emit('update-message', 'New Message')
}
}
}
</script>
2)ref / $refs
a)ref 用在组件可以调用组件的属性方法
b)ref 用在标签可以对标签进行操作
c) ref属性不能用在uniapp的内置组件上面,只能用在自定义组件上面;用在内置组件比如<view>标签
,this.$refs.xxx获取dom是undefind
父组件想调用子组件的方法:通过 this.$refs(ref)
ref
:这个属性用在子组件上,他的引用就指向了子组件的实例。可以通过实例来访问组件的数据
和方法
。
// 父组件
<template>
<view>
<view class="index-goods-list-con">
<WaterfallList
ref="waterFallCon"
:status="waterfall.status"
:list="waterfall.list"
:reset="waterfall.reset"
@clickCard="onClickCard"
@handleGood="handleGood"
@done="onDone"
></WaterfallList>
</view>
</view>
</template>
<script>
import WaterfallList from '@/components/waterfall/waterfall-list.vue'
export default {
data() {
return {}
},
methods: {
handleDailyBestGood(e, good) {
// 用$refs赋值调用子组件的handleDailyBestNum方法
this.$refs.waterFallCon.handleDailyBestNum(good)
// 调用子组件的属性
this.$refs.waterFallCon.name // 铁锤妹妹
},
},
}
</script>
<style></style>
// 子组件
<template></template>
<script>
export default {
data() {
return {
renderBasic: '',
name: '铁锤妹妹'
}
},
methods: {
//父组件调用的方法
handleDailyBestNum(good) {
//good 是父组件弹窗传递过来的值,我们可以打印看看
console.log(good)
this.renderBasic = good
},
},
}
</script>
二、兄弟传值( $emit / $on )
借助中间代理, $emit
和 $on
比如在uniapp项目中使用兄弟传值, uniapp页面通讯官方文档,vue使用同理。
在Vue.js中,事件总线(Event Bus)是一种实现
发布-订阅模式
的机制。它允许不同组件之间进行解耦的通信。Vue的事件总线是一个空的 Vue 实例,作为中央事件处理器。
事件总线的事件中心就是这个空的Vue 实例,它充当了消息的中介。通过在组件中引入并使用事件总线,可以在任何组件中发布事件和订阅事件。
具体来说,事件总线使用$emit
方法发布事件,用$on
方法订阅事件。通过调用$emit
方法并指定事件名称,可以向事件总线发布一个事件;而通过调用$on
方法并指定事件名称和回调函数,可以订阅该事件并在事件发生时执行相应的回调函数。
a) 说明
b) 使用场景
在商品下单页点击选择优惠券,进入优惠券页面,选择一张适合的,再返回下单页,需要带着优惠券id返回,现在商品 提交订单页和优惠券页面是 跨页面
的。
c) 使用方法
//优惠券页面
clickCoupon (item) {
if (this.orderSource) {
// 触发全局自定义事件,传参id
uni.$emit('updateData', item.id)
uni.navigateBack({
delta: 1
})
}
},
// 从优惠券跳回下单页
// 监听全局自定义事件updateData,接收参数id
uni.$on('updateData', async function (couponId) {
that.couponId = couponId
})
d) 注意事项
uni.$emit
、uni.$on
、uni.$once
、uni.$off
触发的事件都是App 全局级别的,跨任意组件,页面,nvue,vue
等- 使用时,注意及时销毁事件监听,比如,页面 onLoad 里边
uni.$on
注册监听,onUnload 里边uni.$off
移除,或者一次性的事件,直接使用uni.$once
监听。 - 注意
uni.$on
定义完成后才能接收到uni.$emit
传递的数据
三、全局事件总线 - mitt库
mitt 是一个JavaScript 库,用于实现事件的订阅和发布。它体积小巧,压缩后只有200字节,且无需依赖其他库。
在vue3项目中可以这样使用:
1) main.ts 中导入 mitt 库,并配置全局属性 eventBus
import mitt from 'mitt';
async function start() {
const app = createApp(App)
app.mount('#app')
// globalProperties 是一个用于定义全局属性和方法的地方;
// eventBus 是一个自定义的全局属性,用来存储 mitt() 返回的事件总线实例。
app.config.globalProperties.eventBus = mitt()
}
start()
2)自定义 hook,用于获取当前组件实例的全局属性
utils文件夹下新建 useCurrentInstance.ts 文件,代码如下:
import { getCurrentInstance } from 'vue'
import type { ComponentInternalInstance } from 'vue'
export default function useCurrentInstance() {
if (!getCurrentInstance()) {
throw new Error('useCurrentInstance() can only be used inside setup() or functional components!')
}
const { appContext } = getCurrentInstance() as ComponentInternalInstance
// proxy 是从当前组件实例的全局属性中获取的,可以通过该属性访问在全局配置中定义的属性和方法
const proxy = appContext.config.globalProperties
return {
proxy,
}
}
3) Vue 3 组件中使用该 hook方法
<script setup lang="ts">
import { onBeforeMount, onUnmounted } from 'vue'
import useCurrentInstance from '/@/utils/useCurrentInstance'
const { proxy } = useCurrentInstance()
onBeforeMount(() => {
// 在其它组件中订阅事件,为了演示方便我写在了同一个组件中
proxy.eventBus.eventBus.on('eventName', eventData => {
console.log('Event received:', eventData);
});
// 在组件中触发事件
proxy.eventBus.emit('eventName', { message: 'Hello!' })
onUnmounted(() => {
// 及时移除掉
proxy.eventBus.off('eventName')
})
</script >
注意点
:尽管全局事件总线在简单应用中很方便,但随着应用规模的增长,可能会导致事件命名冲突和难以追踪的问题。建议在大型应用中考虑更为结构化和可预测的状态管理和事件传递方案,如
Vuex 或 Composition API 中的 provide 和 inject。
四、祖孙或者更深嵌套的组件间传值
1) provide / inject
a) 类型
provide:Object | () => Object
inject:Array<string> | { [key: string]: string | Symbol | Object }
b) 详细
这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。
provide 选项应该是一个对象或返回一个对象的函数。该对象包含可注入其子孙的 property。
inject 选项应该是: 一个字符串数组,或一个对象
提示:provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的。
c) 使用场景
如果要将祖先组件直接传递给孙子组件,我们要将props逐级传递下去:祖先组件
=》子组件
=》孙子组件
,而通过provide/inject,可直接从祖先组件传给孙子组件,即使再嵌套多层也没关系。
d) 使用方法
<template>
<div>
<button @click="changeMsg">祖组件触发</button>
<h1>祖组件</h1>
<parent></parent>
</div>
</template>
<script>
import parent from './parent.vue';
export default {
data(){
return{
obj:{
name:'JavaScript',
},
developer:'布兰登·艾奇',
year:1995,
update:'2021年06月',
}
},
provide(){
return {
obj: this.obj, // 方式1.传入一个可监听的对象
developerFn:() => this.developer, // 方式2.通过 computed 来计算注入的值
// developerFn: this.getDeveloper, 方式3.或者调用一个方法,也能实现响应式
year: this.year, // 方式4.直接传值
app: this, // 方式5. 提供祖先组件的实例 缺点:实例上挂载很多没有必要的东西 比如:props,methods。
}
},
components: {
parent,
},
methods:{
getShowCartValue() {
return this.developer
},
changeMsg(){
this.obj.name = 'Vue';
this.developer = '尤雨溪';
this.year = 2014;
this.update = '2021年6月7日';
},
},
}
</script>
子组件
<template>
<div class="wrap">
<h4>子组件(只做中转)</h4>
<child></child>
</div>
</template>
<script>
import child from './child.vue';
export default {
components:{
child,
},
}
</script>
孙组件
<template>
<div>
<h5>孙组件</h5>
<span>名称:{{obj.name}}</span> |
<span>作者:{{developer}}</span> |
<span>诞生于:{{year}}</span> |
<span>最后更新于:{{this.app.update}}</span>
</div>
</template>
<script>
export default {
computed:{
developer(){
return this.developerFn()
}
},
inject:['obj','developerFn','year','app'],
}
</script>
未点击按钮,原有状态
当点击按钮触发 changeMsg 方法后,效果如下:
对比一下前后差异:无论点击多少次,孙组件中的诞生于 year 字段永远都是1995 并不会发生变化,通过 方式1、方式2、方式3、方式5传值是可以响应的
。
正是官网所提到的:provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的。
注意:
1)另外如果孙子组件更改祖先组件传来的值,会发现祖先组件的值也会跟着变,所以慎用 provide / inject;
2)Vuex 和 provide/inject 最大的区别:Vuex 中的全局状态的每次修改是可以追踪回溯的,而 provide/inject 中变量的修改是无法控制的。换句话说,不知道是哪个组件修改了这个全局状态。
所以对于业务庞大而复杂的,还是建议使用vuex~
2) $attrs
多层嵌套组件传递数据时,如果只是传递数据,而不做中间处理的话就可以用这个,比如父组件向孙子组件传递数据时使用。
$attrs
: $attrs
对象包含了除组件所声明的 props
和 emits
之外的所有其他 attribute,例如 class
,style
,v-on
监听器等等。Vue2通过 this.$attrs
或者Vue3通过$attrs.props
获取父作用域中所有符合条件的属性集合,然后还要继续传给子组件内部的其他组件,就可以通过 v-bind="$attrs"
,进行多层组件的传值。
有几点需要注意:
- 和 props 有所不同,透传 attributes 在 JavaScript 中保留了它们原始的大小写,所以像 foo-bar 这样的一个 attribute 需要通过
$attrs['foo-bar']
来访问。- 像 @click 这样的一个 v-on 事件监听器将在此对象下被暴露为一个函数
$attrs.onClick
。
//Parent.vue
<template>
<child :name="name" age="18" ></child>
</template
export default{
data(){
return {
name:"oldCode"
}
}
}
//Child.vue
<template>
// 继续传给孙子组件
<sun-child v-bind="$attrs"></sun-child>
</template>
export default{
props:["name"], // 这里可以接收,也可以不接收
mounted(){
// 如果props接收了name 就是 { age:18 },否则就是{ name:"oldCode", age:18 }
console.log(this.$attrs)
}
}
五、更复杂的结构:vuex
请移步看我之前写的关于vuex使用总结文章,学习笔记之Vuex总结(Vue状态管理)