根据源码,模拟实现 RabbitMQ - 从需求分析到实现核心类(1)

news2024/11/25 6:35:13

目录

一、需求分析

1.1、对 Message Queue 的认识

1.2、消息队列核心概念

1.3、Broker Server 内部关键概念

1.4、Broker Server 核心 API (重点实现)

1.5、交换机类型

Direct 直接交换机

Fanout 扇出交换机

Topic 主题交换机

1.6、持久化

1.7、网络通信

通信流程

远程调用设计思想

1.8、模块设计图

二、实现核心类

2.1、交换机和队列的属性及绑定关系

2.2、Message 消息


一、需求分析


1.1、对 Message Queue 的认识

消息队列就是把 阻塞队列 这样的数据结构,单独提取成了一个独立的程序进行部署,实现 “进程和进程之间 / 服务和服务之间” 的生产者消费者模型(分布式系统中则是一组服务器构成的集群).

生产者消费者模型的好处如下:

  1. 解耦合:在一个分布式系统中 服务器A 给 服务器B 发送请求,B 给 A 返回响应,这样 A 和 B 的耦合是比较大的(B 一旦挂了,A 这边也无法正常接收响应);引入消息队列后,A 把请求发送给消息队列,B 再从消息队列中获取请求,就降低了耦合度(哪怕 B 挂了,A 也不用管,继续发送请求给消息队列,队列反馈响应).
  2. 削峰填谷:假设这样一个常见, A 是入口服务器, A 再调用 B 完成一些具体的业务,如果是 A 和 B 直接通信,突然有一天 A 收到用户请求的峰值,此时 B 也会随之感受到峰值;引入消息队列之后, A 把请求发给队列,这时候 B 就可以按照自己原有的节奏从队列中取请求,不至于一下子收到太多的并发量.

1.2、消息队列核心概念

生产者(Producer):发布消息的客户端应用程序.

消费者(Consumer):订阅消息的客户端应用程序,用于处理生产者产生的消息.

中间人(Broker):消费者要拿到生产者的消息,需要经过中间人,用来削峰填谷.

发布(Publish):生产者向中间人这里投递消息的过程.

订阅(Subscribe):类似于订阅报纸,消费者要从中间人这里取到对应消息的前提是,先订阅消息.

消费(Consume):消费者从中间人这里取数据的操作.

1.3、Broker Server 内部关键概念

 

1.虚拟主机(Virtual Host):类似于 MySQL 中的 database,是一个 “逻辑” 上的数据集合.

实际的开发中,一个 Broker Server  也可以有多种不同类型的数据,可能会同时用来管理多组 业务线 上的数据,可以使用 Virtual Host 进行逻辑上的区分~

2.交换机(Exchange):实际上,生产者是先把消息给了 Broker Server 上的某一个交换机,再由交换机把消息转发给对应的队列.

交换机就类似于公司的前台小姐姐,有一天你来面试,你就告诉前台小姐姐,然后她就会把你带到对应的楼层(队列).

3.队列(Queue):在一个大的消息队列中,可以又很多哥具体的小队列,他们用来存储消息实体,后续消费者也是从对应的队列中取数据.

4.绑定(Binding):把交换机和队列之间,建立起关联关系.

可以把 交换机 和 队列 看作 数据库 中的 “多对多” 这样的关系.

5.消息(Message):服务器 A  给 B 发的请求(通过 MQ 转发),就是一个消息,服务器 B 给 A 返回的响应(通过 MQ 转发)也是一个消息.

一个消息,可以视为是一个 字符串(二进制数据)。消息中具体包含啥样的数据,都是程序员自定义的.

Ps:这些概念,既需要在内存中存储(方便,快),也需要在硬盘中存储(持久化).

1.4、Broker Server 核心 API (重点实现)

1. 创建队列(queueDeclare)

这里不使用 Create 创建 这样的术语,而使用 Declare ,是因为这里以 RabbitMQ 为蓝本, Declare 表示 不存在则创建,存在就什么也不干.

2.销毁队列(queueDelete)

3.创建交换机(exchangeDeclare)

4.销毁交换机(exchangeDelete)

5.创建绑定(queueBind)

6.解除绑定(queueUnbind)

7.发布消息(basicPublish)

