2 个不稳定版本

0.2.0 2023 年 7 月 27 日
0.1.0 2022 年 10 月 4 日

#7#透镜

MPL-2.0 许可证

91KB
1.5K SLoC

X-Bow: 精确状态管理

X-Bow 是一个面向 UI 编程的状态管理库。它让你...

  • 将数据保存在集中存储中。
  • 构建指向存储中部分内容的“路径”。
  • 在那些路径上借用和修改数据。
  • 通过异步 API 订阅那些路径上的修改。

快速示例

// Derive `Trackable` to allow parts of the struct to be tracked.
#[derive(Default, Trackable)]
#[track(deep)] // `deep` option is useful if the fields themselves are structs
struct MyStruct {
    field_1: i32,
    field_2: u64,
    child_struct: AnotherStruct
}

// Create a centralized store with the data.
let store = Store::new(MyStruct::default());

// Build a path to the `i32` at `field_1` in the `MyStruct`.
let path = store.build_path().field_1();

// This implements the `Stream` trait. You can do `stream.next().await`, etc.
let stream = path.until_change();

// Mutably borrow the `i32` of the path, and increment it.
// This will cause the `stream` we created to fire.
*path.borrow_mut() += 1;

概念

存储

存储是应用程序状态所在的地方。它是一个大的 RefCell。

路径

路径标识存储中的数据片段。它实现了 [PathExt],其中包含你将交互的大部分方法。

路径通常被包装在 PathBuilder 对象中。这些对象每个都指向它们包装的路径对象。

路径构建器

路径构建器包装路径对象并解引用到路径对象。

它还提供了“继续”路径的方法;如果路径指向 T,路径构建器将允许你将其转换为指向 T 内部某些部分的路径。

例如:包装指向 Vec<T> 的路径的路径构建器有一个 index(idx: usize) 方法,它返回一个指向 T 的路径。

要将路径构建器转换为路径,请使用 IntoPath::into_path。要将路径转换为路径构建器,请使用 PathExt::build_path

可追踪类型

实现 [Trackable] 的类型具有相应的 PathBuilder 类型。为了成为 Trackable,类型应该在它们上使用 #[derive(Trackable)]

用法

步骤

  1. 通过在它们上添加 #[derive(Trackable)][track(deep)] 来使您的结构和枚举可追踪。请参阅 [Trackable 宏][derive@Trackable] 的文档。
    // 👇 Derive `Trackable` to allow parts of the struct to be tracked.
    #[derive(Trackable)]
    #[track(deep)]
    struct MyStruct {
        field_1: i32,
        field_2: u64,
        child_enum: MyEnum
    }
    // 👇 Derive `Trackable` to allow parts of the enum to be tracked.
    #[derive(Trackable)]
    #[track(deep)]
    enum MyEnum {
        Variant1(i32),
        Variant2 {
            data: String
        }
    }
    
  2. 将您的数据放入一个 [Store] 中。
    #
    let my_data = MyStruct {
        field_1: 42,
        field_2: 123,
        child_enum: MyEnum::Variant2 { data: "Hello".to_string() }
    };
    let store = Store::new(my_data);
    
  3. 创建 [Path]。
    #
    let my_data = MyStruct {
        field_1: 42,
        field_2: 123,
        child_enum: MyEnum::Variant2 { data: "Hello".to_string() }
    };
    let path_to_field_1 = store.build_path().field_1();
    let path_to_data = store.build_path().child_enum().Variant2_data();
    
  4. 使用您创建的 Paths。请参阅 [PathExt] 和 [PathExtGuaranteed] 以获取可用的 API。

通过 Vec 和 HashMap 进行追踪

您可以使用 index(_) 方法通过 [Vec] 进行追踪。

let store = Store::new(vec![1, 2, 3]);
let path = store.build_path().index(1); // 👈 path to the second element in the vec

您可以使用 key(_) 方法通过 HashMap 进行追踪。

let store = Store::new(HashMap::<u32, String>::new());
let path = store.build_path().key(555); // 👈 path to the String at key 555 in the hashmap.

设计

借用和 Paths

这个库的设计是简单 RefCell 和在函数式编程世界中流行的 "lens" 概念的混合。

X-Bow 的中央数据存储是一个 RefCell。库提供了类似 RefCell 的 API,如 borrowborrow_mut。支持变更,并且不需要不可变数据结构。

库使用 Path 来识别存储中数据的一部分。Paths 由组合段创建。每个段就像一个 "lens",知道如何将类型的一些子结构投影进来。组合在一起,这些段成为一个路径,知道如何从存储的根数据投影到它所标识的部分。

Paths 和 Lens/Optics 之间的区别只是我们的路径是可变的,而 Lens/Optics 经常与不可变/函数式设计相关联。

通知和订阅

库设计的另一个重要方面是通过 until_changeuntil_bubbling_change 提供的通知/订阅功能。

基于 Paths 的哈希值进行变更监听。我们有一个将每个哈希与版本号和监听 Wakers 的列表相关联的映射。当某个包含目标数据的数据块发生变化时,我们假设目标数据也发生了变化,因此 until_change 方法在其目标路径的哈希值以及所有前缀路径上注册 wakers。

哈希冲突

如果两个路径最终得到相同的哈希值,唤醒通知将同时唤醒其他路径的监听者。因此,until_change 流可能意外触发。

请记住,u64 哈希冲突的概率非常低;在存储中有 10,000 个不同的路径时,冲突概率可以计算为小于 1E-11(0.000000001%)。

为了进一步最小化哈希冲突的影响,X-Bow同时保存路径的长度及其哈希值。这增加了冲突概率,但确保不同长度的路径永远不会冲突;修改状态树深处的一些数据永远不会导致整个树被唤醒。

依赖项

~0.4–0.9MB
~20K SLoC