vue3学习 --- 非父子组件通信和slot

2,323 阅读6分钟

非父子组件之间通信

在开发中,我们构建了组件树之后,除了父子组件之间的通信之外,还会有非父子组件之间的通信

主要的方式有

  • provide 和 inject
  • vuex
  • 事件总线

provide 和 inject

Provide/Inject用于非父子组件之间共享数据, 主要是那些层级嵌套较深的组件之间相互传递数据,

比如有一些深度嵌套的组件,子组件想要获取父组件的部分内容

无论层级结构有多深,父组件都可以作为其所有子组件的依赖提供者,

即provide提供的依赖注入可以被看成是long range props

父组件有一个 provide 选项来提供数据

子组件有一个 inject 选项来开始使用这些数据

在整个过程中:

父组件不需要知道哪些子组件使用它 provide 的 property

子组件不需要知道 inject 的 property 来自哪里

IceweE.png

依赖提供者 --- 父组件

<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搜索框为例:

IcbXgw.png

  • 这个组件分成三块区域:左边-中间-右边,每块区域的内容是不固定
  • 左边区域可能显示一个菜单图标,也可能显示一个返回按钮,可能什么都不显示
  • 中间区域可能显示一个搜索框,也可能是一个列表,也可能是一个标题,等等
  • 右边可能是一个文字,也可能是一个图标,也可能什么都不显示

在封装组件中,使用特殊的元素<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>

此时每个插槽都会获取到我们插入的内容来显示

因为所有的都是默认插槽,所有都能匹配的上

IcbszR.png

此时我们就需要为我们的插槽起一个名字,这种插槽就被称之为具名插槽

具名插槽顾名思义就是给插槽起一个名字,<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中有渲染作用域的概念:

  • 父级模板里的所有内容都是在父级作用域中编译的
  • 子模板里的所有内容都是在子作用域中编译的

这就意味着,插槽中的内容虽然是在别的组件中使用的,但是插槽中的内容的编译是在调用插槽的组件中编译的

所以插槽中的变量只能是调用插槽的组件作用域中存在的变量

IcuL6e.png

作用域插槽

因为有渲染作用域存在,所以插槽中使用的变量,只能是调用插槽的组件作用域中存在的变量

但插槽毕竟是在别的组件中进行显示的,也就意味着我们需要使用调用插槽的组件作用域中不存在的变量

此时就可以使用作用域插槽

插槽调用者

<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>