8.订阅消息(basicConsume)

9.确定消息(basicAck)

类似于 TCP 的确认应答机制,确认消息这个 api 就是让消费者显式告诉 broker server ,这个消息,我已经处理完毕了,提高整个系统的可靠性.

客户端除了提供以上 9 中方法,还需要提供 4 个方法:

1.创建 Connection

2.关闭 Connection

3.创建 Channel

4.关闭 Channel

 一个 Connection 对象,就代表一个 TCP 连接,里面可以包含多个 Channel ,每隔 Channel 上面传输的数据都是互不相干的.

有了 Connection 了,为什么还要搞一个 Channel ?

TCP 中,建立 / 断开一个连接成本还是挺高的,因此,很多时候,不希望频繁的建立断开 TCP 连接,因此引入 Channel ,相比于 TCP,就要轻量很多. Connection 和 Channel 之间的关系,就类似于 “网线” 一样.

1.5、交换机类型

交换机在转发消息的时候,按照转发规则,提供了几种不同的 交换机类型(ExchangeType)来描述这里不同的转发规则.

RabbitMQ 主要实现了 四种 交换机类型(也是 AMQP 协议定义的)

  1. Direct 直接交换机
  2. Fanout 扇出交换机
  3. Topic 主题交换机
  4. Header 消息头交换机

Ps:这里主要实现前三种交换机类型,因为第四种交换机规则复杂,应用场景少,前三种就已经够用了.

Direct 直接交换机

有两个关键概念:

bindingKey:把队列和交换机绑定的时候,指定一个单词(类似于暗号).

routingKey:生产者发送消息的时候,也指定一个单词.

当 routingKey 和 BindingKey 对上暗号了,就可以把这个消息转发到对应的队列中了.

生产者发送消息的时候,会指定一个 “目标队列的名字”(routingKey),交换机收到之后,就看看绑定的队列里,有没有匹配的队列(BindingKey),如果有,就转发过去(把消息塞进对应的队列中),如果没有,消息直接丢弃.

Fanout 扇出交换机

会将接收到的消息广播到每一个跟其绑定的queue,最后交给对应的消费者,值得注意的是,exchange负责消息路由,而不是存储,路由失败则消息丢失。

Topic 主题交换机

有两个关键概念:

bindingKey:把队列和交换机绑定的时候,指定一个单词(类似于暗号).

routingKey:生产者发送消息的时候,也指定一个单词.

当 routingKey 和 BindingKey 对上暗号了,就可以把这个消息转发到对应的队列中了.

Ps:TopicExchange 与 DirectExchange 十分类似,区别在于routingKey必须是多个单词的列表,并且以 分割。

1.6、持久化

上述这些概念(交换机、队列、绑定、消息....)对应的数据,都需要存储和管理起来,此时内存和硬盘都会各自存储一份,内存为主,硬盘为辅.

在内存中存储的原因:对于 MQ 来说,能够高效的转发处理数据式非常关键的,因此在内存上组织数据比硬盘上要快的多.

在硬盘上存储的原因:防止内存中的数据随着 进程重启/主机重启 而丢失.

Ps:硬盘存储,这个持久化是相对于 内存 的,对于一个硬盘来说,存储的消息寿命一般为 几年~几十年(一直不通电的情况下).

1.7、网络通信

通信流程

⽣产者和消费者都是客⼾端程序, broker server 则是作为服务器. 通过⽹络进⾏通信.

例如如下过程:

  1. 客户端发送请求:生产者的代码中需要有一个方法 queueDeclare 来创建队列,这个方法内部要做的事情就是给服务器发送一个请求,告诉服务器,咱们要创建一个队列,以及队列长啥样子......
  2. 服务器处理请求,并返回响应:broker server 收到这个请求之后,再执行服务器这边的 queueDeclare 方法,真正取给 内存/硬盘 上写一些数据,把这个队列给真正创建出来,再把创建 成功/失败 结果包装成响应,返回给客户端.
  3. 客户端接收响应:当响应回来了,客户端的 queueDeclare 就会获取到这个响应,看到 创建队列成功,此时 queueDeclare 就算执行完毕了.

远程调用设计思想

