Skip to content

Vue2项目资料:iView 组件通信

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

<!-- more -->

组件间通信

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

组件间通信方式

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 的子组件。

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

js
/**
 * 由一个组件,向上找到最近的指定组件
 * @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) &lt; 0)) {
    parent = parent.$parent;
    if (parent) name = parent.$options.name;
  }
  return parent;
}

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

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

js
/**
 * 由一个组件,向上找到所有的指定组件
 * @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

js
/**
 * 由一个组件,向下找到最近的指定组件
 * @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

js
/**
 * 由一个组件,向下找到所有指定的组件
 * @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

js
/**
 * 由一个组件,找到指定组件的兄弟组件
 * @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 是不会重复的,借此我们可以从一系列兄弟组件中把自己排除掉

共 20 个模块,1301 篇 Markdown 文档。