#pub-sub #events #event-listener #global-data #simple-wasm

nuts

Nuts 是一个提供简单发布-订阅 API 的库,特点是无耦合地创建发布者和订阅者。

4 个版本

0.2.1 2021 年 2 月 28 日
0.2.0 2020 年 12 月 6 日
0.1.1 2020 年 10 月 3 日
0.1.0 2020 年 10 月 3 日

#15#event-listener


用于 paddle

MIT/Apache

130KB
2.5K SLoC

Nuts

Nuts 是一个提供简单发布-订阅 API 的库,特点是无耦合地创建发布者和订阅者。

快速入门示例

struct Activity;
let activity = nuts::new_activity(Activity);
activity.subscribe(
    |_activity, n: &usize|
    println!("Subscriber received {}", n)
);
nuts::publish(17usize);
// "Subscriber received 17" is printed
nuts::publish(289usize);
// "Subscriber received 289" is printed

如上例所示,发布者和订阅者之间不需要显式通道。对 publish 的调用是一个静态方法,不需要用户的状态信息。由于它们都使用 usize 作为消息类型,它们之间的连接是隐式的。

Nuts 通过管理线程局部存储中所有必要的状态来实现这个简单的 API。这特别适用于网页应用。然而,Nuts 也可以在其他平台上使用。实际上,Nuts 除了 std 之外没有其他依赖。

库的状态

随着 Nuts 版本 0.2 在 crates.io 的发布,它已经达到了一个重要的里程碑。单线程功能都已实现。可能这里或那里需要添加一些方法。但在此处,我并不期望在现有接口中再次进行重大 API 重构。

然而,还有一个重要的特性尚未实现。这是并行分发,在 #2 中讨论。理想情况下,这将是在底层实现的。但很可能需要向 API 中添加更多方法。

如果并行分发得到实现,Nuts 可能会推出稳定的 1.0 版本。

活动

活动是 Nuts 的核心。从全局管理的数据来看,它们代表活动的部分,即它们可以具有事件监听器。被动的对应部分由 DomainState 定义。

任何具有静态生命周期(即仅在运行时确定的生命周期参数的类型)的结构都可以用作活动。您不需要自己实现 Activity 特性,如果可能的话,它将始终自动派生。

要创建活动,只需使用 nuts::new_activity 或其变体之一注册应用作活动的对象。

重要的是要理解,活动唯一由其类型定义。您不能从同一类型创建两个活动。(但您可以从它周围创建一个包装类型。)这允许活动通过其类型进行引用,该类型必须在运行时知道。

发布

任何结构体或原始数据类型都可以发布,只要在编译时已知其类型。(与活动相同的限制。)在调用 nuts::publish 之后,所有相同类型的活跃订阅都会执行,并且发布对象将与它们全部共享。

示例

struct ChangeUser { user_name: String }
pub fn main() {
    let msg = ChangeUser { user_name: "Donald Duck".to_owned() };
    nuts::publish(msg);
    // Subscribers to messages of type `ChangeUser` will be notified
}

订阅

活动可以根据消息的Rust类型标识符订阅消息。闭包或函数指针可以用来为特定类型的消息创建订阅。

Nuts使用core::any::TypeId来比较类型。当发布消息的类型与订阅的消息类型匹配时,会调用订阅。

创建新订阅有几种不同的方法。其中最简单的是简单地调用 subscribe(...),它可以这样使用

struct MyActivity { id: usize };
struct MyMessage { text: String };

pub fn main() {
    let activity = nuts::new_activity(MyActivity { id: 0 } );
    activity.subscribe(
        |activity: &mut MyActivity, message: &MyMessage|
        println!("Subscriber with ID {} received text: {}", activity.id, message.text)
    );
}

在上面的示例中,创建了一个订阅,等待发布类型为 MyMessage 的消息。到目前为止,闭包中的代码还没有执行,控制台也没有打印任何内容。

请注意,闭包的第一个参数是对活动对象的可变引用。第二个参数是对发布消息的只读引用。两种类型必须完全匹配,否则编译器将不接受闭包。

也可以使用具有正确参数类型的函数来订阅。

struct MyActivity { id: usize };
struct MyMessage { text: String };

pub fn main() {
    let activity = nuts::new_activity(MyActivity { id: 0 } );
    activity.subscribe(MyActivity::print_text);
}

impl MyActivity {
    fn print_text(&mut self, message: &MyMessage) {
        println!("Subscriber with ID {} received text: {}", self.id, message.text)
    }
}

示例:带有发布 + 订阅的基本活动

#[derive(Default)]
struct MyActivity {
    round: usize
}
struct MyMessage {
    no: usize
}

// Create activity
let activity = MyActivity::default();
// Activity moves into globally managed state, ID to handle it is returned
let activity_id = nuts::new_activity(activity);

// Add event listener that listens to published `MyMessage` types
activity_id.subscribe(
    |my_activity, msg: &MyMessage| {
        println!("Round: {}, Message No: {}", my_activity.round, msg.no);
        my_activity.round += 1;
    }
);