此处,客户端调用了一个本地方法,结果这个方法的背后,又给服务器发送了一系列消息,由服务器完成了一系列工作,站在调用者的角度,只是看到了这个功能完成了,并不知道背后的实现细节.

虽然调用的是一个本地方法,但实际上好像调用另一个远端服务器的方法一样~

这里可以认为是编写客户端服务器程序,通信过程中的一种设计思想——远程调用(RPC)

举个例子

以前有个美国的老哥,再大厂干活,但是他特别想摸鱼,于是他就找了一个中国人帮他干(有偿). 同样级别的程序员,在美国工作的薪水是国内的 3-4 倍.

1.8、模块设计图

二、实现核心类


Ps:Spring boot、MyBatis

2.1、交换机和队列的属性及绑定关系

我们可以使用 一个枚举类 表示三种交换机.

public enum ExchangeType {

    DIRECT(0),
    FANOUT(1),
    TOPIC(2);

    private final int type;

    private ExchangeType(int type) {
        this.type = type;
    }

    public int getType() {
        return type;
    }

}

交换机属性如下

public class Exchange {

    //模仿 rabbitmq 使用 name 作为唯一身份标识
    private String name;
    //交换价类型,DIRECT,FANOUT,TOPIC
    private ExchangeType type = ExchangeType.DIRECT;
    //当前交换机是否要持久化存储,true 标识持久化.
    private boolean durable = false;
    //TODO: 若当前交换机没人使用了,就会自动删除
    private boolean autoDelete = false;
    //TODO: 表示创建交换机时可以指定一些额外的选项
    private Map<String, Object> arguments = new HashMap<>();

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public ExchangeType getType() {
        return type;
    }

    public void setType(ExchangeType type) {
        this.type = type;
    }

    public boolean isDurable() {
        return durable;
    }

    public void setDurable(boolean durable) {
        this.durable = durable;
    }

    public boolean isAutoDelete() {
        return autoDelete;
    }

    public void setAutoDelete(boolean autoDelete) {
        this.autoDelete = autoDelete;
    }

    public Map<String, Object> getArguments() {
        return arguments;
    }

    public void setArguments(Map<String, Object> arguments) {
        this.arguments = arguments;
    }
}

队列属性如下

public class MSGQueue {
    //表示队列的身份标识
    private String name;
    //是否持久化,true 表示支持
    private boolean durable;
    //TODO: 这个属性为 true,表示队列只能被一个消费者使用, false表示大家都能用
    private boolean exclusive = false;
    //TODO: 自动删除,为 true 表示没有人使用以后自动删除
    private boolean autoDelete = false;
    //TODO: 扩展参数,自定义选项
    private Map<String, Object> arguments = new HashMap<>();

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public boolean isDurable() {
        return durable;
    }

    public void setDurable(boolean durable) {
        this.durable = durable;
    }

    public boolean isExclusive() {
        return exclusive;
    }

    public void setExclusive(boolean exclusive) {
        this.exclusive = exclusive;
    }

    public boolean isAutoDelete() {
        return autoDelete;
    }

    public void setAutoDelete(boolean autoDelete) {
        this.autoDelete = autoDelete;
    }

    public Map<String, Object> getArguments() {
        return arguments;
    }

    public void setArguments(Map<String, Object> arguments) {
        this.arguments = arguments;
    }
}

交换机和队列绑定关系如下

public class Binding {
    //交换机身份标识
    private String exchangeName;
    //队列身份标识
    private String queueName;
    private String bindingKey;

    //绑定没必要设置持久化,因为没有意义,他存在的意义前提是 交换机和队列存在(持久化)

    public String getExchangeName() {
        return exchangeName;
    }

    public void setExchangeName(String exchangeName) {
        this.exchangeName = exchangeName;
    }

    public String getQueueName() {
        return queueName;
    }

    public void setQueueName(String queueName) {
        this.queueName = queueName;
    }

    public String getBindingKey() {
        return bindingKey;
    }

    public void setBindingKey(String bindingKey) {
        this.bindingKey = bindingKey;
    }
}

2.2、Message 消息

