Vue的组件是其非常重要的系统,组件之间的通信也是开发中不可避免的需求
一般来说Vue组件是以下几种关系
A组件和B组件、B组件和C组件、B组件和D组件是父子关系,C组件和D组件是兄弟关系,A组件和C/D组件是隔代关系。
本文阐述了几种常用的通信方式和使用场景
props&&emit
父组件通过 props
传递数据给子组件,子组件通过 emit
发送事件传递数据给父组件
这种父子通信方式也就是典型的单向数据流,父组件通过 props
传递数据,子组件不能直接修改 props
, 而是必须通过发送事件的方式告知父组件修改数据。
// component-a
<template>
<div id="app">
<com-b title="cc" @changeTitle="handChange"></com-b>
<!-- 传入title 接受自定义事件changeTitle -->
</div>
</template>
<script>
import comB from './components/comB.vue'
export default {
name: 'app',
components: {
comB
},
methods:{
// 接受子组件传递的事件
handChange(val){
alert(val)
}
}
}
</script>
// component-b
<template>
<div>
{{title}}
<button @click="handChange">Change</button>
</div>
</template>
<script>
export default {
name: "com-b",
props: ["title"],// props 父组件传来的值
methods:{
// 触发自定义事件,想父组件传递发送修改后的数据
handChange(){
this.$emit('changeTitle','newTitle')
}
}
};
</script>
优点:易于使用,结构清晰
缺点:只能用于父子组件通信
ref&&$parent / $children
这两种都是直接得到组件实例,使⽤后可以直接调⽤组件的⽅法或访问数据
- ref:给元素或组件注册引⽤信息
- $parent/$children:访问父/子组件
ref
// component-a
<template>
<div id="app">
<!-- 为子组件注册引用信息 -->
<com-b ref="comB"></com-b>
</div>
</template>
<script>
import comB from "./components/comB.vue";
export default {
name: "app",
components: {
comB
},
mounted() {
// 通过$refs获取到对应子组件的引用信息
console.log(this.$refs.comB);
}
};
</script>
$parent / $children
$parent
和$children
都是基于当前上下文访问父组件和子组件
// component-a
<template>
<div id="app">
<com-b></com-b>
</div>
</template>
<script>
import comB from "./components/comB.vue";
export default {
name: "app",
components: {
comB
},
mounted() {
// 通过$children获取全部子组件
console.log(this.$children);
}
};
</script>
// component-b
<template>
<div>
</div>
</template>
<script>
export default {
name: "com-b",
mounted(){
// 通过$parent获取父组件
console.log(this.$parent);
}
};
</script>
ref
和$parent/$children
的优缺点和props&&emit
相同,弊端都是无法在跨级和兄弟间通信
provide/inject
ref
和$parent/$children
在跨级通信中有一定的弊端。Vue.js 2.2.0
版本后新增 provide / inject
API
vue文档
这对选项需要⼀起使⽤,以允许⼀个祖先组件向其所有⼦孙后代注⼊⼀个依赖,不论组件层次有多深,并在起上下游关系成⽴的时间⾥始终⽣效
provide 选项应该是一个对象或返回一个对象的函数。该对象包含可注入其子孙的属性。
inject 选项应该是:
- 一个字符串数组
一个对象,对象的 key 是本地的绑定名,value 是:
- 在可用的注入内容中搜索用的 key (字符串或 Symbol),或
一个对象,该对象的:
- from 属性是在可用的注入内容中搜索用的 key (字符串或 Symbol)
- default 属性是降级情况下使用的 value
provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。
// 父级组件提供 'foo'
var Provider = {
provide: {
foo: 'bar'
},
// ...
}
// 子组件注入 'foo'
var Child = {
inject: ['foo'],
created () {
console.log(this.foo) // => "bar"
}
// ...
}
模拟Vuex
在做 Vue ⼤型项⽬时,可以使⽤ Vuex 做状态管理。使用 provide / inject
,可以模拟 达到 Vuex
的效果 。
使⽤ Vuex,最主要的⽬的是跨组件通信、全局数据维护。⽐如⽤户的登录信息维护、通知信息维护等全局的状态和数据
通常vue应用都有一个根根组件app.vue
,可以⽤来存储所有需要的全局数据和状态,methods
等。项目中所有的组件其父组件都是app
,通过provide
将app
实例暴露对外提供
<template>
<div>
<router-view></router-view>
</div>
</template>
<script>
export default {
provide () {
return {
app: this
}
}
}
</script>
接下来任何组件只要通过 inject
注⼊ app.vue
的 app
的话,都可以直接通过this.app.xxx
来访问 app.vue
的 data、computed、methods
等内容
例如通过这个特性保存登录信息
export default {
provide() {
return {
app: this
};
},
data() {
return {
userInfo: null
};
},
methods: {
getUserInfo() {
// 这⾥通过 ajax 获取⽤户信息后,赋值给this.userInfo;
$.ajax("/user", data => {
this.userInfo = data;
});
}
},
mounted() {
this.getUserInfo();
}
};
之后在任何⻚⾯或组件,只要通过 inject
注⼊ app
后,就可以直接访问 userInfo
的数据了
<template>
<div>
{{ app.userInfo }}
</div>
</template>
<script>
export default {
inject: ['app']
}
</script>
优点:
- 跨级注入
- 所有子组件都可获取到注入的信息
缺点:
- 注入的数据非响应式
Vuex
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。当需要开发开发大型单页应用(SPA),就应该考虑使用Vuex了,它能把组件的共享状态抽取出来,当做一个全局单例模式进行管理。这样不管你在何处改变状态,都会通知使用该状态的组件做出相应修改。Vuex
官方文档已经给出了详细的使用方式
优点:
- 官方集成管理库,可以处理各种场景的通信和状态管理
缺点:
- 需要额外引入管理库
Bus
如果不是大型项目,状态管理不复杂,数据量不是很大,没有必要使用Vuex
可以使用一个空的vue
实例作为事件总线中间件Bus
处理组件间的通信
首先在全局定义bus
let bus = new Vue();
var eventBus = {
install(Vue, options) {
Vue.prototype.$bus = bus;
}
};
Vue.use(eventBus);
然后就可以在组件中使用$on
,$emit
,off
来监听,分发和销毁组件
分发组件
// component-c
<template>
<div>
<button @click="handClick">handClick</button>
</div>
</template>
<script>
export default {
name: "com-c",
methods: {
handClick: function() {
this.$bus.$emit("bus", "val");
}
}
};
</script>
监听组件
// component-d
<template>
<div></div>
</template>
<script>
export default {
name: "com-d",
// ...
created() {
this.$bus.$on("bus", val => {
//获取传递的参数并进行操作
console.log(val);
});
},
// 最好在组件销毁前
// 清除事件监听
beforeDestroy() {
this.$bus.$off("evetn");
}
};
</script>
最好在组件销毁之前清除监听事件
优点:
- 使用简单,不需要额外支持
- 可以实现跨级和兄弟间通信
缺点:
- 需要在组件销毁时,手动清除事件监听
- 事件过多时比较混乱
dispatch/broadcast
$dispatch
和 $broadcast
是Vue1.x中提供的API,前者⽤于向上级派发事件,只要是它的⽗级(⼀级或多级以上),都可以在组件内通过 $on 监听到,后者相反,是由上级向下级⼴播事件
// 子组件
vm.$dispatch(eventName,params)
// 父组件
vm.$on(eventName
, (params) => {
console.log(params);
});
$broadcast
类似,只不过⽅向相反。这两种⽅法⼀旦发出事件后,任何组件都是可以接收到的,就近原则,⽽且会在第⼀次接收到后停⽌冒泡,除⾮返回 true
。
这2个方法在已经被弃用,Vue
官方给出的解释是:
因为基于组件树结构的事件流方式实在是让人难以理解,并且在组件结构扩展的过程中会变得越来越脆弱。这种事件方式确实不太好,我们也不希望在以后让开发者们太痛苦。并且$dispatch 和 $broadcast 也没有解决兄弟组件间的通信问题。
虽然在开发中,没有Vuex这样的专业状态管理工具方便好用,但是在独立组件库和一些特殊场景中,也是非常好用的一种传递方式。
模拟dispatch/broadcast
自行模拟dispatch/broadcast
无法达到与原方法一模一样的效果,但是基本功能都是可以实现的,解决组件之间的通信问题
方法有功能有向上/下找到对应的组件,触发指定事件并传递数据,其下/上级组件已经通过$on
监听了该事件。
首先需要正确地向上或向下找到对应的组件实例,并在它上⾯触发⽅法。
function 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]));
}
});
}
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(componentName, eventName, params) {
broadcast.call(this, componentName, eventName, params);
}
}
};
这两个⽅法都接收了三个参数,第⼀个是组件的 name 值,⽤于向上或向下递归遍历来寻找对应的组件,第⼆个和第三个就是上⽂分析的⾃定义事件名称和要传递的数据。
在 dispatch
⾥,通过 while
语句,不断向上遍历更新当前组件(即上下⽂为当前调⽤该⽅法的组件)的⽗组件实例(变量 parent
即为⽗组件实例),直到匹配到定义的 componentName
与某个上级组件的 name
选项⼀致时,结束循环,并在找到的组件实例上,调⽤ $emit
⽅法来触发⾃定义事件 eventName
。 broadcast
⽅法与之类似,只不过是向下遍历寻找
优点:
- 使用简单
- 可以实现跨级通信
缺点:
- 原生支持已经废除,需要自行实现
findComponents系列
上述介绍的各种通信方法都有各自的局限性,我们可以实现一个 findComponents
系列的方法,可以实现
- 向上找到最近的指定组件
- 向上找到所有的指定组件
- 向下找到最近的指定组件
- 向下找到所有指定的组件
- 找到指定组件的兄弟组件
5个方法都是通过递归和遍历,通过组件name
选项匹配到指定组件返回
向上找到最近的指定组件
function findComponentUpward(context, componentName) {
let parent = context.$parent; // 获取父级组件
let name = parent.$options.name; // 获取父级组件名称
// 如果父级存在 且 父级组件 没有name 或 name与要寻找的组件名不一致,重置parent和name,再逐级寻找
while (parent && (!name || [componentName].indexOf(name) < 0)) {
parent = parent.$parent;
if (parent) name = parent.$options.name;
}
// 逐级查找父级组件名和传参名是否一致,返回找到的parent
return parent;
}
findComponentUpward
接收两个参数,第⼀个是当前上下⽂,即你要基于哪个组件来向上寻找,⼀般都是基于当前的组件,也就是传⼊ this;第⼆个参数是要找的组件的 name 。dispatch
是通过触发和监听事件来完成事件交互,findComponentUpward
会直接拿到组件的实例
向上找到所有的指定组件
function findComponentsUpward(context,
componentName) {
let parents = []; // 收集指定组件
const parent = context.$parent;
if (parent) {
if (parent.$options.name === componentName)
parents.push(parent);
return parents.concat(findComponentsUpward(parent, // 递归逐级向上寻找
componentName));
} else {
return [];
}
}
findComponentsUpward
会返回的是⼀个数组,包含了所有找到的组件实例findComponentsUpward
的使⽤场景较少
向下找到最近的指定组件
function findComponentDownward(context,
componentName) {
const childrens = context.$children;
let children = null;
if (childrens.length) {
for (const child of childrens) {
const name = child.$options.name;
if (name === componentName) {
children = child;
break;
} else {
children = findComponentDownward(child,
componentName);
if (children) break;
}
}
}
return children;
}
context.$children
得到的是当前组件的全部⼦组件,所以需要遍历⼀遍,找到有没有匹配到的组件 name
,如果没找到,继续递归找每个 $children
的 $children
,直到找到最近的⼀个为⽌
向下找到所有指定的组件
function findComponentsDownward(context,
componentName) {
return context.$children.reduce((components, child) => {
if (child.$options.name === componentName)
components.push(child);
const foundChilds = findComponentsDownward(child, componentName);
return components.concat(foundChilds);
}, []);
}
使⽤ reduce
做累加器,并⽤递归将找到的组件合并为⼀个数组并返回
找到指定组件的兄弟组件
function findBrothersComponents(context,
componentName, exceptMe = true) {
let res = context.$parent.$children.filter(item
=> {
return item.$options.name === componentName;
});
let index = res.findIndex(item => item._uid ===
context._uid);
if (exceptMe) res.splice(index, 1);
return res;
}
findBrothersComponents
多了⼀个参数 exceptMe
,是否把本身除外,默认是 true
。寻找兄弟组件的⽅法,是先获取 context.$parent.$children
,也就是⽗组件的全部⼦组件,这⾥⾯当前包含了本身,所有也会有第三个参数exceptMe
。Vue.js
在渲染组件时,都会给每个组件加⼀个内置的属性 _uid
,这个 _uid
是不会重复的,借此我们可以从⼀系列兄弟组件中把⾃⼰排除掉。