Rust仿node事件总线的简单实现

 作者:LengineerC
 发布时间:2025-11-25 16:25:17
 文章字数:12420

用过node的人都知道,事件驱动是node的核心运行机制。在异步编程中我们可以给一个事件绑定一个回调,当任务执行完成后emit一个事件触发回调。node中,官方提供了EventEmitter的给用户做自定义事件处理:

js
const EventEmitter = require("events");
 
const emitter = new EventEmitter();
emitter.on('xxx', callback);
emitter.emit('xxx');

而在Rust中,官方库并没有提供这样一个功能。作为一个习惯了写js的人来说,事件驱动编程是很爽的,一些场景下不用刻意去处理代码的执行逻辑顺序。所以就仿造node自己实现一个简单的rust事件总线来满足基本需求,同时使用tokio来实现异步处理。

需求分析

rust和js不同,js是单线程的,而rust有多线程,所以在写的时候需要考虑数据竞态、死锁等线程安全问题。

考虑到js的弱类型和动态特性,回调函数的定义可以很轻松,而rust是静态强类型,而且有宇宙第一的编译器,光是在类型定义这一步就很难,因此只能给变量做一个类型擦除。(不用泛型是因为泛型会限制参数和函数的类型范围,虽然写起来会更简单,但是在实际场景中只能定义多种emitter来适配不同的使用需求,变得极度不灵活)

实现

单线程

先从单线程的回调参数开始设计,因为涉及任意类型参数,所以可以直接用最简单的类型擦除实现单个参数的类型

rust
type Arg = Box<dyn Any>;

实现类似js函数参数中的...args只需要使用Vec就行,没用数组切片是为了避免考虑生命周期的问题,同时为了便于调用,可以使用Rc来实现多所有权

rust
type Args=Rc<Vec<Arg>>;

js中可以一个事件对应多个回调并且还有对回调的增删操作,所以后续可以用一个HashMap来存储一个事件和其对应的回调函数。为了适配这个功能,需要封装一个对象用来存储单个回调函数本体和其标识符,暂且就命名为Handler,不过在此之前还要先考虑回调函数本身的类型怎么定义。因为在使用过程中回调函数为一个闭包,而不同的回调可能形式不同,所以可以使用

rust
type SyncCallback = Rc<dyn Fn(Args)>; // 同步回调
type AsyncCallback = Rc<dyn Fn(Args) -> Pin<Box<dyn Future<Output = ()>>>>; // 异步回调

来兼容不同类型的闭包。使用枚举包裹回调函数则可以在一种Emitter中处理两种回调

rust
enum Callback {
Sync(SyncCallback),
Async(AsyncCallback),
}

然后我们就可以编写Handler对象了

rust
type HandlerId = u64;
 
struct Handler {
pub id: HandlerId,
pub callback: Callback,
pub once: bool, // 是否单次触发
}

