共计 6354 个字符,预计需要花费 16 分钟才能阅读完成。
download:Vue3 + React18 + TS4 入门到实战的积分回
Vue 组件间的通信形式
前言
在 Vue 组件库的开发过程中,组件之间的通信一直是一个重要的课题。诚然官网的 Vuex 状态治理打算可能很好的解决组件之间的通信问题,然而组件库外部对 Vuex 的使用经常比较沉重。本文列举了几种实用的不使用 Vuex 的组件间通信方法,供大家参考。
组件之间通信的场景
在进入咱们明天的主题之前,咱们先来总结下 Vue 组件之间通信的几种场景,一般可能分为如下几种场景:
父子组件之间的通信
兄弟组件之间的通信
隔代组件之间的通信
父子组件之间的通信
父子组件之间的通信应该是 Vue 组件通信中最简略也最常见的一种了,概括为两个部分:父组件通过 prop 向子组件传送数据,子组件通过自定义事件向父组件传送数据。
父组件通过 prop 向子组件传送数据
Vue 组件的数据流向都遵循单向数据流的原则,所有的 prop 都使得其父子 prop 之间形成了一个单向上行绑定:父级 prop 的更新会向下流动到子组件中,然而反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的利用的数据流向难以理解。
额定的,每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件外部改变 prop。如果你这样做了,Vue 会在阅读器的控制台中收回警告。
父组件 ComponentA:
<component-b title="welcome">
子组件 ComponentB:
{{title}}
16.
子组件通过自定义事件向父组件传送数据
在子组件中可能通过 $emit 向父组件发生一个事件,在父组件中通过 v-on/@ 进行监听。
子组件 ComponentA:
<component-b :title="title" @title-change="titleChange">
子组件 ComponentB:
<div @click="handleClick">{{title}}
这个例子非常简略,在子组件 ComponentB 外面通过 $emit 派发一个事件 title-change,在父组件 ComponentA 通过 @title-change 绑定的 titleChange 事件进行监听,ComponentB 向 ComponentA 传送的数据在 titleChange 函数的传参中可能获取到。
兄弟组件之间的通信
状态晋升
写过 React 的同学应该对组件的 状态晋升 概念并不陌生,React 外面将组件按照职责的不同划分为两类:展示型组件(Presentational Component) 和 容器型组件(Container Component)。
展示型组件不关心组件使用的数据是如何获取的,以及组件数据应该如何修改,它只需要知道有了这些数据后,组件 UI 是什么样子的即可。内部组件通过 props 传送给展示型组件所需的数据和修改这些数据的回调函数,展示型组件只是它们的使用者。
容器型组件的职责是获取数据以及这些数据的处理逻辑,并把数据和逻辑通过 props 提供应子组件使用。
因此,参考 React 组件中的 状态晋升 的概念,咱们在两个兄弟组件之上提供一个父组件,相当于容器组件,负责处理数据,兄弟组件通过 props 接收参数以及回调函数,相当于展示组件,来解决兄弟组件之间的通信问题。
ComponentA (兄弟组件 A):
{{title}}
<div @click="changeTitle">click me
ComponentC (容器组件 C):
<component-a :title="titleA" :change-title="titleAChange">
<component-b :title="titleB" :change-title="titleBChange">
可能看到,上述这种 “ 状态晋升 ” 的形式是比较繁琐的,特地是兄弟组件的通信还要借助于父组件,组件简单之后处理起来是相当麻烦的。
隔代组件之间的通信
隔代组件之间的通信可能通过如下几种形式实现:
$attrs/$listeners
rovide/inject
基于$parent/$children 实现的dispatch 和broadcast
attrs/attrs/attrs/listeners
Vue 2.4.0 版本新增了 $attrs 和 $listeners 两个方法。先看下官网对 $attrs 的介绍:
蕴含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外)。当一个组件没有申明任何 prop 时,这里会蕴含所有父作用域的绑定 (class 和 style 除外),并且可能通过 v-bind=”$attrs” 传入外部组件 —— 在创建高级别的组件时非常有用。
看个例子:
组件 A (ComponentA):
<component-a name=”Lin” age=”24″ sex=”male”>
复制代码
组件 B (ComponetB):
I am component B
<component-c v-bind="$attrs">
复制代码
组件 C (ComponetC):
I am component C
这里有三个组件,先人组件 (ComponentA)、父组件 (ComponentB) 和子组件 (ComponentC)。这三个组件形成了一个典型的子孙组件之间的关系。
ComponetA 给 ComponetB 传送了三个属性 name、age 和 sex,ComponentB 通过 v-bind=”$attrs” 将这三个属性再透传给 ComponentC,最初在 ComponentC 中打印 $attrs 的值为:
{age: ’24’, sex: ‘male’}
复制代码
为什么咱们一开始传送了三个属性,最初只打印了两个属性 age 和 sex 呢?因为在 ComponentC 的 props 中申明了 name 属性,$attrs 会主动排除掉在 props 中申明的属性,并将其余属性以对象的形式输入。
说白了就是一句话,$attrs 可能获取父组件中绑定的非 Props 属性。
一般在使用的时候会同时和 inheritAttrs 属性配合使用。
如果你不心愿组件的根元素继承 attribute,你可能在组件的选项中设置 inheritAttrs: false。
在 ComponentB 增加了 inheritAttrs=false 属性后,ComponentB 的 dom 结构中可能看到是不会继承父组件传送过去的属性:
再看下 $listeners 的定义:
蕴含了父作用域中的 (不含 .native 润饰器的) v-on 事件监听器。它可能通过 v-notallow=”$listeners” 传入外部组件 —— 在创建更高层次的组件时非常有用。
$listeners 也能把父组件中对子组件的事件监听全副拿到,这样咱们就能用一个 v-on 把这些来自于父组件的事件监听传送到下一级组件。
持续革新 ComponentB 组件:
I am component B
<component-c v-bind="$attrs" v-on="$listeners">
这里利用 $attrs 和 $listeners 方法,可能将先人组件 (ComponentA) 中的属性和事件透传给孙组件 (ComponentC),这样就可能实现隔代组件之间的通信。
provide/inject
provide/inject 是 Vue 2.2.0 版本后新增的方法。
这对选项需要一起使用,以容许一个先人组件向其所有子孙后代注入一个依赖,不论组件档次有多深,并在其上下游关系成立的工夫里始终失效。如果你熟悉 React,这与 React 的上下文个性很相似。
先看下简略的用法:
父级组件:
export default {
provide: {
name: 'Lin'
}
}
子组件:
export default {
inject: [‘name’],
mounted () {
console.log(this.name); // Lin
}
}
下面的例子可能看到,父组件通过 privide 返回的对象外面的值,在子组件中通过 inject 注入之后可能间接拜访到。
然而需要注意的是,provide 和 inject 绑定并不是可响应的,按照官网的说法,这是刻意为之的。
也就是说父组件 provide 外面的 name 属性值变动了,子组件中 this.name 获取到的值不变。
如果想让 provide 和 inject 变成可响应的,有以下两种形式:
provide 先人组件的实例,而后在子孙组件中注入依赖,这样就可能在子孙组件中间接修改先人组件的实例的属性,不过这种方法有个缺点就是这个实例上挂载很多没有必要的货色比如 props,methods
使用 Vue 2.6 提供的 Vue.observable 方法优化响应式 provide
看一下第一种场景:
先人组件组件 (ComponentA):
export default {
name: ‘ComponentA’,
provide() {
return {app: this}
},
data() {
return {
appInfo: {title: ''}
}
},
methods: {
fetchAppInfo() {this.appInfo = { title: 'Welcome to Vue world'}
}
}
}
咱们把整个 ComponentA.vue 的实例 this 对外提供,命名为 app。接下来,任何组件只需通过 inject 注入 app 的话,都可能间接通过 this.app.xxx 来拜访 ComponentA.vue 的 data、computed、methods 等内容。
子组件 (ComponentB):
{{title}}
<button @click="fetchInfo"> 获取 App 信息
这样,任何子组件,只需通过 inject 注入 app 后,就可能间接拜访先人组件中的数据了,同时也可能调用先人组件提供的方法修改先人组件的数据并反应到子组件上。
当点击子组件 (ComponentB) 的获取 App 信息按钮,会调用 this.app.fetchAppInfo 方法,也就是拜访先人组件 (ComponentA) 实例上的 fetchAppInfo 方法,fetchAppInfo 会修改 fetchAppInfo 的值。同时子组件 (ComponentB) 中会监听 this.app.appInfo 的变动,并将变动后的 title 值浮现在组件上。
再看一下第二种场景,通过 Vue.observable 方法来实现 provide 和 inject 绑定并可响应。
基于下面的示例,革新先人组件 (ComponentA):
import Vue from ‘vue’
const state = Vue.observable({title: ”});
export default {
name: ‘ComponentA’,
provide() {
return {state}
}
}
使用 Vue.observable 定义一个可响应的对象 state,并在 provide 中返回这个对象。
革新子组件 (ComponentB):
{{title}}
<button @click="fetchInfo"> 获取 App 信息
与之前的例子不同的是,这里咱们间接修改了 this.state.title 的值,因为 state 被定义成了一个可响应的数据,所以 state.title 的值被修改后,视图上的 title 也会立即响应并更新,从这里看,其实很像 Vuex 的处理形式。
以上两种形式对比可能发现,第二种借助于 Vue.observable 方法实现 provide 和 inject 的可响应更加简略高效,推荐大家使用这种形式。
基于 $parent/$children 实现的 dispatch 和 broadcast
先了解下 dispatch 和 broadcast 两个概念。
dispatch: 派发,指的是从一个组件外部向上传送一个事件,并在组件外部通过 $on 进行监听
broadcast: 广播,指的是从一个组件外部向下传送一个事件,并在组件外部通过 $on 进行监听
在实现 dispatch 和 broadcast 方法之前,先来看一下具体的使用方法。有 ComponentA.vue 和 ComponentB.vue 两个组件,其中 ComponentB 是 ComponentA 的子组件,两头可能跨多级,在 ComponentA 中向 ComponentB 通信:
组件 ComponentA:
<button @click=”handleClick”> 派发事件
组件 ComponentB:
export default {
name: ‘ComponentB’,
created () {
this.$on('on-message', this.showMessage)
},
methods: {
showMessage (text) {console.log(text)
}
}
}
dispatch 的逻辑写在 emitter.js 中,使用的时候通过 mixins 混入到组件中,这样可能很好的将事件通信逻辑和组件进行解耦。
dispatch 的方法有三个传参,别离是:需要接受事件的组件的名字 (全局唯一,用来精确查找组件)、事件名和事件传送的参数。
dispatch 的实现思路非常简略,通过 $parent 获取以后父组件对象,如果组件的 name 和接受事件的 name 一致 (dispatch 方法的第一个参数),在父组件上调用 $emit 发射一个事件,这样就会触发目标组件上 $on 定义的回调函数,如果以后组件的 name 和接受事件的 name 不一致,就递归地向上调用此逻辑。
dispath:
export default {
methods: {
dispatch(componentName, eventName, params) {
let parent = this.$parent || this.$root;
let name = parent.$options.name;
while (parent && (!name || name !== componentName)) {
parent = parent.$parent;
if (parent) {name = parent.$options.name}
}
if (parent) {parent.$emit.apply(parent, [eventName].concat(params));
}
}
}
}
broadcast 逻辑和 dispatch 的逻辑差不多,只是一个是通过 $parent 向上查找,一个是通过 $children 向下查找,
复制
export default {
methods: {
broadcast(componentName, eventName, params) {
this.$children.forEach(child {
const name = child.$options.name
if (name === componentName) {child.$emit.apply(child, [eventName].concat(params))
} else {broadcast.apply(child, [componentName, eventName].concat([params]))
}
})
}
}
}