// prints "Round: 0, Message No: 1"
nuts::publish( MyMessage { no: 1 } );
// prints "Round: 1, Message No: 2"
nuts::publish( MyMessage { no: 2 } );

示例:私有通道

到目前为止,我向您展示的示例中,所有消息都是共享引用,并且发送到已注册到特定消息类型的所有监听器。另一种选择是使用私有通道。在这种情况下,发送者可以决定哪个监听活动将接收到消息。在这种情况下,消息的所有权被赋予监听者。

struct ExampleActivity {}
let id = nuts::new_activity(ExampleActivity {});
// `private_channel` works similar to `subscribe` but it owns the message.
id.private_channel(|_activity, msg: usize| {
    assert_eq!(msg, 7);
});
// `send_to` must be used instead of `publish` when using private channels.
// Which activity receives the message is decide by the first type parameter.
nuts::send_to::<ExampleActivity, _>(7usize);

活动生命周期

每个活动都有一个生命周期状态,可以使用 set_status 来改变。它从 LifecycleStatus::Active 开始。在Nuts的当前版本中,唯一的另一个状态是 LifecycleStatus::Inactive

可以使用不活动状态暂时将活动置于休眠状态。在不活动状态时,活动将不会收到它已订阅的事件。可以使用订阅过滤器来更改此行为。(见 subscribe_masked

如果状态从活动变为不活动,活动将调用其 on_leaveon_leave_domained 订阅。

如果状态从不活动变为活动,活动将调用其 on_enteron_enter_domained 订阅。

域存储任意数据,以便在多个 活动 之间共享。库用户可以定义域的数量,但每个活动只能加入一个域。

域只应在数据需要在同一类型或不同类型的多项活动之间共享时使用。如果数据只被单个活动使用,通常更好的做法是将它存储在活动结构体本身中。

如果只使用一个域,也可以考虑使用 DefaultDomain 而不是创建自己的枚举。

目前,除了数据隔离之外,使用多个域并没有真正的优势。但未来有计划将活动根据其域在不同的线程中进行调度。

创建域

Nuts 在后台隐式创建域。用户只需提供一个实现 DomainEnumeration 特性的枚举或结构体。这个特性只需要 fn id(&self) -> usize 函数,该函数将每个对象映射到表示域的数字。

通常,域由枚举定义,并通过使用 domain_enum!DomainEnumeration 特性进行派生。

#[macro_use] extern crate nuts;
use nuts::{domain_enum, DomainEnumeration};
#[derive(Clone, Copy)]
enum MyDomain {
    DomainA,
    DomainB,
}
domain_enum!(MyDomain);

使用域

函数 nuts::store_to_domain 允许在域中初始化数据。之后,数据将在活动的订阅函数中可用。

域内只能存储一个类型 id 的实例。如果域中已经存在相同类型的旧值,它将被覆盖。

如果活动与域相关联,它们必须使用 nuts::new_domained_activity 进行注册。这将允许使用可以访问域状态的闭包进行订阅。使用 subscribe_domained 添加这些订阅。还可以使用 subscribe 为不访问域的订阅进行订阅。

具有域的活动示例

use nuts::{domain_enum, DomainEnumeration};

#[derive(Default)]
struct MyActivity;
struct MyMessage;

#[derive(Clone, Copy)]
enum MyDomain {
    DomainA,
    DomainB,
}
domain_enum!(MyDomain);

// Add data to domain
nuts::store_to_domain(&MyDomain::DomainA, 42usize);

// Register activity
let activity_id = nuts::new_domained_activity(MyActivity, &MyDomain::DomainA);

// Add event listener that listens to published `MyMessage` types and has also access to the domain data
activity_id.subscribe_domained(
    |_my_activity, domain, msg: &MyMessage| {
        // borrow data from the domain
        let data = domain.try_get::<usize>();
        assert_eq!(*data.unwrap(), 42);
    }
);

// make sure the subscription closure is called
nuts::publish( MyMessage );

高级:理解执行顺序

调用 nuts::publish(...) 时,消息可能不会立即发布。在执行上一个 publish 的订阅处理程序时,所有新消息都会排队,直到上一个消息完成。

struct MyActivity;
let activity = nuts::new_activity(MyActivity);
activity.subscribe(
    |_, msg: &usize| {
        println!("Start of {}", msg);
        if *msg < 3 {
            nuts::publish( msg + 1 );
        }
        println!("End of {}", msg);
    }
);

nuts::publish(0usize);
// Output:
// Start of 0
// End of 0
// Start of 1
// End of 1
// Start of 2
// End of 2
// Start of 3
// End of 3

完整演示示例

使用坚果构建基本点击游戏的简单示例可在examples/clicker-game中找到。它需要已安装wasm-packnpm。要运行示例,执行wasm-pack build,然后cd www; npm install; npm run start。这个示例仅展示了nuts的最低功能。

目前,没有更多的示例(一些示例因为过时的依赖而不得不删除)。希望这会在某个时刻改变。

所有示例都设置为独立项目。(以避免污染库的依赖。)因此,标准的cargo run --example将无法工作。必须进入示例的目录并从那里构建。

依赖项

~0–2MB
~39K SLoC