Message 消息主要分为:

  1.  属性部分 BasicProperties(网络):这是一个自定义类,里面承载着 Message 核心属性,如 消息的唯一身份标识、routingKey、是否需要持久化.......
  2.  正文部分 byte[]:承载着具体的消息内容.
  3. 辅助属性:通过 offsetBeg 和 offsetEnd 快速找到文件中某一个消息的具体位置(一个文件中会存储很多消息),这里约定的规则为 “前闭后开”(以字节为单位);isValid 标识消息要删除,如果删除采用逻辑删除(并不是真的删除,而是标记为无效数据),这里约定 0x1 为有效数据、0x0 表示无效数据.

Ps:前两个信息需要在网络上传输,并且写入文件,因此就需要通过 Serializable 序列化和反序列化(直接继承接口即可,不需要具体实现)~~ 

而第三条不需要被序列化保存到文件中,这个属性主要是为了内存中的 Message 对象快速找到 文件中的 Message  对象,因此使用 trasient 修饰即可防止序列化 

public class Message implements Serializable {
    //这两个属性是 Message 最核心的属性
    private BasicProperties basicProperties = new BasicProperties();
    private byte[] body;

    //以下是辅助类型的属性
    private transient long offsetBeg = 0; //消息数据的开头距离文件开头的位置偏移量(字节)
    private transient long offsetEnd = 0; //消息数据的结尾距离文件开头的位置偏移量(字节)
    //表示文件中的消息是否是有效消息(对文件中的消息,如果删除,使用逻辑删除)
    // 0x1 表示有效, 0x0 表示无效
    private byte isValid = 0x1;

    //创建一个工厂方法,让工厂方法创建 Message 对象
    //此方法中创建的 Message 对象,会自动生成唯一的 MessageId
    //万一 routingKey 和 basicProperties 里的 routingKey 冲突,以外面为主
    public static Message createMessageWithId(String routingKey, BasicProperties basicProperties, byte[] body) {
        Message message = new Message();
        if(basicProperties != null) {
            message.setBasicProperties(basicProperties);
        }
        //生成的 MessageId 以 M- 作为前缀
        message.setMessageId("M-" + UUID.randomUUID());
        message.setRoutingKey(routingKey);
        message.body = body;
        // offsetBeg, offsetEnd, isValid ,是消息持久化的时候才会用到,消息写入文件之前在进行设定
        return message;
    }

    public String getMessageId() {
        return basicProperties.getMessageId();
    }

    public void setMessageId(String messageId) {
        basicProperties.setMessageId(messageId);
    }

    public String getRoutingKey() {
        return basicProperties.getRoutingKey();
    }

    public void setRoutingKey(String routingKey) {
        basicProperties.setRoutingKey(routingKey);
    }

    public int getDeliverMode() {
        return basicProperties.getDeliverMode();
    }

    public void setDeliverMode(int mode) {
        basicProperties.setDeliverMode(mode);
    }

    public BasicProperties getBasicProperties() {
        return basicProperties;
    }

    public void setBasicProperties(BasicProperties basicProperties) {
        this.basicProperties = basicProperties;
    }

    public byte[] getBody() {
        return body;
    }

    public void setBody(byte[] body) {
        this.body = body;
    }

    public long getOffsetBeg() {
        return offsetBeg;
    }

    public void setOffsetBeg(long offsetBeg) {
        this.offsetBeg = offsetBeg;
    }

    public long getOffsetEnd() {
        return offsetEnd;
    }

    public void setOffsetEnd(long offsetEnd) {
        this.offsetEnd = offsetEnd;
    }

    public byte getIsValid() {
        return isValid;
    }

    public void setIsValid(byte isValid) {
        this.isValid = isValid;
    }
}

Message 中的核心属性如下

public class BasicProperties implements Serializable {

    //消息的唯一标识(使用 UUID 保证唯一性)
    private String messageId;
    //和 bindingKey 做匹配
    //如果当前交换机类型是 DIRECT ,此时 routingKey 就表示要转发的队列名
    //如果当前交换机类型是 FANOUT ,此时 routingKey 无意义(不使用)
    //如果当前交换机类型是 TOPIC ,此时 routingKey 就是要和 bindingKey 做匹配,匹配才进行转发
    private String routingKey;
    //标识消息是否要持久化,1 表示不持久化, 2 表示持久化(rabbitmq 是这么搞得)
    private int deliverMode = 1;

    //RabbitMQ 还有其他属性,这里就不考虑了


    public String getMessageId() {
        return messageId;
    }

