【Vue】组件间通信和几个常用方法

关于这个主题的内容其实网上已经有很多了,这篇写着只是作为补充,特别是最近看了 iview 组件实现后觉得很有意思,光看着理解不如边写代码总结着来得快。

组件间通信

我们知道的组件通信方式:

组件间通信方式

A 和 B、B 和 C、B 和 D 都是父子关系,C 和 D 是兄弟关系,A 和 C 是隔代关系(可能隔多代)。组件间经常会通信,一般地:

  1. 父到子(prop)、父到孙(EventBus、Vuex)
  2. 子到父、孙到父($on/$emit、$parent/$children)
  3. 兄弟节点,非关联节点(EventBus、Vuex)

而 Vue.js 内置的通信手段一般有两种:

  • ref:给元素或组件注册引用信息;
  • $parent / $children:访问父 / 子实例

$parent 和 $children 类似,也是基于当前上下文访问父组件或全部子组件的。这两种方法的弊端是,无法在跨级或兄弟间通信。ref 和 $parent / $children 在跨级通信时是有弊端的。当组件 A 和组件 B 中间隔了数代(甚至不确定具体级别)时,以往会借助 Vuex 或 Bus 这样的解决方案,不得不引入三方库来支持。

这里介绍一种无依赖的组件通信方法:Vue.js 内置的 provide / inject 接口。

具体可参考官方文档 . 这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。如果你熟悉 React,这与 React 的上下文特性很相似。

provide 和 inject 主要为高阶插件/组件库提供用例。并不推荐直接用于应用程序代码中。

我们先来看一下这个 API 怎么用,假设有两个组件: A.vue 和 B.vue,B 是 A 的子组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// A.vue
export default {
provide: {
name: 'Fri'
}
}

// B.vue
export default {
inject: ['name'],
mounted () {
console.log(this.name); // Fri
}
}

可以看到,在 A.vue 里,我们设置了一个 provide: name,值为 Aresn,它的作用就是将 name 这个变量提供给它的所有子组件。而在 B.vue 中,通过 inject 注入了从 A 组件中提供的 name 变量,那么在组件 B 中,就可以直接通过 this.name 访问这个变量了,它的值也是 Fri 。这就是 provide / inject API 最核心的用法。

需要注意的是:

provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。

所以,上面 A.vue 的 name 如果改变了,B.vue 的 this.name 是不会改变的,仍然是 Fri 。

独立组件使用 provide / inject 的场景,主要是具有联动关系的组件,比如接下来很快会介绍的第一个实战:具有数据校验功能的表单组件 Form。它其实是两个组件,一个是 Form,一个是 FormItem,FormItem 是 Form 的子组件,它会依赖 Form 组件上的一些特性(props),所以就需要得到父组件 Form,这在 Vue.js 2.2.0 版本以前,是没有 provide / inject 这对 API 的,而 Form 和 FormItem 不一定是父子关系,中间很可能间隔了其它组件,所以不能单纯使用 \$parent 来向上获取实例。

寻找组件实例 - 几个常用方法

这并非 Vue.js 内置,而是需要自行实现,以工具函数的形式来使用,它是一系列的函数,可以说是组件通信的终极方案。findComponents 系列方法最终都是返回组件的实例,进而可以读取或调用该组件的数据和方法。

它适用于以下场景:

  • 由一个组件,向上找到最近的指定组件;
  • 由一个组件,向上找到所有的指定组件;
  • 由一个组件,向下找到最近的指定组件;
  • 由一个组件,向下找到所有指定的组件;
  • 由一个组件,找到指定组件的兄弟组件。

5 个不同的场景,对应 5 个不同的函数,实现原理也大同小异。5 个函数的原理,都是通过递归、遍历,找到指定组件的 name 选项匹配的组件实例并返回。

向上找到最近的指定组件——findComponentUpward

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 由一个组件,向上找到最近的指定组件
* @param {Object} context - 当前上下文,比如你要基于哪个组件来向上寻找,一般都是基于当前的组件,也就是传入 this
* @param {String} componentName - 要找的组件的 name
* @return {Object} parent - 返回的就是我们要找的组件
*/
function findComponentUpward(context, componentName) {
let parent = context.$parent;
let name = parent.$options.name;

while (parent && (!name || [componentName].indexOf(name) < 0)) {
parent = parent.$parent;
if (parent) name = parent.$options.name;
}
return parent;
}

findComponentUpward 方法会在 while 语句里不断向上覆盖当前的 parent 对象,通过判断组件(即 parent)的 name 与传入的 componentName 是否一致,直到直到最近的一个组件为止。与 dispatch 不同的是,findComponentUpward 是直接拿到组件的实例,而非通过事件通知组件。

向上找到所有的指定组件——findComponentsUpward

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 由一个组件,向上找到所有的指定组件
* @param {Object} context - 当前上下文,一般都基于当前组件,也就是传 this
* @param {string} componentName - 要找的组件的 name
* @return {Array} parents - 要找的所有组件
*/
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 [];
}
}

indComponentsUpward 的使用场景较少,一般只用在递归组件里面,因为这个函数是一直向上寻找父级(parent)的,只有递归组件的父级才是自身。

向下找到最近的指定组件——findComponentDownward

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 由一个组件,向下找到最近的指定组件
* @param {Object} context - 当前上下文,一般都基于当前组件,也就是传 this
* @param {String} componentName - 要找的组件的 name
* @return {Object} children - 要找的所有组件
*/
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,直到找到最近的一个为止。

向下找到所有指定的组件——findComponentsDownward

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 由一个组件,向下找到所有指定的组件
* @param {Object} context - 当前上下文,一般都基于当前组件,也就是传 this
* @param {string} componentName - 要找的组件的 name
* @return {Object} 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 comopnents.concat(foundChilds);
}, []);
}

这个函数实现的方式有很多,这里巧妙使用 reduce 做累加器,并用递归将找到的组件合并为一个数组并返回,代码量较少,但理解起来稍困难。用法与 findComponentDownward 大同小异

找到指定组件的兄弟组件——findBrothersComponents

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 由一个组件,找到指定组件的兄弟组件
* @param {Object} context - 当前上下文,一般都基于当前组件,也就是传 this
* @param {String} componentName - 要找的组件的 name
* @param {Boolean} exceptMe - 是否包含组件自身
* @return {Object} children - 要找的所有组件
*/
function findBrothersComopnents(context, componentName, exceptMe = true) {
let res = context.$parent.$children.filter(item => item.$options.name === componentName);
let index = res.findIndex(item => item._uid === context._uid);
if (exceptMe) res.splice(index, 1);
return res;
}

相比其他 4 个函数, findBrothersComponents 多了一个参数exceptMe,是否把本身除外,默认是 true。寻找兄弟组件的方法,是先获取 context.$parent.$children,也就是父组件的全部子组件,这里面当然包含了本身,所以也会有第三个参数 exceptMe。

Vue 在渲染组件时,都会给每个组件加一个内置属性 _uid,这个 _uid 是不会重复的,借此我们可以从一系列兄弟组件中把自己排除掉

Author: Fridolph
Link: http://blog.fridolph.wang/2019/01/10/【Vue】组件间通信和几个常用方法/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.