Element-ui 之跨级实现组件间通信 Emitter

285 阅读1分钟

Element-ui 的源代码中,Emitter 用于跨级实现组件间的通信,本文将从 Emitter 的基本概念、使用方法和源代码这几个方面来进行解析。

一. 基本概念

Emitter 用于跨级实现组件间的通信,通过层层查找对应组件的事件名称来实现通信。当混入 Emitter 时,每一个组件都可以通过 dispatch 来向上级派发事件,或者通过 broadcast 来向下级派发事件。

二. 使用方法

2.1 dispatch 的使用

Emitter 提供 dispatch 方法,作用是向上级派发事件。下面以 Radio 组件和 Radio-Group 组件为例,来介绍一下使用方法:

Radio 组件的 value 值发生改变时,需要触发 Radio-Group 组件的 input 事件,此时 Radio 组件需要用到 dispatch 来向上级 Radio-Group 组件派发事件。

Radio 组件:

model: {
  ...
  set(val) {
    if (this.isGroup) {
      // 如果 Radio 组件处于按钮组中,则通过 dispatch 找到对应的上级 Radio-Group,并且触发其 input 事件
      this.dispatch('ElRadioGroup', 'input', [val]);
    } else {
      this.$emit('input', val);
    }
    ...
  }
},

2.2 broadcast 的使用

Emitter 提供 broadcast 方法,作用是向下级派发事件。下面以 Tree 树形组件为例,介绍一下使用方法:

该组件是由 TreeTree-node 组成,当展开树形的下级节点时,如果设置了 accordiontrue,表示每次只打开一个同级树节点,则在 Tree 组件的处理打开下级节点时,需要用 broadcast 来向下级 Tree-node 组件派发事件。

handleNodeExpand(nodeData, node, instance) {
  this.broadcast('ElTreeNode', 'tree-node-expand', node);
  ...
},

Tree-node 组件用 $on 监听当前实例的自定义事件:

created() {
  if(this.tree.accordion) {
    // 监听当前实例的自定义事件 tree-node-expand
    this.$on('tree-node-expand', node => {
      if(this.node !== node) {
        this.node.collapse();
      }
    });
  }
}

三. 源代码分析

Emitter 的源代码在 src/mixins/emitter.js 中:

function broadcast(componentName, eventName, params) {
  // 遍历当前节点下所有子组件
  this.$children.forEach(child => {
    // 用变量 name 记录子组件名称
    var name = child.$options.componentName;
    // 如果变量 name 与要查找的组件的 componentName 相等
    if (name === componentName) {
      // 说明找到对应的子组件了,则用emit触发事件
      child.$emit.apply(child, [eventName].concat(params));
    } else {
      // 没找到则递归遍历更深层次的子组件
      broadcast.apply(child, [componentName, eventName].concat([params]));
    }
  });
}
export default {
  methods: {
    // 一层一层的向上查找父组件,调用父组件中的方法
    dispatch(componentName, eventName, params) {
      // 获取当前元素的父组件,或者是根组件
      var parent = this.$parent || this.$root;
      // 用变量 name 记录当前元素父组件的组件名称
      var name = parent.$options.componentName;
      
      // 如果存在父组件,父组件的组件名称不存在,或者父组件的组件名称与传入的组件名称不一致,则继续向上查找
      while (parent && (!name || name !== componentName)) {
        parent = parent.$parent;
        // 继续记录父组件名称
        if (parent) {
          name = parent.$options.componentName;
        }
      }
      if (parent) {
        // 找到后调用emit触发事件
        parent.$emit.apply(parent, [eventName].concat(params));
      }
    },
    // 一层一层的向下查找子组件,调用子组件中的方法
    broadcast(componentName, eventName, params) {
      broadcast.call(this, componentName, eventName, params);
    }
  }
};