之后就可以开始考虑Emitter本体了。(最开始考虑使用泛型在一种Emitter中处理单/多线程,后面发现实现过于困难,于是拆解为两种实现,有大佬知道解决方案的请给我留言学习一下

首先肯定要有个变量来存储事件监听器的信息,还是老套路,使用Rc, RefCell实现单线程的内部可变多所有权,Handler中还有id,因此需要一个计数器实现自动递增

rust
struct SingleThreadEventEmitter {
listeners: Rc<RefCell<HashMap<String, Vec<Handler>>>>,
id_counter: Rc<RefCell<HandlerId>>,
}
 
impl SingleThreadEventEmitter {
pub fn new() -> Self {
Self {
listeners: Rc::new(RefCell::new(HashMap::new())),
id_counter: Rc::new(RefCell::new(1)),
}
}
 
fn get_id(&self) -> HandlerId {
let mut id = self.id_counter.borrow_mut();
let old_value = *id;
*id += 1;
old_value
}
}

接下来先考虑处理同步回调涉及的方法(因为最开始想把单线程多线程写在一起,所以直接写成了trait,后面推翻了这个想法导致trait稍微有点多此一举),本项目只是个简单实现,所以只实现回调的增删和触发即可

rust
trait EventEmitter {
// 添加事件及其回调(回调函数可以多次添加)
fn on<F>(&self, event: &str, callback: F) -> HandlerId
where
F: Fn(Args) + 'static;
 
// 同node的once
fn once<F>(&self, event: &str, callback: F) -> HandlerId
where
F: Fn(Args) + 'static;
 
// 删除单个回调
fn off(&self, event: &str, id: HandlerId) -> bool;
 
// 删除整个事件
fn off_all(&self, event: &str);
 
// 触发事件
fn emit(&self, event: &str, args: Rc<Vec<Arg>>);
}

这里添加'static的原因是这个闭包存进Handlercallback里面,而回调在没有主动删除的情况下应该一直存在,所以标记为静态生命周期告诉编译器该闭包全程有效。

先看最简单的on的实现

rust
fn on<F>(&self, event: &str, callback: F) -> HandlerId
where
F: Fn(Args) + 'static,
{
let id = self.get_id();
let handler = Handler {
id,
callback: Callback::Sync(Rc::new(callback)),
once: false,
};
 
self.listeners
.borrow_mut()
.entry(event.to_string())
.or_default()
.push(handler);
 
id
}

代码还是比较简单的这里就不在赘述,once的实现和on的唯一区别就是构造Handler的时候把once字段变为true

接下来看看off的实现

rust
fn off(&self, event: &str, id: HandlerId) -> bool {
let mut listeners = self.listeners.borrow_mut();
 
if let Some(handlers) = listeners.get_mut(event) {
let len = handlers.len();
handlers.retain(|h| h.id != id);
 
return len != handlers.len();
}
 
false
}

就是一个简单的根据HandlerId删除对应Handler

off_all就是直接从HashMap中删除整个事件

rust
fn off_all(&self, event: &str) {
self.listeners.borrow_mut().remove(event);
}

这里先暂时不讲emit实现,前面提到过单个Emitter中同时处理了同步和异步闭包,还要为异步回调创建单独的on/once方法

rust
trait AsyncEventEmitter {
fn on_async<F>(&self, event: &str, callback: F) -> HandlerId
where
F: Fn(Args) -> Pin<Box<dyn Future<Output = ()>>> + 'static;
 
fn once_async<F>(&self, event: &str, callback: F) -> HandlerId
where
F: Fn(Args) -> Pin<Box<dyn Future<Output = ()>>> + 'static;
}
 
impl AsyncEventEmitter for SingleThreadEventEmitter {
fn on_async<F>(&self, event: &str, callback: F) -> HandlerId
where
F: Fn(Args) -> Pin<Box<dyn Future<Output = ()>>> + 'static,
{
let id = self.get_id();
let handler = Handler {
id,
callback: Callback::Async(Rc::new(callback)),
once: false,
};
 
self.listeners
.borrow_mut()
.entry(event.to_string())
.or_default()
.push(handler);
 
id
}
 
fn once_async<F>(&self, event: &str, callback: F) -> HandlerId
where
F: Fn(Args) -> Pin<Box<dyn Future<Output = ()>>> + 'static,
{
let id = self.get_id();
let handler = Handler {
id,
callback: Callback::Async(Rc::new(callback)),
once: true,
};
 
self.listeners
.borrow_mut()
.entry(event.to_string())
.or_default()
.push(handler);
 
id
}
}

异步版on/once和同步版的唯一区别就是异步创建Callback时使用的是Async,同步为Sync

现在来处理emit了,逻辑也不算太复杂,这里先贴上具体实现

rust
fn emit(&self, event: &str, args: Rc<Vec<Arg>>) {
let callbacks: Vec<Callback> = self
.listeners
.borrow()
.get(event)
.map(|handlers| handlers.iter().map(|h| h.callback.clone()).collect())
.unwrap_or_default();
 
for callback in callbacks {
match callback {
Callback::Sync(cb) => {
cb(args.clone());
}
Callback::Async(cb) => {
let args_clone = args.clone();
tokio::task::spawn_local(async move {
cb(args_clone).await;
});
}
}
}
 
// 清除单次执行的Handler
let mut listeners = self.listeners.borrow_mut();
let handlers_opt = listeners.get_mut(event);
if let Some(handlers) = handlers_opt {
handlers.retain(|h| !h.once);
}
}

这里面有一个需要注意的点就是在之前的参数定义中,没有实现Send,并且也是使用的Rc非线程安全型,而在tokiospawn是多线程调度,我们的参数不能在线程间安全传递,所以使用的是spawn_local,缺点就是调用的时候会有一点复杂。

使用

同步

如果不涉及异步,那调用相当简单

rust
fn test() {
let emitter = SingleThreadEventEmitter::new();
 
let id1 = emitter.on("test", |_| { ... });
let id2 = emitter.once("test", move |args| {
if let Some(arg) = args[0].downcast_ref::<i32>() {
// 参数处理逻辑
}
});
let id3 = emitter.on("test", |_| { ... });
 
emitter.emit("test", Rc::new(vec![Box::new(42) as Arg]));
emitter.emit("test", Rc::new(vec![]));
}

直接结果和我们要求的一样,参数能正确获取,并且事件第二次触发的时候id2对应的Handler已经被删除不会触发。唯一的缺点还是参数的使用有点麻烦,这里给出一些解决方式:

  1. emit时参数必须要类型断言且套RcVec

    我们可以写一个简单的声明宏来自动生成参数代码模板

    rust
    #[macro_export]
    macro_rules! args {
    ($($v:expr),* $(,)?) => {
    std::rc::Rc::new(vec![$(Box::new($v) as Box<dyn std::any::Any>),*])
    };
    }
  2. 闭包中使用参数要经过复杂的类型向下转换(downcast)

    可以自己封装一个函数来处理

异步

由于Emitter中异步设计和spawn_local的局限性,在单线程中使用异步会稍微有点复杂(在多线程版本中会好很多),下面给出一个测试时候使用的用例

rust
#[tokio::main(flavor = "current_thread")]
async fn main() {
let local = LocalSet::new();
 
local
.run_until(async {
let emitter = SingleThreadEventEmitter::new();
 
emitter.on("e1", |_| {
println!("Emitted e1");
});
 
emitter.once("e1", |args| {
let n: i32 = *args[0].downcast_ref().unwrap();
println!("{}", n);
println!("Emitted e1 2");
});
 
emitter.on_async("e1", |args| {
// 必须这样写
Box::pin(async move {
let n = *args[0].downcast_ref::<i32>().unwrap();
let res = async_test(n).await;
println!("{}", res);
})
});
 
println!("Emitting e1 first time");
emitter.emit("e1", args![1]);
 
println!("Emitting e1 second time");
emitter.emit("e1", args![1]);
 
tokio::task::yield_now().await;
})
.await;
}

因为我们的单线程回调是!Send任务,使用flavor = "current_thread"保证我们的任务在单线程上面运行,同时要结合LocalSet进行本地任务调度。

关于必须写

rust
Box::pin(async move { ... })

本人技术不精,也没找到什么好的办法(向大佬求助)。

多线程

有了单线程的代码结构,我们很容易在此基础上改造出一个多线程版本出来。

因为闭包参数会在多个线程中转递,所以要实现基本的SendSync

rust
type ThreadSafeArg = Box<dyn Any + Send + Sync>;

其余对应的参数组,回调类型之类的只需要把Rc换为Arc,同时添加SendSync即可

rust
type ThreadSafeArgs=Arc<Vec<ThreadSafeArg>>;
type SyncThreadSafeCallback = Arc<dyn Fn(ThreadSafeArgs) + Send + Sync>;
type AsyncThreadSafeCallback =
Arc<dyn Fn(ThreadSafeArgs) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;

AsyncThreadSafeCallback中由于Future不可能被多个函数同时轮询(在某一个线程中独占执行)所以只用实现Send

Emitter本体设计上和单线程版本有些区别,需要手动传入运行时,具体原因后面再说

rust
struct MultiThreadEventEmitter {
listeners: Arc<Mutex<HashMap<String, Vec<ThreadSafeHandler>>>>,
id_counter: Arc<AtomicU64>,
handle: Option<tokio::runtime::Handle>,
}
 
impl MultiThreadEventEmitter {
pub fn new() -> Self {
Self {
listeners: Arc::new(Mutex::new(HashMap::new())),
id_counter: Arc::new(AtomicU64::new(0)),
handle: None,
}
}
 
pub fn set_handle(mut self, handle: tokio::runtime::Handle) -> Self {
self.handle = Some(handle);
self
}
 
fn get_id(&self) -> HandlerId {
self.id_counter.fetch_add(1, Ordering::SeqCst)
}
}

对应的接口方法定义

rust
trait ThreadSafeEventEmitter: Send + Sync {
fn on<F>(&self, event: &str, callback: F) -> HandlerId
where
F: Fn(ThreadSafeArgs) + Send + Sync + 'static;
 
fn once<F>(&self, event: &str, callback: F) -> HandlerId
where
F: Fn(ThreadSafeArgs) + Send + Sync + 'static;
 
fn off(&self, event: &str, id: HandlerId) -> bool;
 
fn off_all(&self, event: &str);
 
fn emit(&self, event: &str, args: Arc<Vec<ThreadSafeArg>>);
}
 
trait ThreadSafeAsyncEventEmitter {
fn on_async<F>(&self, event: &str, callback: F) -> HandlerId
where
F: Fn(ThreadSafeArgs) -> Pin<Box<dyn Future<Output = ()> + Send>>
+ Send
+ Sync
+ 'static;
 
fn once_async<F>(&self, event: &str, callback: F) -> HandlerId
where
F: Fn(ThreadSafeArgs) -> Pin<Box<dyn Future<Output = ()> + Send>>
+ Send
+ Sync
+ 'static;
}

类型定义和大部分实现上和单线程版本没有太大差别,只是多实现了SendSync,然后把Rc换为ArcRefCell换为Mutex,然后多了一些并发锁的处理。具体实现:multi_thread.rs

回到需要传入运行时的问题,在多线程的emit中,一样采用了同步异步的混合处理,但是可以发现并没有采用tokio::spawn的方式执行异步任务,因为tokio::spawn只能在当前上下文中执行,emit函数本身不是async的,而且整个MultiThreadEventEmitter也不是async块本身。所以为了保留上下文,最好的办法就是调用时直接传递上下文。

使用

同步

和单线程没有任何区别,也要参数类型断言和向下转换,也可以写一个宏来处理参数类型断言的问题

rust
#[macro_export]
macro_rules! ts_args {
($($v:expr),* $(,)?) => {
std::sync::Arc::new(vec![$(Box::new($v) as Box<dyn std::any::Any + Send + Sync>),*])
};
}
异步

同样展示一个测试是的用例

rust
#[tokio::main]
fn main() {
let rt = tokio::runtime::Runtime::new().unwrap();
let emitter = MultiThreadEventEmitter::new()
.set_handle(rt.handle().clone());
emitter.on("e1", |_| {
println!("e1 1");
});
 
emitter.on("e1", |args| {
let n: i32 = *args[0].downcast_ref().unwrap();
println!("{}", n);
});
 
emitter.on_async("e1", |args| {
let n: i32 = *args[1].downcast_ref().unwrap();
Box::pin(async move {
let res = async_test(n).await;
println!("async: {}", res);
})
});
 
emitter.once_async("e1", |args| {
let n: i32 = *args[0].downcast_ref().unwrap();
Box::pin(async move {
let res = async_test(n).await;
println!("async once: {}", res);
})
});
 
emitter.emit("e1", ts_args![1, 2]);
 
let h = thread::spawn(move || {
emitter.emit("e1", ts_args![1, 2]);
});
 
h.join().unwrap();
}

除了没有单线程的flavor = "current_thread"LocalSet,调用方法和单线程并无区别。

总结

这个事件总线的实现了最基本的订阅发布功能,异步处理上还是存在一些未解决的问题。本文只是作为一种实现思路,在细节部分上没有做到完美(当然和实力有关)。作为一个初学者欢迎大家批评指正,有好的思路也欢迎大家指导。


文章链接:
版权声明:本博客所有文章除特別声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 LengineerC
 目录
1需求分析
2实现
2.1单线程
2.1.1使用
2.1.1.1同步
2.1.1.2异步
2.2多线程
2.2.1使用
2.2.1.1同步
2.2.1.2异步
3总结