以组件之间的关系,主要分为3种情况
兄弟组件通信:以父级为媒介,就变成父子组件通信。或通过全局通信。
这是最基础也最常用的方式。
props
向子组件传递数据。$emit
调用父组件方法向父组件传参。需要说明2点:
1,不应该在子组件中改变 props,这会破坏单向数据流。如果有这样需求,可用 computed 做转换。
2,$emit
触发的是组件自己的事件,下面的代码中 @updateCount 也是属于子组件的事件,只不过事件处理函数是父组件的方法(this._events
可查看组件所有的事件)。
举例1
<template>
<Children :count="count" @updateCount="updateCount" />
template>
<script>
import Children from "./components/Children.vue";
export default {
components: {
Children,
},
data() {
return {
count: 0,
};
},
methods: {
updateCount(num) {
this.count = num;
},
},
};
script>
<template>
<div>
<div>{{ count }}div>
<button @click="handleClick">修改 countbutton>
div>
template>
<script>
export default {
props: ["count"],
methods: {
handleClick() {
this.$emit("updateCount", 10);
},
},
};
script>
子组件调用父组件方法时,需要等待父组件处理之后,再执行子组件其他逻辑。如何实现?
$emit()
可以传多个参数,所以也可以传递回调函数。实现如下:
<template>
<Children @updateCount="updateCount" />
template>
<script>
import Children from "./components/Children.vue";
// 模拟延迟
const delay = (duration) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve()
}, duration)
})
}
export default {
components: {
Children,
},
data() {
return {
count: 0,
};
},
methods: {
async updateCount(num, callback) {
this.count = num;
await delay(2000)
callback && callback()
},
},
};
script>
<template>
<button @click="handleClick">修改 countbutton>
template>
<script>
export default {
methods: {
handleClick() {
this.$emit("updateCount", 10, () => {
// 等待父组件调用该回调函数后,再执行子组件其他逻辑
// ...
});
},
},
};
script>
将父组件的方法作为参数传递(而不是事件),子组件中直接使用该属性即可。
<template>
<Children :updateCount="updateCount" />
template>
<script>
import Children from "./components/Children.vue";
// 模拟延迟
const delay = (duration) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, duration);
});
};
export default {
components: {
Children,
},
data() {
return {
count: 0,
};
},
methods: {
async updateCount(num) {
this.count = num;
await delay(2000);
},
},
};
script>
<template>
<button @click="handleClick">修改 countbutton>
template>
<script>
export default {
props: {
updateCount: Function,
},
methods: {
async handleClick() {
await this.updateCount(10);
// 等待父组件调用该回调函数后,再执行子组件其他逻辑
// ...
},
},
};
script>
这2个都是语法糖,可以实现数据的双向绑定。但本质上还是
props / $emit
。
参考-自定义事件
当在一个组件上使用 v-model
时, 默认传入名为 value
的 prop 和名为 input
的事件。
<template>
<Children v-model="count" />
template>
<script>
import Children from "./components/Children.vue";
export default {
components: {
Children,
},
data() {
return {
count: 0,
};
},
};
script>
<template>
<div>
<div>{{ value }}div>
<button @click="handleClick">修改 countbutton>
div>
template>
<script>
export default {
props: ["value"],
methods: {
handleClick() {
this.$emit("input", 123);
},
},
};
script>
可以使用 model
来修改 v-model
的默认设置。
<script>
export default {
model: {
prop: "value1",
event: "change",
},
props: ["value1"],
methods: {
handleClick() {
this.$emit('change', 123)
},
},
};
script>
参考 - .sync 修饰符
本质如下:
<template>
<Children :count="count" @update:count="(newValue) => (count = newValue)" />
template>
子组件中的处理和 v-model
类似,不做赘述。
$parent
和 $children
都可以访问组件的实例,所以可直接访问和修改属性。
组件的子组件可能有多个,所以
$children
是数组,如果组件没有子组件,则$children
是空数组。
子组件的父组件只有一个,所以$parent
直接就是组件实例,App.vue
的父组件是根组件实例$root
,在往上就是undefined
。
举例:
<template>
<div>
<Children :count="count" />
<button @click="changeChild">修改子组件的值button>
div>
template>
<script>
import Children from './components/Children.vue'
export default {
components: {
Children
},
data() {
return {
count: 0
}
},
methods: {
changeChild() {
this.$children[0].name = '下雪天的夏风'
}
}
}
script>
<template>
<div>
<div>{{ count }}div>
<div>{{ name }}div>
<button @click="changeParent">修改父组件的值button>
div>
template>
<script>
export default {
props: ['count'],
data() {
return {
name: '子组件'
}
},
methods: {
changeParent() {
this.$parent.count = 1
}
}
}
script>
1,称为模板引用,用于获取子组件的实例。在 v-for
中使用时,获取的是一个 ref 数组(注意不保证和源数组顺序相同)。
2,对普通元素使用时,获取的是DOM元素,比 document.querySelector()
更方便。
举例:
<template>
<div>
<Children ref="_refChild" />
<button @click="changeChild">改变子组件的值button>
div>
template>
<script>
import Children from './components/Children.vue'
export default {
components: {
Children
},
methods: {
changeChild() {
this.$refs._refChild.name = '下雪天的夏风'
}
}
}
script>
<template>
<div>{{ name }}div>
template>
<script>
export default {
data() {
return {
name: '子组件'
}
}
}
script>
下面是隔代通信
当组件嵌套多层时,使用上面的父子组件通信的方式,略显繁琐。
比如对 A <-B<-C 3个组件,A 是父组件。当C需要使用A组件的属性和方法时,一般处理情况:
对属性来说,C 中 prop
接收 B 的属性,B 又 prop
接收 A 的属性。
对方法来说,C 中 $emit()
B 的方法,B 又 $emit()
A 的方法。
B 只起了中转的作用,但却需要写重复的代码,属性和方法较多时都就更难受了。
所以出现了 $attrs / $listeners
如果子组件没有在 props 中接收父组件传递给它的属性(不包含 class 和 style 属性),则这些属性会放在 $attrs
中
包含了父组件传递过来了所有自定义事件 (不包含 .native 修饰器的)。
所以,在中间组件上可用$attrs / $listeners
直接中转属性和方法即可,不用写多余的代码。
举例
<template>
<BComponent :name="name" :age="age" :sex="sex" @clickA="methodA" />
template>
<script>
import BComponent from './components/B.vue'
export default {
components: {
BComponent
},
data() {
return {
name: '下雪天的夏风',
age: 18,
sex: 'male'
}
},
methods: {
methodA(item) {
console.log(item)
}
}
}
script>
<template>
<CComponent v-bind="$attrs" v-on="$listeners" />
template>
<script>
import CComponent from './C.vue'
export default {
inheritAttrs: false, // 下面有解释
props: ['name'], // 则 $attrs 上只有 age 和 sex
components: {
CComponent
}
}
script>
<template>
<div>
<div>{{ sex }} {{ age }}div>
<button @click="handleClick">触发A组件的方法button>
div>
template>
<script>
export default {
props: ['age', 'sex'],
methods: {
handleClick() {
this.$emit('clickA', 123)
}
}
}
script>
表示是否将 $attrs
作为当前组件的根元素上的 HTML 属性,默认 true。
为 true 时,B组件最终渲染的HTML
<div age="18" sex="male">
<div>male 18div>
<button>触发A组件的方法button>
div>
false 时
<div>
<div>male 18div>
<button>触发A组件的方法button>
div>
原问题:子组件调用父组件方法时,需要等待父组件处理之后,再执行子组件其他逻辑。如何实现?
<template>
<Children @updateCount="updateCount" />
template>
<script>
import Children from "./components/Children.vue";
// 模拟延迟
const delay = (duration) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, duration);
});
};
export default {
components: {
Children,
},
data() {
return {
count: 0,
};
},
methods: {
async updateCount(num) {
this.count = num;
await delay(2000);
},
},
};
script>
<template>
<button @click="handleClick">修改 countbutton>
template>
<script>
export default {
props: {
updateCount: Function,
},
methods: {
async handleClick() {
await this.$listeners.updateCount(10);
// 等待父组件调用该回调函数后,再执行子组件其他逻辑
// ...
},
},
};
script>
provide / inject
需要在一起使用,无视嵌套的层级。
祖先组件通过 provide
提供属性和方法,所有后代组件都可以通过 inject
接收注入的属性和方法。
注意,provide 和 inject 绑定不是响应的!
<script>
export default {
provide: {
author: 'xxx'
},
}
script>
<template>
<div>{{ author }}div>
template>
<script>
export default {
inject: ['author'],
// 也可以像 props 一样,设置默认值
// inject: {
// author: {
// default: 'fpp'
// }
// },
}
script>
下面是全局通信
不多赘述,官方文档 很详细。
事件总线
我们需要一个插件满足以下需求,来实现不受层级约束的组件通信。
实现:
// eventBus.js
const listeners = {}
export default {
// 监听
$on(event, handler) {
if (!listeners[event]) {
listeners[event] = new Set()
}
// 同一个事件,可以绑定多个处理函数
listeners[event].add(handler)
},
// 取消监听
$off(event, handler) {
if (listeners[event]) {
listeners[event].delete(handler)
}
},
// 触发监听,可传递参数
$emit(event, ...args) {
if (listeners[event]) {
for (const handler of listeners[event]) {
handler(...args)
}
}
}
}
举例1
// test.js
import eventBus from './eventBus.js'
eventBus.$on('event1', function () {
console.log('1')
})
eventBus.$on('event1', function () {
console.log('2')
})
setTimeout(() => {
eventBus.$emit('event1')
}, 1000)
而 Vue 实例本身就实现了上面这3个方法,所以可直接这样实现
// eventBus.js
import Vue from 'vue'
export default new Vue({})
或是直接绑定到原型上来使用。
// main.js
import Vue from 'vue'
import App from './App.vue'
const vm = new Vue({
render: (h) => h(App)
})
Vue.prototype.$bus = vm
vm.$mount('#app')
举例2:下面2个组件层级关系不做限制。
<template>
<button @click="handleClick">触发 event1 事件button>
template>
<script>
export default {
methods: {
handleClick() {
this.$bus.$emit('event1', 123)
}
}
}
script>
<script>
export default {
methods: {
methodC1(item) {
console.log(item)
},
methodC2(item) {
console.log(item)
}
},
created() {
this.$bus.$on('event1', this.methodC1)
this.$bus.$on('event1', this.methodC2)
}
}
script>
注意,这个思路是将事件放到一个对象中管理(监听和触发)。如果直接在不同组件中使用
this.$on()
和this.$emit()
是无效的,因为this
指向不同的组件实例,而$emit()
触发的是实例自己通过$on()
监听的事件。
因为这篇文章太长了,无法发表,所以 看这篇文章
通过 url 参数通信。
不多赘述,官方文档 很详细。
不多赘述。
以上。