    public void setMessageId(String messageId) {
        this.messageId = messageId;
    }

    public String getRoutingKey() {
        return routingKey;
    }

    public void setRoutingKey(String routingKey) {
        this.routingKey = routingKey;
    }

    public int getDeliverMode() {
        return deliverMode;
    }

    public void setDeliverMode(int deliverMode) {
        this.deliverMode = deliverMode;
    }
}

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/868167.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

美团软件测试工程师高频面试题和答案

前言 8月底了&#xff0c;马上到了大家的找工作的高峰期了&#xff01;为了帮助大家更好的备战面试和跳槽&#xff0c;可以在众多求职者中脱颖而出&#xff0c;我帮大家准备了丰富的企业真实面试题&#xff0c;大家赶紧收藏吧&#xff01; 1、说下你最近做的项目&#xff0c;你…

C语言学习系列-->看淡指针(2)

文章目录 前言一、数组名的理解二、使用指针访问数组三、一维数组传参本质四、二级指针五、指针数组六、指针数组模拟二维数组 前言 不把指针学的扎实&#xff0c;可不敢说自己C语言基础学的好 一、数组名的理解 #include <stdio.h> int main() {int arr[10] { 1,2,3,4…

利用python实现批量登录网络设备进行日常巡检

利用python实现批量登录网络设备 实现ensp与物理机互通ensp 配置配置网络设备远程登录 用python实现批量登录常见问题 通过阅读本文可以学习自动化运维相关知识&#xff0c;本文章代码可以直接使用&#xff0c;通过批量登录功能后&#xff0c;可以按照自己意愿进行功能更改与完…

电路基础笔记(更新中)

导体 导体是指那些能够轻易传递电荷&#xff08;电子&#xff09;的物质。在固体、液体或气体中&#xff0c;电子可以在导体内部自由移动&#xff0c;从而形成电流。导体的电导性取决于它的电子结构和能带特性。常见的导体包括金属&#xff08;如铜、铝、铁等&#xff09;以及一…

UNIX网络编程——TCP协议API 基础demo服务器代码

目录 一.TCP客户端API 1.创建套接字 2.connect连接服务器​编辑 3.send发送信息 4.recv接受信息 5.close 二.TCP服务器API 1.socket创建tcp套接字(监听套接字) 2.bind给服务器套接字绑定port,ip地址信息 3.listen监听并创建连接队列 4.accept提取客户端的连接 5.send,r…

怎么做Tik Tok海外娱乐公会呢?新加坡市场怎么样?

一、为什么选择TikTok直播 1. 海外市场潜力巨大 • 自2016年始&#xff0c;多家直播平台陆续拓展至东南亚、中东、俄罗斯、日韩、欧美、拉美等地区。 • 海外市场作为直播发展新蓝海&#xff0c;2021年直播行业整申请cmxyci体规模达百亿美元&#xff0c;并维持高速增长。 &a…

在阿里云服务器上安装部署WDCP主机管理系统教程

阿里云百科分享在阿里云服务器上安装部署WDCP主机管理系统流程&#xff0c;WDCP&#xff08;WDlinux Control Panel&#xff09;是一套Linux服务器及虚拟主机管理系统&#xff0c;通过Web控制和管理服务器。在WDCP的后台中&#xff0c;您可以更方便地使用Linux系统作为网站服务…

STM32CubeMX之freeRTOS消息队列

创建一个消息队列&#xff0c;两个发送任务&#xff0c;一个接受任务 发送任务一&#xff1a;等待时间为0 发送任务二&#xff1a;等待时间为最大 接受为0 简单来说就是&#xff1a; 任务一&#xff1a;一个普写 一个死写 一个普读 任务二&#xff1a;创造队列 一个普写 …

JDBC快速入门操作

一、jdbc简介 JDBC是java用于连接数据库的api&#xff0c;数据库软件有多种&#xff0c;像MySQL,SQLsever&#xff0c;Oracle等数据库&#xff0c;这些数据库都是由不同的团队开发的&#xff0c;所以相同的功能的api的名字不同&#xff0c;当一个后端工程需要切换一个数据库软件…

Java 实现敏感词检测

