Dioxus 开发指南

钩子(Hooks)与内部状态(State)

在本章节中,我们将更加深入的了解 Hook 的工作原理。

Hooks 理论§

在过去的几十年里,计算机学者和开发者们一直在试图寻找一种设计用户界面的 “正确方法”。

现在来看,已经出现了很多种设计思路,而它们都各有优势,而大致上有这两种:

  • 即时式用户界面 - 它会在每次更新时刷新整个界面内容。
  • 保留式用户界面 - 它仅仅会更新被变化的那一部分内容。

通常来说,即时式的设计方法会更加方便开发,但随着功能的增加,也会出现很多问题。(比如性能) 现代的大部分应用都采用了 “保留式” 的方案:你的代码更改了具体的内容,但呈现程序负责实际绘制到屏幕上。 状态信息也不会因为 UI 的更新而被重置,它会在整个程序的运行周期内始终保持不变(除非人为的触发了更新), 而 Dioxus 提供了一种机制来保持这些状态信息。

在这里特意声明一下:在运行过程中,一个组件的函数(包括 app 的入口),都不止运行一次,它会在数据更新后被重新调用。 这就是上面说到保存的意思,使用 Hook 工具下的 API 获取的值、引用、可变引用都是已经被保存的值(也就是包含修改的值),而不是第一次初始化的值。

Hooks 机制§

为了在重复的渲染期间保持状态,Dioxus 为 Hook 提供了 use_hook 的 API ,它会返回一个可变的数据引用。 它来自于 use_hook 中的初始化函数最终值。

fn example(cx: Scope) -> Element {
    let name: &mut String = cx.use_hook(|| "John Doe".to_string());
    // 后续代码...
}

我们可以在事件中对它进行更新:

fn example(cx: Scope) -> Element {
    let name: &mut String = cx.use_hook(|| "John Doe".to_string());

    cx.render(rsx!(
        button {
            onclick: move |_| name.push_str(".."),
        }
    ))
}

准确来说,我们每一次调用 use_hook 都会获得一个 &mut T 的值。

fn example(cx: Scope) -> Element {
    let name: &mut String = cx.use_hook(|| "John Doe".to_string());
    let age: &mut u32 = cx.use_hook(|| 10);
    let friends: &mut Vec<String> = cx.use_hook(|| vec!["Jane Doe".to_string()]);
}

在内部,我们使用一个数组来保存每一次 use_hook 的信息,同时也包含了它们的顺序。

在内部,它们大概是这样存放的:

[
    Hook<String>,
    Hook<u32>,
    Hook<String>,
]

它会严格按照我们声明时的顺序被保存,所以说 Hooks Api 不允许被使用在任何条件语句中,它会打断顺序,从而无法完成匹配。 被打乱的调用顺序会直接的导致 Dioxus 程序的 painc 错误,因为它无法处理这种问题。当然有一种函数 try_use_hook , 但我们并不建议您在项目中使用它。

构建 Hooks§

use_hook 所返回的 &mut T 在很多场景下并不方便使用。

考虑一下,我们尝试将 &mut String 同时传递给两个事件处理器:

fn example(cx: Scope) -> Element {
    let name: &mut String = cx.use_hook(|| "John Doe".to_string());

    cx.render(rsx!(
        button { onclick: move |_| name.push_str("yes"), }
        button { onclick: move |_| name.push_str("no"), }
    ))
}

Rust 不会允许它通过编译!我们不能将唯一的可变引用 “分成两份” 来使用。 不过我们可以将可变引用重新借用为不可变引用交给两个程序使用:

fn example(cx: Scope) -> Element {
    let name: &String = &*cx.use_hook(|| "John Doe".to_string());

    cx.render(rsx!(
        button { onclick: move |_| log::info!("{}", name), }
        button { onclick: move |_| log::info!("{}", name), }
    ))
}

不过我们可以使用 Cell 来通过内部可变性来更新值,它的开销几乎为零,不过它的限制要比类似的 RefCell 多一些。

fn example(cx: Scope) -> Element {
    let name: &Cell<&'static str> = cx.use_hook(|| Cell::new("John Doe"));

    cx.render(rsx!(
        button { onclick: move |_| name.set("John"), }
        button { onclick: move |_| name.set("Jane"), }
    ))
}

通过 Hooks 更新§

类似于 use_stateuse_ref 通过封装 hooks 来实现可变 Copy , 此外,每当设置了新值,它还会将组件标记为 dirty。组件就知道需要要被更新。

fn example(cx: Scope) -> Element {
    let (name, set_name) = use_state(&cx, || "Jack");

    cx.render(rsx!(
        "Hello, {name}"
        button { onclick: move |_| set_name("John"), }
        button { onclick: move |_| set_name("Jane"), }
    ))
}

在内部,我们的 set 函数类似于这样:

impl<'a, T> UseState<'a, T> {
    fn set(&self, new: T) {
        // 将内容进行替换更新
        self.value.set(new);

        // 将当前组件标记为需要刷新
        self.cx.needs_update();
    }
}

我们所实现的大部分 Hooks 都提供了 Deref 在它们的值之中,如果你想直接访问它们的值:

fn example(cx: Scope) -> Element {
    let (name, _set_name) = use_state(&cx, || "Jack");

    match *name {
        "Jack" => {}
        "Jill" => {}
        _ => {}
    }
}

Dioxus-Hooks 提供的方法§

我们通过 Dioxus-Hooks 为各位开发者提供了以下这些钩子方法,请根据需求自行调用:

  • use_state - 储存状态信息并在适当的时候更新
  • use_ref - 使用 Refcell 存放未实现 Clone 的数据
  • use_future - 存放在初始化结束后需要的 Future
  • use_coroutine - 存放可以被 启动/暂停/通讯的 Future
  • use_noderef - 存放本地元素的句柄
  • use_callback - 存放实现了 PartialEq 的回调用于记忆化
  • use_provide_context - 暴露状态数据到派生组件中
  • use_context - 使用 use_provide_context 的状态数据