C++实现分布式网络通信框架RPC(3)--rpc调用端
目录
一、前言
二、UserServiceRpc_Stub
三、 CallMethod方法的重写
头文件
实现
四、rpc调用端的调用
实现
五、 google::protobuf::RpcController *controller
头文件
实现
六、总结
一、前言
在前边的文章中,我们已经大致实现了rpc服务端的各项功能代码,接下来我们就来看看,如果一个rpc调用端想要调用都要干什么。
二、UserServiceRpc_Stub
我们在预备知识就提到过我们通过在 .proto 文件中使用service关键字定义了用来描述rpc方法的类型后
service UserServiceRpc{rpc Login(LoginRequest) returns(LoginResponse);}
使用 protoc 编译后会自动生成两个服务类,分别是 UserServiceRpc 、UserServiceRpc_Stub
UserServiceRpc是继承自Service这个类的,我们讲到过这个类是在rpc发布端使用的,而UserServiceRpc_Stub是继承自UserServiceRpc的,这个类是在rpc调用端使用的。
可以看到 UserServiceRpc_Stub这个类是没有默认构造函数的,想要构造这个类它必须传入 ::PROTOBUF_NAMESPACE_ID::RpcChannel* channel
我们再来看看这个RpcChannel是什么
class PROTOBUF_EXPORT RpcChannel {public:inline RpcChannel() {}virtual ~RpcChannel();// Call the given method of the remote service. The signature of this// procedure looks the same as Service::CallMethod(), but the requirements// are less strict in one important way: the request and response objects// need not be of any specific class as long as their descriptors are// method->input_type() and method->output_type().virtual void CallMethod(const MethodDescriptor* method,RpcController* controller, const Message* request,Message* response, Closure* done) = 0;private:GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(RpcChannel);
};
可以看到它是一个抽象类,所以我们肯定还要定义一个类Mprpcchannel来继承这个类并对该类中的CallMethod方法进行重写。
在预备知识一节中,我们已经提到了,无论是用service 关键字定义用来描述rpc方法的类型,无论定义几个该服务下的方法,最后对这些方法的调用都最终调用的是channel的CallMethod方法,即是在调用在创建 UserServiceRpc_Stub对象时传入的我们创建的Mrpcchannel中的CallMethod方法,所以我们就对CallMethod方法进行重写,集中来做所有rpc方法调用的参数的序列化和反序列化操作。
三、 CallMethod方法的重写
我们创建一个类Mrpcchannel用来继承UserServiceRpc_Stub类中的RpcChannel类并对类中的 CallMethod方法进行重写,在该方法中我们集中来做所有rpc方法调用的参数的序列化和反序列化操作。
CallMethod方法中需要做的事情:
- 将用户发送的消息按照双方协定的消息格式(header_size + header_str + args_str)序列化好。
- 通过网络发送,因为我们是客户端是不需要处理高并发的情况的,所以这里我们采用tcp编程就行。
- 接下来就是阻塞等待数据的响应。
- 最后拿到响应数据后反序列化之后返回给用户。
头文件
//mprpcchannel.h
class MprpcChannel:public google::protobuf::RpcChannel
{//所有通过stub代理对象调用的rpc方法,都是走到了这里,统一做rpc方法调用的数据序列化和网络发送void CallMethod(const google::protobuf::MethodDescriptor* method,google::protobuf::RpcController* controller, const google::protobuf::Message* request,google::protobuf::Message* response, google::protobuf::Closure* done);
};
实现
//mprpcchannel.cc
/*我们约定好在进行网络传输的时候的规则是 header_size + service_name + method_name + args_size +args*/
void MprpcChannel::CallMethod(const google::protobuf::MethodDescriptor *method,google::protobuf::RpcController *controller,const google::protobuf::Message *request,google::protobuf::Message *response,google::protobuf::Closure *done)//在调用端最后一个参数没用
{//通过method的service方法得到方法属于的服务类const google::protobuf::ServiceDescriptor *sd = method->service();std::string service_name = sd->name(); // service_namestd::string method_name = method->name(); // method_name// 获取参数的序列化字符串长度 args_sizeuint32_t args_size = 0;std::string args_str;if (request->SerializeToString(&args_str)){// 序列化成功args_size = args_str.size();}else{controller->SetFailed("Serialize request error!");return;}// 定义rpc的请求headermprpc::RpcHeader rpcHeader;rpcHeader.set_service_name(service_name);rpcHeader.set_method_name(method_name);rpcHeader.set_args_size(args_size);// 进行序列化uint32_t header_size = 0;std::string rpc_header_str;if (rpcHeader.SerializeToString(&rpc_header_str)) // 将东西设置进rpcHeader后进行序列化{header_size = rpc_header_str.size();}else{controller->SetFailed("Serialize rpc header error!");return;}// 组织待发送的rpc请求字符串std::string send_rpc_str;send_rpc_str.insert(0, std::string((char *)&header_size, 4));send_rpc_str += rpc_header_str;send_rpc_str += args_str;// 打印调试信息std::cout << "==================================================" << std::endl;std::cout << "header_size::" << header_size << std::endl;std::cout << "rpc_header_str::" << rpcHeader.DebugString() << std::endl;std::cout << "service_name::" << service_name << std::endl;std::cout << "method_name::" << method_name << std::endl;std::cout << "args_size::" << args_size << std::endl;std::cout << "args_str::" << args_str << std::endl;std::cout << "==================================================" << std::endl;// 由于这里是客户端,所以不需要处理高并发的情况,所以这边的网络发送我们采用tcp编程,完成rpc方法的远程调用int clientfd = socket(AF_INET, SOCK_STREAM, 0);if (-1 == clientfd){char errtxt[512] = {0};sprintf(errtxt, "creat socket error! errno:%d", errno);controller->SetFailed(errtxt);return;}// 将服务器的ip地址和端口号读一下// std::string ip = MprpcApplication::GetInstance().GetConfig().Load("rpcserverip");// uint16_t port= atoi(MprpcApplication::GetInstance().GetConfig().Load("rpcserverport").c_str());// 之前都是使用上面的方法来获取配置文件中的参数,但是现在我们想调用什么,就去zk上查询该服务所在的host信息zkClient zkCli;zkCli.Start();std::string method_path = "/" + service_name + "/" + method_name;std::string host_data = zkCli.GetData(method_path.c_str());if (host_data == ""){controller->SetFailed(method_name + "is not exist!");return;}int idx = host_data.find(":");if (idx == -1){controller->SetFailed(method_path + "address is invalid!");return;}std::string ip = host_data.substr(0, idx);uint16_t port = atoi(host_data.substr(idx + 1, host_data.size() - idx).c_str());struct sockaddr_in server_addr;server_addr.sin_addr.s_addr = inet_addr(ip.c_str());server_addr.sin_family = AF_INET;server_addr.sin_port = htons(port);// 连接rpc服务节点if (-1 == connect(clientfd, (struct sockaddr *)&server_addr, sizeof(server_addr))){close(clientfd);char errtxt[512] = {0};sprintf(errtxt, "connect error! errno:%d", errno);controller->SetFailed(errtxt);return;}// 发送rpc请求if (-1 == send(clientfd, send_rpc_str.c_str(), send_rpc_str.size(), 0)){close(clientfd);char errtxt[512] = {0};sprintf(errtxt, "send error! errno:%d", errno);controller->SetFailed(errtxt);// exit(EXIT_FAILURE);这里不能因为没有成功发送而整个的服务全部推出了return;}// 接收rpc端发来的请求响应值char recv_buf[1024] = {0};int recv_size = 0; // 整个接收的缓冲区中不可能全是发送过来的数据吧,所以记录一下多少数据if (-1 == (recv_size = recv(clientfd, recv_buf, 1024, 0))){close(clientfd);char errtxt[512] = {0};sprintf(errtxt, "recv error! errno:%d", errno);controller->SetFailed(errtxt);return;}// 接下来就是把接收到的数据填进response中,这样框架就可以通过response知道响应值并返回给用户// std::string response_str(recv_buf,0,recv_size);//string有构造函数可以使用recv_buf来初始化,即将recv_buf的从0到recv_size的这一段数据初始化response_str// 有bug,recv_buf在遇到\0后面的数据就存不下来了****std::string response_str(recv_buf,recv_size);也可以// if(!response->ParseFromString(response_str))反序列化rpc调用的响应数据if (!response->ParseFromArray(recv_buf, recv_size)){close(clientfd);char errtxt[512] = {0};sprintf(errtxt, "parse error! response_str:%d", recv_buf);controller->SetFailed(errtxt);return;}close(clientfd);
}
我们在调用rpc服务的时候, 我们用的是相应的描述rpc的服务的 UserServiceRpc_Stub这个类,在使用这个类的时候,我们需要传入一个Rpcchannel这个类
UserServiceRpc_Stub(::PROTOBUF_NAMESPACE_ID::RpcChannel* channel);
然后在这个UserServiceRpc_Stub这个类中调用的所有的 rpc 方法,最终都跑到了调用channel的CallMethod方法下。举个例子如下
UserServiceRpc_Stub stub(new MprpcChannel());
stub.Login(nullptr,&request,&response,nullptr);
像这样的通过stub调用的Login方法都是调用的channel的CallMethod方法。
四、rpc调用端的调用
在这里我们需要考虑如何去发起一个rpc调用,去调用某个服务下的某个方法,下面都以UserServiceRpc服务下的Login方法为例,我们同样要定义一个与发布端相同的 user.proto文件。然后使用 protoc 编译,接着
- 对调用的rpc框架进行初始化操作
- 实例化出一个代理对象 fixbug::UserServiceRpc_Stub stub;后面都会通过stub来调用rpc方法。
- 填写请求方法的参数
- 用stub代理调用Login方法
- 等待读取返回值。
实现
//calleruserservice.cc
int main(int argc,char* argv[])
{//先调用框架的初始化类且全局只调用这一次//MprpcApplication& myin= MprpcApplication::GetInstance();//myin.Init(argc,argv);它是静态函数,也就是说它和具体对象无关,可以直接通过类名调用。//因为你拿到对象后并没有使用它的状态,只是用来调用静态函数。下面的方法就做到了单例懒加载,且只初始化执行一次MprpcApplication::Init(argc,argv);//演示调用远程发布rpc的方法Login,UserServiceRpc_Stub是专门用来协助rpc客户端的fixbug::UserServiceRpc_Stub stub(new MprpcChannel());//生成一个代理对象,以后通过stub来调用rpc方法//rpc方法的请求参数fixbug::LoginRequest request;//用户端发起调用,request肯定是这边给request.set_name("zhangsan");request.set_pwd("123456");//rpc方法的响应fixbug::LoginResponse response;//发起rpc方法的调用,下面是同步的rpc方法调用过程,即它的底层是MprpcChannel::callMethodstub.Login(nullptr,&request,&response,nullptr);//集中来做rpc方法调用的参数的序列化和网络发送了//走到这里表示一次rpc调用完成,读取调用的结果if(0 == response.result().errcode()){std::cout<<"rpc login response success:"<<response.success()<<std::endl;}else{std::cout<<"rpc login response error:"<<response.result().errmsg()<<std::endl;}//演示调用远程发布的rpc方法Registerfixbug::RegisterRequset req;req.set_id(2000);req.set_name("lisi");req.set_pwd("12314");fixbug::RegisterResponse rsp;//以同步的方式发起rpc调用请求,等待返回结果stub.Register(nullptr,&req,&rsp,nullptr);if(0 == rsp.result().errcode()){std::cout<<"rpc register response success:"<<rsp.success()<<std::endl;}else{std::cout<<"rpc register response error:"<<rsp.result().errmsg()<<std::endl;}return 0;
}
五、 google::protobuf::RpcController *controller
前面还有最后一个坑需要填上,google::protobuf::RpcController *controller是个什么东西呢?
我们先不着急回答这个问题,我们思考一下在 calleruserservice.cc 中的代码有没有什么问题?
我们是在使用stub对象发起调用Login方法后,就开始等待着读取返回的响应了,但是我们在CallMethod方法中也看到了,在许多地方失败时都会有return直接返回的情况,比如反序列化失败,套接字创建失败等,所以这里一旦失败,直接返回。我们还有必要进行后面的工作吗,即访问响应response,此时还没有response呢。所以我们需要在这里得到一些控制信息。
我们可以看到调用的这个Login方法的第一个参数就是google::protobuf::RpcController *controller
从他的名字就可以猜到它可以存储一些控制信息,让我们清楚地知道当前rpc调用处于什么状态。
下面是 RpcController类,可以看到它的成员函数都是虚函数,它是一个抽象类,无法实例化出对象,所以还需要我们创建一个类来继承它,并且重写它的成员函数。
也就是说这个 RpcController可以携带我们rpc调用过程中的一些信息,所以接下来我们创建类继承它并对方法进行重写。
头文件
//mprpccontroller.hclass MprpcController : public google::protobuf::RpcController
{public:MprpcController();void Reset();bool Failed() const;std::string ErrorText() const;void SetFailed(const std::string& reason);//对于这三个方法目前未实现具体的功能,所以暂时不用void StartCancel();bool IsCanceled() const;void NotifyOnCancel(google::protobuf::Closure* callback);private:bool m_failed;//RPC方法执行过程中的状态std::string m_errText;//RPC方法执行过程中的错误信息};
实现
//mprpccontroller.cc
MprpcController::MprpcController()
{m_failed=false;m_errText="";
}void MprpcController::Reset()//重置成刚开始的样子
{m_failed=false;m_errText="";
}
bool MprpcController::Failed() const//判断当前调用成功与否,返回状态,true就是发生问题了
{return m_failed;
}
std::string MprpcController::ErrorText() const//返回错误信息
{return m_errText;
}void MprpcController::SetFailed(const std::string&reason)//设置错误
{m_failed=true;m_errText=reason;
}//对于我们目前尚未实现的功能,我们实现成空函数就可以了
void MprpcController::StartCancel(){};
bool MprpcController::IsCanceled() const{};
void MprpcController::NotifyOnCancel(google::protobuf::Closure* callback){};
定义很简单,使用方法就是在 mprpcchannel.cc 中的使用,也比较简单。
之前我们在 calleruserservice.cc文件中使用stub调用Login方法时,第一个参数传的是空指针,现在传controller就行了
MprpcController controller;//实例化出一个控制对象
stub.Login(&controller,&request,&response,nullptr);
这样在 return之前就可以知道rpc调用过程中的状态了,后面我们就要这样子调用了
if (controller.Failed())
{// 表示出问题std::cout << controller.ErrorText() << std::endl;
}
else
{if (0 == response.result().errcode()){std::cout << "rpc login response success:" << response.success() << std::endl;}else{std::cout << "rpc login response error:" << response.result().errmsg() << std::endl;}
}
六、总结
到这里我们基本已经实现了整个框架,包括rpc发布端rpc方法的发布和rpc调用端的调用。后面我们还要给整个框架添加必不可少的一个模块——日志模块,还有zookeeper在本项目上的应用。见下一文!
感谢阅读!
相关文章:

C++实现分布式网络通信框架RPC(3)--rpc调用端
目录 一、前言 二、UserServiceRpc_Stub 三、 CallMethod方法的重写 头文件 实现 四、rpc调用端的调用 实现 五、 google::protobuf::RpcController *controller 头文件 实现 六、总结 一、前言 在前边的文章中,我们已经大致实现了rpc服务端的各项功能代…...
Ubuntu系统下交叉编译openssl
一、参考资料 OpenSSL&&libcurl库的交叉编译 - hesetone - 博客园 二、准备工作 1. 编译环境 宿主机:Ubuntu 20.04.6 LTSHost:ARM32位交叉编译器:arm-linux-gnueabihf-gcc-11.1.0 2. 设置交叉编译工具链 在交叉编译之前&#x…...
Cursor实现用excel数据填充word模版的方法
cursor主页:https://www.cursor.com/ 任务目标:把excel格式的数据里的单元格,按照某一个固定模版填充到word中 文章目录 注意事项逐步生成程序1. 确定格式2. 调试程序 注意事项 直接给一个excel文件和最终呈现的word文件的示例,…...
synchronized 学习
学习源: https://www.bilibili.com/video/BV1aJ411V763?spm_id_from333.788.videopod.episodes&vd_source32e1c41a9370911ab06d12fbc36c4ebc 1.应用场景 不超卖,也要考虑性能问题(场景) 2.常见面试问题: sync出…...
基于大模型的 UI 自动化系统
基于大模型的 UI 自动化系统 下面是一个完整的 Python 系统,利用大模型实现智能 UI 自动化,结合计算机视觉和自然语言处理技术,实现"看屏操作"的能力。 系统架构设计 #mermaid-svg-2gn2GRvh5WCP2ktF {font-family:"trebuchet ms",verdana,arial,sans-…...
设计模式和设计原则回顾
设计模式和设计原则回顾 23种设计模式是设计原则的完美体现,设计原则设计原则是设计模式的理论基石, 设计模式 在经典的设计模式分类中(如《设计模式:可复用面向对象软件的基础》一书中),总共有23种设计模式,分为三大类: 一、创建型模式(5种) 1. 单例模式(Sing…...

linux之kylin系统nginx的安装
一、nginx的作用 1.可做高性能的web服务器 直接处理静态资源(HTML/CSS/图片等),响应速度远超传统服务器类似apache支持高并发连接 2.反向代理服务器 隐藏后端服务器IP地址,提高安全性 3.负载均衡服务器 支持多种策略分发流量…...

C++_核心编程_多态案例二-制作饮品
#include <iostream> #include <string> using namespace std;/*制作饮品的大致流程为:煮水 - 冲泡 - 倒入杯中 - 加入辅料 利用多态技术实现本案例,提供抽象制作饮品基类,提供子类制作咖啡和茶叶*//*基类*/ class AbstractDr…...

深入剖析AI大模型:大模型时代的 Prompt 工程全解析
今天聊的内容,我认为是AI开发里面非常重要的内容。它在AI开发里无处不在,当你对 AI 助手说 "用李白的风格写一首关于人工智能的诗",或者让翻译模型 "将这段合同翻译成商务日语" 时,输入的这句话就是 Prompt。…...

使用VSCode开发Django指南
使用VSCode开发Django指南 一、概述 Django 是一个高级 Python 框架,专为快速、安全和可扩展的 Web 开发而设计。Django 包含对 URL 路由、页面模板和数据处理的丰富支持。 本文将创建一个简单的 Django 应用,其中包含三个使用通用基本模板的页面。在此…...
SkyWalking 10.2.0 SWCK 配置过程
SkyWalking 10.2.0 & SWCK 配置过程 skywalking oap-server & ui 使用Docker安装在K8S集群以外,K8S集群中的微服务使用initContainer按命名空间将skywalking-java-agent注入到业务容器中。 SWCK有整套的解决方案,全安装在K8S群集中。 具体可参…...

超短脉冲激光自聚焦效应
前言与目录 强激光引起自聚焦效应机理 超短脉冲激光在脆性材料内部加工时引起的自聚焦效应,这是一种非线性光学现象,主要涉及光学克尔效应和材料的非线性光学特性。 自聚焦效应可以产生局部的强光场,对材料产生非线性响应,可能…...
【杂谈】-递归进化:人工智能的自我改进与监管挑战
递归进化:人工智能的自我改进与监管挑战 文章目录 递归进化:人工智能的自我改进与监管挑战1、自我改进型人工智能的崛起2、人工智能如何挑战人类监管?3、确保人工智能受控的策略4、人类在人工智能发展中的角色5、平衡自主性与控制力6、总结与…...

Linux 文件类型,目录与路径,文件与目录管理
文件类型 后面的字符表示文件类型标志 普通文件:-(纯文本文件,二进制文件,数据格式文件) 如文本文件、图片、程序文件等。 目录文件:d(directory) 用来存放其他文件或子目录。 设备…...

Flask RESTful 示例
目录 1. 环境准备2. 安装依赖3. 修改main.py4. 运行应用5. API使用示例获取所有任务获取单个任务创建新任务更新任务删除任务 中文乱码问题: 下面创建一个简单的Flask RESTful API示例。首先,我们需要创建环境,安装必要的依赖,然后…...

C++初阶-list的底层
目录 1.std::list实现的所有代码 2.list的简单介绍 2.1实现list的类 2.2_list_iterator的实现 2.2.1_list_iterator实现的原因和好处 2.2.2_list_iterator实现 2.3_list_node的实现 2.3.1. 避免递归的模板依赖 2.3.2. 内存布局一致性 2.3.3. 类型安全的替代方案 2.3.…...

【kafka】Golang实现分布式Masscan任务调度系统
要求: 输出两个程序,一个命令行程序(命令行参数用flag)和一个服务端程序。 命令行程序支持通过命令行参数配置下发IP或IP段、端口、扫描带宽,然后将消息推送到kafka里面。 服务端程序: 从kafka消费者接收…...

iOS 26 携众系统重磅更新,但“苹果智能”仍与国行无缘
美国西海岸的夏天,再次被苹果点燃。一年一度的全球开发者大会 WWDC25 如期而至,这不仅是开发者的盛宴,更是全球数亿苹果用户翘首以盼的科技春晚。今年,苹果依旧为我们带来了全家桶式的系统更新,包括 iOS 26、iPadOS 26…...

TDengine 快速体验(Docker 镜像方式)
简介 TDengine 可以通过安装包、Docker 镜像 及云服务快速体验 TDengine 的功能,本节首先介绍如何通过 Docker 快速体验 TDengine,然后介绍如何在 Docker 环境下体验 TDengine 的写入和查询功能。如果你不熟悉 Docker,请使用 安装包的方式快…...
【Linux】shell脚本忽略错误继续执行
在 shell 脚本中,可以使用 set -e 命令来设置脚本在遇到错误时退出执行。如果你希望脚本忽略错误并继续执行,可以在脚本开头添加 set e 命令来取消该设置。 举例1 #!/bin/bash# 取消 set -e 的设置 set e# 执行命令,并忽略错误 rm somefile…...

stm32G473的flash模式是单bank还是双bank?
今天突然有人stm32G473的flash模式是单bank还是双bank?由于时间太久,我真忘记了。搜搜发现,还真有人和我一样。见下面的链接:https://shequ.stmicroelectronics.cn/forum.php?modviewthread&tid644563 根据STM32G4系列参考手…...

springboot 百货中心供应链管理系统小程序
一、前言 随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱,百货中心供应链管理系统被用户普遍使用,为方…...

调用支付宝接口响应40004 SYSTEM_ERROR问题排查
在对接支付宝API的时候,遇到了一些问题,记录一下排查过程。 Body:{"datadigital_fincloud_generalsaas_face_certify_initialize_response":{"msg":"Business Failed","code":"40004","sub_msg…...
Linux链表操作全解析
Linux C语言链表深度解析与实战技巧 一、链表基础概念与内核链表优势1.1 为什么使用链表?1.2 Linux 内核链表与用户态链表的区别 二、内核链表结构与宏解析常用宏/函数 三、内核链表的优点四、用户态链表示例五、双向循环链表在内核中的实现优势5.1 插入效率5.2 安全…...

智慧医疗能源事业线深度画像分析(上)
引言 医疗行业作为现代社会的关键基础设施,其能源消耗与环境影响正日益受到关注。随着全球"双碳"目标的推进和可持续发展理念的深入,智慧医疗能源事业线应运而生,致力于通过创新技术与管理方案,重构医疗领域的能源使用模式。这一事业线融合了能源管理、可持续发…...

Lombok 的 @Data 注解失效,未生成 getter/setter 方法引发的HTTP 406 错误
HTTP 状态码 406 (Not Acceptable) 和 500 (Internal Server Error) 是两类完全不同的错误,它们的含义、原因和解决方法都有显著区别。以下是详细对比: 1. HTTP 406 (Not Acceptable) 含义: 客户端请求的内容类型与服务器支持的内容类型不匹…...
应用升级/灾备测试时使用guarantee 闪回点迅速回退
1.场景 应用要升级,当升级失败时,数据库回退到升级前. 要测试系统,测试完成后,数据库要回退到测试前。 相对于RMAN恢复需要很长时间, 数据库闪回只需要几分钟。 2.技术实现 数据库设置 2个db_recovery参数 创建guarantee闪回点,不需要开启数据库闪回。…...
论文解读:交大港大上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化学习框架(二)
HoST框架核心实现方法详解 - 论文深度解读(第二部分) 《Learning Humanoid Standing-up Control across Diverse Postures》 系列文章: 论文深度解读 + 算法与代码分析(二) 作者机构: 上海AI Lab, 上海交通大学, 香港大学, 浙江大学, 香港中文大学 论文主题: 人形机器人…...

微信小程序之bind和catch
这两个呢,都是绑定事件用的,具体使用有些小区别。 官方文档: 事件冒泡处理不同 bind:绑定的事件会向上冒泡,即触发当前组件的事件后,还会继续触发父组件的相同事件。例如,有一个子视图绑定了b…...
ES6从入门到精通:前言
ES6简介 ES6(ECMAScript 2015)是JavaScript语言的重大更新,引入了许多新特性,包括语法糖、新数据类型、模块化支持等,显著提升了开发效率和代码可维护性。 核心知识点概览 变量声明 let 和 const 取代 var…...