ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Dioxus 派生状态完全指南:用 use_memo 高效缓存并传递由信号计算出的数据

Dioxus 派生状态完全指南:用 use_memo 高效缓存并传递由信号计算出的数据 Dioxus 派生状态完全指南用 use_memo 高效缓存并传递由信号计算出的数据【免费下载链接】dioxusFullstack app framework for web, desktop, and mobile.项目地址: https://gitcode.com/GitHub_Trending/di/dioxus导读在 Dioxus 应用中大量界面状态是从其他状态推导而来的例如根据原始count信号计算出的double。本文以 packages/hooks/docs/derived_state.md 为核心文档系统讲解use_memo的创建、缓存、更新与依赖传播机制并结合 Dioxus 源码中的use_memo实现与Memo底层结构说明为何 memo 只在结果真正变化时才通知依赖方、如何用use_reactive将非响应式 props 作为依赖以及链式 memo 与异步环境下的注意事项。读完你既能熟练写出正确的派生状态代码也能理解其更新链路便于排查性能与生命周期问题。一、什么是use_memo创建即运行、随依赖自动重算的派生值use_memo是 Dioxus 中创建Memo记忆化派生值的 Hook。它会创建一个新的Memo该 memo立即执行一次你传入的闭包并且在之后每当闭包内读取到的任意信号被写入时自动重算。use dioxus::prelude::*; use dioxus_signals::*; fn App() - Element { let mut count use_signal(|| 0); // double 这个 memo 永远等于 count 的两倍即使 count 随后变化也会保持一致 let double use_memo(move || count * 2); rsx! { {double} button { // 当 count 变化时memo 会重跑double 会被更新 // memo 重跑后只有当结果发生变化时才去重跑那些依赖它的值或组件 onclick: move |_| count 1, Increment } } }这段代码对应官方文档的原始示例把count直接显示到 UI再把count * 2封装为 memo。点击按钮后count信号被写入memo 立即重算而界面里读取double的部分只有在 memo 输出真正变化时才重新渲染。值得注意的一个精细语义传入 memo 的闭包会在其内部读取的任意状态被写入时被调用——即使该状态的实际值并没有变化但 memo不会因此重跑应用中依赖它的其它部分除非 memo 的输出确实改变了即新旧值经PartialEq比较返回false。二、把 Memo 当作只读信号来读、传与显示Memo的行为非常接近一个只读的Signal你可以读取它、在 UI 中显示它、把它当作普通值在组件间传递也可以调用它double()克隆内部值。但它不能被直接写入——要更新它唯一途径是写入它所依赖的底层信号。# use dioxus::prelude::*; let mut count use_signal(|| 1); // double_count 会在闭包内读取到的状态即 count变化时重跑 let double_count use_memo(move || count() * 2); // memo 用法与只读信号基本一致可读取、可显示、可像其它信号一样到处传递 println!({}, double_count); // 打印 2 // 不能直接写入 memo取而代之的是任何一次对被读信号的写入都会触发 memo 重跑 count 1; println!({}, double_count); // 打印 4 // 再来创建一个读取 double_count 的 memo let double_count_plus_one use_memo(move || double_count() 1); println!({}, double_count_plus_one); // 打印 5 // 写入 count 会让 double_count 重跑 // 只有当 double_count 的输出改变时才会进一步触发 double_count_plus_one 重跑 count 1; println!({}, double_count); // 打印 6 println!({}, double_count_plus_one); // 打印 7 // 但如果一次写入没有改变 double_count 的输出它就不会触发下游 memo 重跑 // 这里我们写入的恰好是同一个值翻倍后仍是 6因此 double_count_plus_one 不会重跑 *count.write() 3; println!({}, double_count); // 打印 6 println!({}, double_count_plus_one); // 打印 72.1 从源码看读取时的懒重算机制为什么上面的例子中把3写进count后double_count打印仍是6这是因为Memo的读取路径内置了脏标记检查。Memo 的定义 中MemoT只包含两个字段pub struct MemoT { inner: SignalT, // 存放最终计算结果的内部信号 update: CopyValueUpdateInformationT, // 记录脏标记与重算回调 }其中UpdateInformationT包含一个dirty: ArcAtomicBool和封装了原闭包的重算回调。在Readable for MemoT的try_read_unchecked中读取时如果发现脏标记为真会同步重算一次再返回值随后才把当前响应式上下文ReactiveContext订阅到 memo 上。而重算函数recompute见 memo.rs 中的fn recompute会把新计算结果与inner.peek()得到的旧值做!比较仅在二者不同时才真正写回内部信号否则只清除脏标记。这正是值没变就不打扰下游的底层实现。也就是说每次信号写入后 memo 的重跑其实是异步批处理的任务写入方会投递一条消息后台循环把积压的多次写入合并后执行一次recompute。测试 packages/hooks/tests/memo.rs 中的use_memo_only_triggers_one_update专门验证了这一点——连续写入count十次下游观察者只被触发一次更新。三、链式 memo派生值之间的级联与剪枝memo 可以读取其它 memo从而构成一棵派生计算树上游 memo 输出变化 → 触发下游 memo 重算 → 依此逐级传播。而输出未变则不下发的剪枝规则保证了链上无关分支不会白白重跑。examples/04-managing-state/memo_chain.rs 给出了一个直观的演示根组件维护value与depth两个信号先算出一层items与statememo递归的Child组件再基于传入的 memo 逐层派生state 1、items()[depth() - 1]等新 memo。这个例子还展示了两个额外特性memo 会随父组件一起挂起/恢复父组件不渲染时依赖它的 memo 也不会被重复计算子组件内创建 memo 后在if depth() 0之前调用——它不会被之后可能发生的 early return 跳过。从测试层面看packages/hooks/tests/memo.rs 的memo_updates覆盖了真实运行时的边界情况memo 在其它线程修改信号时同步更新、元素删除后其内部 memo 不再重跑并断言len_memo始终与信号真实长度一致。四、当依赖不是信号用use_reactive订阅非响应式数据use_memo的闭包默认只自动订阅它读取到的信号。如果 memo 需要依赖组件传入的普通 props如count: u32这些值不会自动成为响应式依赖——它们只是组件重渲染时闭包被重新构造拿到的快照。为了在 props 变化时也能触发 memo 重跑可以借助use_reactive把非响应式数据包装成响应式依赖# use dioxus::prelude::*; #[component] fn Comp(count: u32) - Element { // 因为闭包通过依赖元组订阅了 count所以每当 count 变化时 memo 都会重跑 let new_count use_memo(use_reactive((count,), |(count,)| count 1)); todo!() }4.1use_reactive的工作原理use_reactive的实现位于 packages/hooks/src/use_reactive.rs。它定义了一个Dependencytrait凡是PartialEq Clone的类型以及少于 8 个成员的引用元组如(A, B, C)都会自动实现它。其核心思路是内部用use_signal保存上一次的依赖值快照每次组件运行时通过changed()比较当前依赖元组与快照若不同就更新快照返回的闭包再执行你传入的计算逻辑从而让普通数据也具备变化即触发的效果。对于信号类型的依赖则无需此包装——信号在被读取的瞬间就自动完成了订阅注册。此外源码中还提供了一个声明式宏use_reactive!(|count| count 1)把传元组 解构参数的样板隐藏起来效果完全等价# use dioxus::prelude::*; let data 5; use_effect(use_reactive!(|data| { println!(Data changed: {}, data); }));五、use_memo背后的 Hook 实现理解了Memo之后再看 Hook 层就非常清晰。packages/hooks/src/use_memo.rs 的完整实现只有十余行pub fn use_memoR: PartialEq static(mut f: impl FnMut() - R static) - MemoR { let callback use_callback(move |_| f()); let caller std::panic::Location::caller(); #[allow(clippy::redundant_closure)] use_hook(|| Memo::new_with_location(move || callback(()), caller)) }从中可以看出几个事实use_memo对返回类型R的唯一静态约束是PartialEq static——PartialEq用于输出是否变化的判断static源于 Dioxus 对存储值的要求它经由use_callback包装闭包再通过use_hook挂载Memo::new_with_location保证 memo 与当前组件 scope 的声明顺序绑定use_memo所在的hooks包是一系列 Hookuse_signal、use_effect、use_resource等的集合本文件对应的文档正是通过include_str!(../docs/derived_state.md)内嵌进 rustdoc 的也就是说你在本文看到的这份官方说明同时也是 hook 的 API 文档正文。因为它是 Hook就必须遵守 Dioxus 的 Hook 规则只能在组件或其它 Hook 的函数体内以固定顺序调用不能放进条件分支、循环、事件处理器或初始化闭包中详见 packages/hooks/docs/rules_of_hooks.md。六、Memo 的生命周期属于创建它的组件别往上传递Memo的生命周期与其创建它的组件绑定。Dioxus 用 generational-box 把 memo 做成Copy值方便在 UI 中传递但当创建 memo 的组件被销毁时memo 也会随之失效。典型的坑是把子组件创建的 memo 传给父组件父组件持有它可子组件一旦卸载memo 即被回收后续读取可能触发 panic。官方建议尽量在组件树中足够高的位置创建需要共享的 memo需要真正全局共享的派生值可以使用Memo::global(...)生成GlobalMemo每个 app 实例首次使用static DOUBLED: GlobalMemoi32 Memo::global(|| SIGNAL() * 2);时自动创建一次。不过 packages/signals/docs/memo.md 也提示库作者应慎用全局 memo因为它会让同一组件被实例化多份且状态各不相同变得困难。关于在组件间搬运状态的一般性策略props 直传、use_context上下文共享、GlobalSignal全局状态可参考 packages/hooks/docs/moving_state_around.md。七、异步代码中的安全读取不要在 await 上悬挂读取由于Memo的借用检查发生在运行时在异步代码里读取 memo 需要格外小心不要在 await 点之间持有 memo 的读借用。如果未来正在等待期间 memo 因底层信号变化而重算仍打开着的读借用会导致 panic# use dioxus::prelude::*; # async fn sleep(delay: u32) {} async fn double_me_async(value: u32) - u32 { sleep(100).await; *value * 2 } let mut signal use_signal(|| 0); let halved use_memo(move || signal() / 2); let doubled use_resource(move || async move { let halved halved.read(); // ❌ 该读借用跨越了 await 点 double_me_async(halved).await });安全做法是先把需要的值克隆出来再进入异步段——直接调用memohalved()会克隆内部值是跨 await 的首选方式# use dioxus::prelude::*; # async fn sleep(delay: u32) {} async fn double_me_async(value: u32) - u32 { sleep(100).await; value * 2 } let mut signal use_signal(|| 0); let halved use_memo(move || signal() / 2); let doubled use_resource(move || async move { let halved halved(); // ✅ 调用 memo 克隆内部值 double_me_async(halved).await });这一节完整内容同样收录在 packages/signals/docs/memo.mdMemos with Async 章节中同时还列出了Readable、ReadableVecExt、ReadableOptionExt等 trait 提供的辅助方法供需要更精细读取控制时查阅。八、实践要点小结场景推荐做法由信号派生的普通数值/列表翻倍、过滤、排序、求和等use_memo(move || /* 只读信号 */)依赖组件 props 等非响应式输入use_memo(use_reactive((count,), |(count,)| ...))或use_memo(use_reactive!(|count| ...))派生值需要被多个组件/层级共享在较高层创建 memo经 props 下发或视情况用Memo::global只想在结果变化时才刷新下游什么都不用做——memo 默认按PartialEq比较结果再决定是否通知异步 future 中需要 memo 的值先halved()克隆再await切勿持有读借用跨 await派生值不随输入立即重算或与 UI 无关的副作用考虑改用use_effect见 side_effects.md九、更多可研读的仓库素材本文核心文档packages/hooks/docs/derived_state.mduse_memoHook 实现packages/hooks/src/use_memo.rsuse_reactive实现与Dependencytraitpackages/hooks/src/use_reactive.rsMemo底层结构、脏标记与重算逻辑packages/signals/src/memo.rsMemo 文档读取辅助、异步、生命周期packages/signals/docs/memo.md行为验证测试packages/hooks/tests/memo.rs链式 memo 运行示例examples/04-managing-state/memo_chain.rs【免费下载链接】dioxusFullstack app framework for web, desktop, and mobile.项目地址: https://gitcode.com/GitHub_Trending/di/dioxus创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表