敏感词检测 敏感词的检测&#xff0c;一般是建立一个敏感词库&#xff0c;然后判断字符串中是否存在敏感词库中的某些词汇&#xff0c;然后将其过滤或者替换显示为其他文本&#xff0c;这对于一个和谐的网络环境是及其必要的&#xff0c;接下来就我们看看敏感词检测的实现方式…

Spring MVC 简介

目录 1. 什么是MVC2. 什么是SpringMVC 1. 什么是MVC MVC是一种常用的软件架构模式。可以看作是一种设计模式&#xff0c;也可以看作是一种软件框架。经典MVC模式中&#xff0c;M是指模型&#xff0c;V是视图&#xff0c;C则是控制器&#xff0c;使用MVC的目的是将M和V的实现代…

最强自动化测试框架Playwright(11)- 录制视频

视频 使用playwright&#xff0c;您可以录制测试视频。 录制视频 视频在测试结束时在浏览器上下文关闭时保存。如果手动创建浏览器上下文&#xff0c;请确保等待 browser_context.close&#xff08;&#xff09;。 context browser.new_context(record_video_dir"vid…

【Java】 java | git | win系统重装会给开发环境带来哪些问题

一、概述 1、近期发现电脑用起来不丝滑了&#xff0c;文件夹操作卡顿&#xff0c;一阵操作还会蓝屏 2、不能忍&#xff0c;整理排查 二、电脑情况 1、CPU&#xff1a; I5-9400F 2.9GHz 6核 2、内存&#xff1a; 32G 3、固态&#xff1a;256G 4、机械&#xff1a;1T 5、盘符使用…

Java基础篇--Number Math 类

目录 Number类 扩展小知识 Math类 实例 Number类 Java中的Number类是一个抽象类&#xff0c;它是所有包装类&#xff08;如Integer、Double、Long等&#xff09;的父类。这个类提供了将基本数据类型&#xff08;如int、double、long等&#xff09;封装为对象&#xff0c;…

[Leetcode] [Tutorial] 多维动态规划

文章目录 62. 不同路径Solution 62. 不同路径 一个机器人位于一个 m ∗ * ∗ n 网格的左上角 &#xff08;起始点在下图中标记为 “Start” &#xff09;。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角。 问总共有多少条不同的路径&#xff1f; 示例…

QT-K线效果显示

QT-K线效果显示 一、演示效果二、关键程序三、程序链接 一、演示效果 二、关键程序 代码如下&#xff1a; #include "kvolumegrid.h"#include <QMessageBox> #include <QPainter> #include <QPen> #include <QString>kVolumeGrid::kVolume…

OpenCV实例(八)车牌字符识别技术(二)字符识别

车牌字符识别技术&#xff08;二&#xff09;字符识别 1.字符识别原理及其发展阶段2.字符识别方法3.英文、数字识别4.车牌定位实例 1.字符识别原理及其发展阶段 匹配判别是字符识别的基本思想&#xff0c;与其他模式识别的应用非常类似。字符识别的基本原理就是对字符图像进行…

iphone拷贝照片中间带E自动去重软件,以及java程序如何打包成jar和exe

文章目录 一、前提二、问题描述三、原始处理方式四、程序处理4.1 java程序如何打包exe4.1.1 首先打包jar4.1.2 开始生成exe软件使用方式 4.2 更换图标4.2.1 更换swing的打包jar图标4.2.2 更换exe图标 4.2 附件下载 一、前提 用苹果手机照相&#xff0c;有不使用默认的4:3拍照的…

【大数据之Kafka】一、Kafka定义消息队列及基础架构

1 定义 Kafka传统定义&#xff1a;Kafka是一个分布式的基于发布/订阅模式的消息队列&#xff08;Message Queue&#xff09;&#xff0c;主要应用于大数据实时处理领域。 发布/订阅&#xff1a;消息的发布者不会将消息直接发送给特定的订阅者&#xff0c;而是将发布的消息分为…

React 组件防止冒泡方法

背景 在使用 antd 组件库开发时&#xff0c;发现点击一个子组件&#xff0c;却触发了父组件的点击事件&#xff0c;比如&#xff0c;我在一个折叠面板里面放入一个下拉框或者对下拉框列表渲染做定制&#xff0c;每个下拉框候选项都有一个子组件… 解决 其实这就是 Javascri…