Vue通信
1.非父子组件通信
-
在开发中,我们构建了组件树之后,除了父子组件之间的通信之外,还会有非父子组件之间的通信。
-
两种方式:
- Provide/Inject
- Mitt全局事件总线(第三方库)
1.1Provide和Inject
-
Provide/Inject用于非父子组件之间共享数据
- 比如有一些深度嵌套的组件,子组件想要获取父组件的部分内容
- 在这种情况下,如果还使用props一层一层接收传递实在是太麻烦
-
这种情况下,使用Procide和Inject是一个好的选择
-
无论层级结构有多深,父组件都可以作为其所有子组件的依赖
提供者
-
父组件有一个 provide 选项来提供数据
-
子组件有一个 inject 选项来开始使用这些数据
-
-
你可以将依赖注入看作是"long range props"
- 父组件不需要知道哪些子组件使用它 provide 的 property
- 子组件不需要知道 inject 的 property 来自哪里
1.2代码操作
-
App.vue
<template> <div> <Home></Home> <button @click="addName">+name</button> </div> </template> <script> import Home from './Home.vue'; import { computed } from "vue" export default { components: { Home }, provide: { name:"tim", age:18, length:computed(()=> this.names.length) } data() { return { names:["abc","cba","nba"] } }, methods: { addName(){ this.names.push("tim") } } } </script> -
Home.vue
<template> <div> <HomeContent></HomeContent> </div> </template> <script> import HomeContent from './HomeContent.vue'; export default { components: { HomeContent } } </script> <style scoped> </style> -
HomeContent.vue
<template> <div> HomeContent: {{name}} - {{age}} - {{length.value}} </div> </template> <script> export default { inject: ["name", "age", "length"], } </script> <style scoped> </style>
2.全局事件总线mitt库
- Vue3从实例中移除了 off 和 $once 方法,所以我们如果希望继续使用全局事件总线,要通过第三方的库
- Vue3官方有推荐一些库,例如 mitt 或 tiny-emitter;
- 这里我们主要的是mitt库的使用;
-
安装这个库
npm install mitt -
我们可以封装一个工具utils/eventbus.js
//eventbus.js import mitt from "mitt"; const emitter = mitt() export default emitter; -
以下代码演示
-
App.vue
<template> <div> <home/> <about/> </div> </template> <script> import Home from './Home.vue'; import About from './About.vue'; export default { components: { Home, About } } </script> <style scoped> </style> -
About.vue
<template> <div> <button @click="btnClick">按钮点击</button> </div> </template> <script> import emitter from './utils/eventbus'; export default { methods: { btnClick() { console.log("about按钮的点击"); emitter.emit("tim", {name: "tim", age: 18}); // emitter.emit("kobe", {name: "kobe", age: 30}); } } } </script> <style scoped> </style> -
HomeContent.vue
<template> <div> </div> </template> <script> import emitter from './utils/eventbus'; export default { created() { emitter.on("tim", (info) => { console.log("tim:", info); }); emitter.on("kobe", (info) => { console.log("kobe:", info); }); emitter.on("*", (type, info) => { console.log("* listener:", type, info); }) } } </script> <style scoped> </style>
-
3.插槽
-
在开发中,我们会经常封装一个个可复用的组件:
- 为了让这个组件具备更强的通用性,我们不能将组件中的内容限制为固定的div、span等等这些元素。
- 比如某种情况下我们使用组件,希望组件显示的是一个按钮,某种情况下我们使用组件希望显示的是一张图片;
- 我们应该让使用者可以决定某一块区域到底存放什么内容和元素;
-
举个栗子:假如我们定制一个通用的导航组件 - NavBar
- 这个组件分成三块区域:左边-中间-右边,每块区域的内容是不固定;
- 左边区域可能显示一个菜单图标,也可能显示一个返回按钮,可能什么都不显示;
- 中间区域可能显示一个搜索框,也可能是一个列表,也可能是一个标题,等等;
- 右边可能是一个文字,也可能是一个图标,也可能什么都不显示;
3.1如何使用插槽呢?
-
这个时候我们就可以来定义插槽slot:
- 插槽的使用过程其实是抽取共性、预留不同;
- 我们会将共同的元素、内容依然在组件内进行封装;
- 同时会将不同的元素使用slot作为占位,让外部决定到底显示什么样的元素;
-
如何使用slot呢?
- Vue中将 元素作为承载分发内容的出口;
- 在封装组件中,使用特殊的元素就可以为封装组件开启一个插槽
- 该插槽插入什么内容取决于父组件如何使用;
-
App.vue
<template> <div> <MySlotCpn> <button>我是插槽button</button> </MySlotCpn> <MySlotCpn> <MyButton></MyButton> </MySlotCpn> </div> </template> <script> import MySlotCpn from './MySlotCpn.vue'; import MyButton from './MyButton.vue'; export default { components: { MySlotCpn, MyButton } } </script> <style scoped> </style> -
MySlotCpn.vue
<template> <div> <h2>组件开始</h2> <slot> <i>我是默认的i元素</i> </slot> <slot> <i>我是默认的i元素</i> </slot> <slot> <i>我是默认的i元素</i> </slot> <h2>组件结束</h2> </div> </template> <script> export default { } </script> <style scoped> </style> -
MyButton.vue
<template> <div> <button>myself button</button> </div> </template> <script> export default { } </script> <style scoped> </style>
3.2具名插槽
-
事实上,我们希望达到的效果是插槽对应的显示,这个时候我们就可以使用 具名插槽
- 具名插槽顾名思义就是给插槽起一个名字, 元素有一个特殊的 attribute:name;
- 一个不带 name 的slot,会带有隐含的名字 default;
-
App.vue
<template> <div> <NavBar :name="name"> <template #left> <button>左边的按钮</button> </template> <template #center> <h2>我是标题</h2> </template> <template #right> <i>右边的i元素</i> </template> <!-- 下面这个name是data中的name --> <template #[name]> <i>why内容</i> </template> </NavBar> </div> </template> <script> import NavBar from './NavBar.vue'; export default { components: { NavBar }, data() { return { name: "tim" } } } </script> <style scoped> </style> -
NavBar.vue
<template> <div class="nav-bar"> <!-- <slot name="default"></slot> --> <div class="left"> <slot name="left"></slot> </div> <div class="center"> <slot name="center"></slot> </div> <div class="right"> <slot name="right"></slot> </div> <div class="addition"> <slot :name="name"></slot> </div> </div> </template> <script> export default { props: { name: String } // data() { // return { // name: "tim" // } // } } </script> <style scoped> .nav-bar { display: flex; } .left, .right, .center { height: 44px; } .left, .right, .addition { width: 80px; background-color: red; } .center { flex: 1; background-color: blue; } </style>
3.3作用域插槽
-
在Vue中有渲染作用域的概念:
- 父级模板里的所有内容都是在父级作用域中编译的;
- 子模板里的所有内容都是在子作用域中编译的;
-
如何理解这句话呢?我们来看一个案例:
- 在我们的案例中ChildCpn自然是可以让问自己作用域中的title内容的;
- 但是在App中,是访问不了ChildCpn中的内容的,因为它们是跨作用域的访问
-
App.vue
<template> <div> <!-- 编译作用域 --> <!-- <child-cpn> <button>{{title}}</button> </child-cpn> --> <ShowNames :names="names"> <template v-slot="tim"> <button>{{tim.item}}-{{tim.index}}</button> </template> </ShowNames> <ShowNames :names="names" v-slot="coderwhy"> <button>{{coderwhy.item}}-{{coderwhy.index}}</button> </ShowNames> <!-- 注意: 如果还有其他的具名插槽, 那么默认插槽也必须使用template来编写 --> <ShowNames :names="names"> <template v-slot="tim"> <button>{{coderwhy.item}}-{{coderwhy.index}}</button> </template> <template v-slot:tim> <h2>我是why的插入内容</h2> </template> </ShowNames> <ShowNames :names="names"> <template v-slot="slotProps"> <strong>{{slotProps.item}}-{{slotProps.index}}</strong> </template> </ShowNames> </div> </template> <script> import ChildCpn from './ChildCpn.vue'; import ShowNames from './ShowNames.vue'; export default { components: { ChildCpn, ShowNames }, data() { return { names: ["why", "kobe", "james", "curry"] } } } </script> <style scoped> </style> -
ChildCpn.vue
<template> <div> <slot></slot> </div> </template> <script> export default { data() { return { title: "我是title" } } } </script> <style scoped> </style> -
ShowNames.vue
<template> <div> <template v-for="(item, index) in names" :key="item"> <slot :item="item" :index="index"></slot> <slot name="tim"></slot> </template> </div> </template> <script> export default { props: { names: { type: Array, default: () => [] } } } </script> <style scoped> </style>