非父子组件之间通信
在开发中,我们构建了组件树之后,除了父子组件之间的通信之外,还会有非父子组件之间的通信
主要的方式有
- provide 和 inject
- vuex
- 事件总线
provide 和 inject
Provide/Inject用于非父子组件之间共享数据, 主要是那些层级嵌套较深的组件之间相互传递数据,
比如有一些深度嵌套的组件,子组件想要获取父组件的部分内容
无论层级结构有多深,父组件都可以作为其所有子组件的依赖提供者,
即provide提供的依赖注入可以被看成是long range props
父组件有一个 provide 选项来提供数据
子组件有一个 inject 选项来开始使用这些数据
在整个过程中:
父组件不需要知道哪些子组件使用它 provide 的 property
子组件不需要知道 inject 的 property 来自哪里
依赖提供者 --- 父组件
<template>
<div>
<Middle />
</div>
</template>
<script>
import Middle from './components/Middle.vue'
export default {
name: 'App',
components: {
Middle
},
provide: {
// 这里面是无法直接使用this关键字的
// 因为此时的this会找到的是script下的this
// 而这个this是undefined,如果需要使用this,这provide需要被设定为是一个函数
msg: 'message in App'
}
}
</script>
依赖使用者 --- 子孙组件
<template>
<div>
{{ msg }}
</div>
</template>
<script>
export default {
name: 'Child',
inject: ['msg']
}
</script>
为了我们可以在provide中正确使用我们的this关键字,我们需要将provide对应的值修改为函数形式
<template>
<div>
<Middle />
</div>
</template>
<script>
import Middle from './components/Middle.vue'
export default {
name: 'App',
components: {
Middle
},
// provide中并不会使用this关键字,所以这里不可以使用箭头函数
// 推荐以后将provide直接写成函数形式
provide() {
return {
// 在vue调用函数的时候,会自动使用call来修正this指向
// 注意: provide中的数据不是响应式的,也就意味着this.msg发生改变的时候,
// provide中的msg属性是不会相应发生实时的改变
msg: this.msg
}
},
// data中并不会使用this关键字,所以这里可以使用箭头函数
data: () => ({
msg: 'message in App'
})
}
</script>
provide中的数据赋值是一次性的,也就是不是响应式的,如果我们希望实际监听对应状态的改变,而实时修改状态使用者中对应的状态,我们需要使用computed方法
状态提供者 --- App.vue
<template>
<div>
<Middle />
<button @click="counter += 1">+1</button>
</div>
</template>
<script>
import Middle from './components/Middle.vue'
import { computed } from 'vue'
export default {
name: 'App',
components: {
Middle
},
provide() {
return {
// computed是vue3提供的compositeAPI
// 作用是可以将this.counter转变为响应式数据,并返回基于参数的对应计算属性
// 参数为一个get方法
counter: computed(() => this.counter)
}
},
data: () => ({
counter: 0
})
}
</script>
状态使用者 --- 子孙组件
<template>
<div>
<!-- counter是一个ref对象,如果需要获取实际的值的时候,需要调用其value属性 -->
<!-- 最新版的ref对象在取值的时候,已经不需要再去取其对应的value属性,直接使用ref对象即可 -->
{{ counter }}
</div>
</template>
<script>
export default {
name: 'Child',
inject: ['counter']
}
</script>
全局事件总线
provide和inject主要是用来祖孙组件之间进行数据的传递,Vuex是用来在多个关联较远的组件之间进行数据传递,
而如果我们希望在一个组件中触发某些事件,在另一个关系较远的组件中监听对应事件并作出相应,就需要使用全局事件总线
npm install mitt
@/utils/emitter.js
import mitt from 'mitt'
// 可以使用mitt方法创建多个事件分发器
// 触发和事件的时候,必须使用同一个事件分发器
export const emitter = mitt()
事件触发组件
<template>
<div>
<Middle />
<button @click="handleClick">触发事件</button>
</div>
</template>
<script>
import Middle from './components/Middle.vue'
import { emitter } from './utils/emit'
export default {
name: 'App',
components: {
Middle
},
methods: {
handleClick() {
// emit(事件名,参数列表)
// 多个参数之间使用对象传递,因为emit事件只有两个参数
emitter.emit('emitEvent', { name: 'Klaus' })
}
}
}
</script>
事件响应组件
<template>
<div></div>
</template>
<script>
import { emitter } from '@/utils/emit.js'
export default {
name: 'Child',
created() {
// 在组件被创建就需要开始监听对应的事件
emitter.on('emitEvent', param => {
console.log(param1)
})
}
}
</script>
// 清除所有的事件监听
emitter.all.clear()
// 如果需要移除某一个具体的事件监听,那么监听和移除的函数必须是同一个
// 也就是参数2 必须是一个指向某一个具体执行函数的引用地址
function onFoo() {}
emitter.on('foo', onFoo) // listen
emitter.off('foo', onFoo) // unlisten
// *表示监听所有的事件
// 参数1为事件名,参数2为传入的参数
// 触发了几个参数,这个事件就会被执行几次
emitter.on('*', (type, param) => {
console.log(type, param)
})
emitter.emit('emitEvent', { name: 'Klaus' })
emitter.emit('foo', { name: 'foo' })
emitter.on('*', (type, param) => {
console.log(type, param)
// => emitEvent {name: "Klaus"}
// => foo {name: "foo"}
})
插槽
前面我们会通过props传递给组件一些数据,让组件来进行展示
但是为了让这个组件具备更强的通用性,我们更希望我们组件中有部分的结构也可以由用户进行自定义操作,而不是只能自定义数据
以JD搜索框为例:
- 这个组件分成三块区域:左边-中间-右边,每块区域的内容是不固定
- 左边区域可能显示一个菜单图标,也可能显示一个返回按钮,可能什么都不显示
- 中间区域可能显示一个搜索框,也可能是一个列表,也可能是一个标题,等等
- 右边可能是一个文字,也可能是一个图标,也可能什么都不显示
在封装组件中,使用特殊的元素<slot>就可以为封装组件开启一个插槽
<slot>本质上就是一个占位元素,让外部决定到底显示什么样的元素和内容
如果外部传入了插槽的内容,那么就显示外部传入的内容
如果外部什么数据和元素都没有传入的时候,就不进行任何的渲染
在slot中,我们可以存放任何的内容,无论是自定义的组件,还是元素结构,还是单纯的数据展示
slot使用者
<template>
<div>
<Child>
<!-- 使用插槽 -->
<p>App Component</p>
</Child>
</div>
</template>
<script>
import Child from './components/Child.vue'
export default {
name: 'App',
components: {
Child
}
}
</script>
slot声明者
<template>
<div>
<!-- 这里使用插槽作为占位符 -->
<!-- 由外部调用者决定具体存放什么内容 -->
<slot />
</div>
</template>
<script>
export default {
name: 'Child'
}
</script>
一个不带 name 的slot,会带有隐含的名字 default
<slot />
<!-- 等价于 -->
<slot name="default" />
<Cpn>
<span>默认插槽中的内容</span>
</Cpn>
<!-- 等价于 -->
<Cpn>
<template v-slot:default>
<span>默认插槽中的内容</span>
</template>
</Cpn>
缺省值
有时候我们希望在使用插槽时,如果没有插入对应的内容,那么我们需要显示一个****默认的内容
这个默认的内容只会在没有提供插入的内容时,才会显示
<slot> default value </slot>
具名插槽
之前我们的插槽都是没有任何的名字的。那么这个插槽被称之为缺省插槽或默认插槽
此时如果我们界面中有多个缺省插槽
调用者
<Child>
<p>App Component</p>
</Child>
使用者
<div>
<slot />
<slot />
<slot />
</div>
此时每个插槽都会获取到我们插入的内容来显示
因为所有的都是默认插槽,所有都能匹配的上
此时我们就需要为我们的插槽起一个名字,这种插槽就被称之为具名插槽
具名插槽顾名思义就是给插槽起一个名字,<slot> 元素有一个特殊的 attribute:name
一个不带 name 的slot,会带有隐含的名字 default
调用者
<Child>
<template v-slot:header>
<p>Header</p>
</template>
<template v-slot:main>
<p>Main</p>
</template>
<template v-slot:footer>
<p>Footer</p>
</template>
</Child>
定义者
<div>
<slot name="header" />
<slot name="main" />
<slot name="footer" />
</div>
跟 v-on 和 v-bind 一样,v-slot 也有缩写
即把参数之前的所有内容 (v-slot:) 替换为字符 #
<Child>
<template #header>
<p>Header</p>
</template>
<template #main>
<p>Main</p>
</template>
<template #footer>
<p>Footer</p>
</template>
</Child>
动态插槽名
目前我们使用的插槽名称都是固定的, 但是有的时候,我们希望插槽的名称也是由外部来具体指定的
此时, 我们可以通过 v-slot:[dynamicSlotName]方式动态绑定一个名称
插槽调用者
<template>
<div>
<!-- 将插槽名以props的方式进行传入 -->
<Child :slotName="slotName">
<!-- 动态指定对应的插槽名 -->
<template #[slotName]>
<p>Main</p>
</template>
</Child>
</div>
</template>
<script>
import Child from './components/Child.vue'
export default {
name: 'App',
components: {
Child
},
data() {
return {
slotName: 'main'
}
}
}
</script>
插槽调用者
<template>
<div>
<!-- 动态指定插槽名 -->
<slot :name="slotName" />
</div>
</template>
<script>
export default {
name: 'Child',
props: {
slotName: String
}
}
</script>
渲染作用域
在Vue中有渲染作用域的概念:
- 父级模板里的所有内容都是在父级作用域中编译的
- 子模板里的所有内容都是在子作用域中编译的
这就意味着,插槽中的内容虽然是在别的组件中使用的,但是插槽中的内容的编译是在调用插槽的组件中编译的
所以插槽中的变量只能是调用插槽的组件作用域中存在的变量
作用域插槽
因为有渲染作用域存在,所以插槽中使用的变量,只能是调用插槽的组件作用域中存在的变量
但插槽毕竟是在别的组件中进行显示的,也就意味着我们需要使用调用插槽的组件作用域中不存在的变量
此时就可以使用作用域插槽
插槽调用者
<template>
<div>
<Child>
<!--
所有传递给插槽调用者的数据都会以键值对的形式存放到slotScope中
该案例中 slotScope的值为{name: 'Msg in Child Cpn'}
注意: slotScope只是一个变量的名称,可以是任意合法的JS变量名
-->
<template v-slot="slotScope">
<p>{{ slotScope.msg }}</p>
</template>
</Child>
</div>
</template>
<script>
import Child from './components/Child.vue'
export default {
name: 'App',
components: {
Child
}
}
</script>
插槽定义者
<template>
<div>
<!-- 将对应数据传递插槽调用者 -->
<slot :msg="msg" />
</div>
</template>
<script>
export default {
name: 'Child',
data() {
return {
msg: 'Msg in Child Cpn'
}
}
}
</script>
独占默认插槽缩写
如果我们的插槽只有默认插槽时,我们就可以将 v-slot 直 接用在组件上,从而省略template标签
<template>
<div>
<Child v-slot="slotScope">
<p>{{ slotScope.msg }}</p>
</Child>
</div>
</template>
但是,如果我们有默认插槽和具名插槽,那么按照完整的template来编写
因为此时如果直接将插槽写在组件上的时候,vue不知道对应的数据应该给那个slot
多个slot所需要的数据可能是不同的
<template>
<div>
<Child >
<template v-slot="slotScope">
<p>{{ slotScope.msg }}</p>
</template>
<template v-slot:foo="{ msg }">
<p>{{ msg }}</p>
</template>
</Child>
</div>
</template>