2 个不稳定版本
0.2.0 | 2023 年 7 月 27 日 |
---|---|
0.1.0 | 2022 年 10 月 4 日 |
#7 在 #透镜
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)]
。
用法
步骤
- 通过在它们上添加
#[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 } }
- 将您的数据放入一个 [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);
- 创建 [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();
- 使用您创建的 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,如 borrow 和 borrow_mut。支持变更,并且不需要不可变数据结构。
库使用 Path 来识别存储中数据的一部分。Paths 由组合段创建。每个段就像一个 "lens",知道如何将类型的一些子结构投影进来。组合在一起,这些段成为一个路径,知道如何从存储的根数据投影到它所标识的部分。
Paths 和 Lens/Optics 之间的区别只是我们的路径是可变的,而 Lens/Optics 经常与不可变/函数式设计相关联。
通知和订阅
库设计的另一个重要方面是通过 until_change 和 until_bubbling_change 提供的通知/订阅功能。
基于 Paths 的哈希值进行变更监听。我们有一个将每个哈希与版本号和监听 Wakers 的列表相关联的映射。当某个包含目标数据的数据块发生变化时,我们假设目标数据也发生了变化,因此 until_change 方法在其目标路径的哈希值以及所有前缀路径上注册 wakers。
哈希冲突
如果两个路径最终得到相同的哈希值,唤醒通知将同时唤醒其他路径的监听者。因此,until_change
流可能意外触发。
请记住,u64
哈希冲突的概率非常低;在存储中有 10,000 个不同的路径时,冲突概率可以计算为小于 1E-11(0.000000001%)。
为了进一步最小化哈希冲突的影响,X-Bow同时保存路径的长度及其哈希值。这增加了冲突概率,但确保不同长度的路径永远不会冲突;修改状态树深处的一些数据永远不会导致整个树被唤醒。
依赖项
~0.4–0.9MB
~20K SLoC