vue中跨无限不同层级父子组件通信优雅解决方案

1,237 阅读2分钟

概述

在vue中,我们常见的组件通信方式有如下几种:

  • 父传子
    • props,ref
  • 子传父
    • emit,emit,on
    • props(父组件传递函数过来也行)
  • 非父子组件通信
    • vuex
    • 自定义的发布订阅实现
  • 跨多个层级的父子组件传值
    • provide和inject(使用较少,不常用)

比较常见的几种传值通信方式主要为上面几种,基本上满足我们项目需求,从上面几种通信方式中,来重点看看emitemit和on,在了解它的实现原理之前,我们先看看设计模式中的发布订阅

发布订阅

官方术语

  • 软件架构中,发布订阅是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)。而是将发布的消息分为不同的类别,无需了解哪些订阅者(如果有的话)可能存在。同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者(如果有的话)存在。

个人理解

发布订阅模式可以类比到日常生活中,比如销售房子,我们需要买一套房子,到售楼处去看了,当时不满意,看了又看,订阅了售楼处的新房通知,等到有新房可售的时候,对方(发布者)会通知我们(订阅者)。

类比前端实际开发

在dom操作中,必不可少的会给dom元素绑定事件处理函数,比如给一个按钮绑定了一个事件处理函数,等到我们点击页面按钮的时候,就会触发绑定的dom元素的事件触发。

类别到vue中

on就相当于订阅者订阅事件处理函数,on就相当于订阅者订阅事件处理函数,emit就相当于发布者发布事件,触发处理函数,我们经常给组件上面绑定自定义事件处理函数,然后通过$emit的方式触发,其实就是发布订阅模式的实现。

emitemit和on

本质是,在组件上面的自定义事件其实就是下面这种写法的另一种形式。

 mounted() {
    //订阅事件
    this.$on("onChange", () => {
      console.log("测试出发了");
    });
  },
   methods: {
     handleTestClick() {
     //这里就会触发上面mounted里面的订阅的回调函数
      this.$emit("onChange");
    },
   }

自己实现一个发布订阅

在了解了发布订阅之后,其实实现起来还是蛮简单的,我们需要一个发布者,订阅者,以及保存这些回调函数的一个事件队列。

class Pubsub {
  constructor() {
  //保存事件类型和对应回调函数的队列
    this.queenList = [];
  }
  //   订阅事件
  subscribe(eventName, fn) {
    let cur = this.queenList.find((item) => item.eventName == eventName);
    if (cur) {
      cur.callback.push(fn);
    } else {
      this.queenList.push({
        eventName,
        callback: [fn],
      });
    }
  }
  //   发布事件
  notice(eventName, ...args) {
    let cur = this.queenList.find((item) => item.eventName == eventName);
    if (cur) {
      cur.callback.forEach((fn) => {
        fn(...args);
      });
    }
  }
  // 清空事件队列
  clearEventQueen() {
    this.queenList = [];
  }
}

//使用
let pubsub=new Pubsub()
//订阅事件
pubsub.subscribe("onChange",(val)=>{
console.log("我订阅了一个事件,传递过来的参数是"+val)
})
//发布事件
pubsub.notice("onChange",2)

vue中无限层级父子组件通信优雅解决方案

在上面都掌握之后,我们来看看我们使用组件库的时候,会发现的一个问题,拿elementui中的select下拉组件来说,先看具体用法.

<el-select v-model="value" placeholder="请选择"> 
    <el-option 
    v-for="item in options" 
    :key="item.value" 
    :label="item.label"
    :value="item.value"> 
    </el-option> 
</el-select>

思考一个问题

有没有发现,select组件并不是一个组件实现的,而是通过select组件嵌套option组件,而且基本上所有组件库的思路都是这样,按照我们业务开发组件,肯定直接一个select组件,然后在select内部去使用option进行v-for,这样子避免组件之前的传值问题不好解决。但是组件库却不是这样的,其好处就是我们可以更好的扩展,我们视乎使用组件的时候,多了更多的选择,我们自己业务组件,也不给我别人用,当然不需要考虑这么多,因此我们可能会想,这样子的话,组件之间的通信岂不是很麻烦了吗?

解决方案

其实这就涉及到了跨多个层级之间的父子组件通信,我们可以使用发布订阅来解决。以下是处理方案。

/**
 * @Description 由于涉及到跨组件之间通信,因此我们只有自己实现发布订阅的模式,来实现组件之间通信,灵感主要来源于element-ui组件库源码中跨层级父子组件通信方案,本质上也是发布订阅和$emit和$on
 * @param { String } componentName 组件名
 * @param { String } eventName 事件名
 * @param { argument } params 参数
 **/

// 广播通知事件
function _broadcast(componentName, eventName, params) {
  // 遍历当前组件的子组件
  this.$children.forEach(function (child) {
    // 取出componentName,组件options上面可以自己配置
    var name = child.$options.componentName;
    // 如果找到了需要通知的组件名,触发组件上面的$eimit方法,触发自定义事件
    if (name === componentName) {
      child.$emit.apply(child, [eventName].concat(params));
    } else {
      // 没找到,递归往下找
      _broadcast.apply(child, [componentName, eventName].concat([params]));
    }
  });
}
const emiiter = {
  methods: {
    // 派发事件(通知父组件)
    dispatch(componentName, eventName, params) {
      var parent = this.$parent || this.$root;
      var name = parent.$options.componentName;
      // 循环往上层父组件,知道知道组件名和需要触发的组件名相同即可,然后触发对应组件的事件
      while (parent && (!name || name !== componentName)) {
        parent = parent.$parent;
        if (parent) {
          name = parent.$options.componentName;
        }
      }
      if (parent) {
        parent.$emit.apply(parent, [eventName].concat(params));
      }
    },
    // 广播事件(通知子组件)
    broadcast(componentName, eventName, params) {
      _broadcast.call(this, componentName, eventName, params);
    },
  },
};

export default emiiter;

总结

发布订阅在前端中,是非常重要且常用的一种设计模式,框架的实现都离不开它,因此我们需要深刻理解,上面实现了vue中的跨不同层级的父子组件通信的问题,下一期我会通过手写组件库select组件的方式,带你掌握这种方案的用法。