异步任务

处理异步任务

异步任务是任何现代应用程序的核心组成部分。Dioxus 提供了多种处理异步任务的方法。本指南将介绍每种方法的使用方式。若您已明确所需的异步任务类型,可直接跳至对应任务章节:

  • spawn 适用于需在后台运行且不返回值的未来任务
  • use_resource 在处理异步状态时能精确控制未来任务运行期间的行为
  • 可与 Suspense 结合使用,通过同一加载视图处理多个待办任务

使用 spawn 运行异步任务

spawn 方法在后台启动未来任务,并返回可用于取消任务的 Task 对象。该方法特别适用于启动后无需关注的任务,例如向服务器发送分析数据:

rust 复制代码
let mut response = use_signal(|| "Click to start a request".to_string());

rsx! {
    button {
        onclick: move |_| {
            response.set("...".into());
            // Spawn will start a task running in the background
            spawn(async move {
                let resp = reqwest::Client::new()
                    .get("https://dioxuslabs.com")
                    .send()
                    .await;

                if resp.is_ok() {
                    response.set("dioxuslabs.com responded!".into());
                } else  {
                    response.set("failed to fetch response!".into());
                }
            });
        },
        "{response}"
    }
}

由于在事件处理程序中创建任务非常常见,Dioxus 为异步事件处理程序提供了更简洁的语法。若从事件处理程序中返回一个未来对象,Dioxus 将自动创建该任务:

rust 复制代码
let mut response = use_signal(|| "Click to start a request".to_string());

rsx! {
    button {
        // Async closures passed to event handlers are automatically spawned
        onclick: move |_| async move {
            response.set("...".into());
            let resp = reqwest::Client::new()
                .get("https://dioxuslabs.com")
                .send()
                .await;

            if resp.is_ok() {
                response.set("dioxuslabs.com responded!".into());
            } else  {
                response.set("failed to fetch response!".into());
            }
        },
        "{response}"
    }
}

传递给 spawn 的未来任务将在组件卸载时自动取消。若需保持未来任务持续运行直至完成,可改用 spawn_forever

使用 use_resource 实现异步状态

use_resource 可用于推导异步状态。它接受一个异步闭包来计算状态,并返回一个包含未来当前状态的追踪值。当资源的依赖项发生变化时,该资源将重新运行:

rust 复制代码
let mut breed = use_signal(|| "hound".to_string());
let dogs = use_resource(move || async move {
    reqwest::Client::new()
        // Since breed is read inside the async closure, the resource will subscribe to the signal
        // and rerun when the breed is written to
        .get(format!("https://dog.ceo/api/breed/{breed}/images"))
        .send()
        .await?
        .json::<BreedResponse>()
        .await
});

rsx! {
    input {
        value: "{breed}",
        // When the input is changed and the breed is set, the resource will rerun
        oninput: move |evt| breed.set(evt.value()),
    }

    div {
        display: "flex",
        flex_direction: "row",
        // You can read resource just like a signal. If the resource is still
        // running, it will return None
        if let Some(response) = &*dogs.read() {
            match response {
                Ok(urls) => rsx! {
                    for image in urls.iter().take(3) {
                        img {
                            src: "{image}",
                            width: "100px",
                            height: "100px",
                        }
                    }
                },
                Err(err) => rsx! { "Failed to fetch response: {err}" },
            }
        } else {
            "Loading..."
        }
    }
}

use_resource 钩子看似与 use_memo 钩子相似,但不同于 use_memo,该资源的输出不会通过 PartialEq 比较然后进行缓存。这意味着当未来重新运行时,任何读取该输出的组件/响应式钩子都会重新运行------即使返回值保持不变

rust 复制代码
let mut number = use_signal(|| 0);

// Resources rerun any time their dependencies change. They will
// rerun any reactive scopes that read the resource when they finish
// even if the value hasn't changed
let halved_resource = use_resource(move || async move { number() / 2 });

log!("Component reran");

rsx! {
    button {
        onclick: move |_| number += 1,
        "Increment"
    }
    p {
        if let Some(halved) = halved_resource() {
            "Halved: {halved}"
        } else {
            "Loading..."
        }
    }
}

注意:传递给 use_resource 的未来任务必须具备可安全取消特性。可安全取消的未来任务是指可在任意时刻中止而不会引发问题的任务。例如,以下任务不具备可安全取消特性:

rust 复制代码
static RESOURCES_RUNNING: GlobalSignal<HashSet<String>> = Signal::global(|| HashSet::new());
let mut breed = use_signal(|| "hound".to_string());
let dogs = use_resource(move || async move {
    // Modify some global state
    RESOURCES_RUNNING.write().insert(breed());

    // 等待未来任务完成。若在任务运行期间更改 breed,资源可能
	// 无预警地被取消。若发生此情况,则推入 RESOURCES_RUNNING 的 breed 将永远不会被弹出
    let response = reqwest::Client::new()
        .get(format!("https://dog.ceo/api/breed/{breed}/images"))
        .send()
        .await?
        .json::<BreedResponse>()
        .await;

    // Restore some global state
    RESOURCES_RUNNING.write().remove(&breed());

    response
});

通过确保在未来被丢弃时恢复全局状态即可解决此问题:

rust 复制代码
static RESOURCES_RUNNING: GlobalSignal<HashSet<String>> = Signal::global(|| HashSet::new());
let mut breed = use_signal(|| "hound".to_string());
let dogs = use_resource(move || async move {
    // Modify some global state
    RESOURCES_RUNNING.write().insert(breed());

    // Automatically restore the global state when the future is dropped, even if
    // isn't finished
    struct DropGuard(String);
    impl Drop for DropGuard {
        fn drop(&mut self) {
            RESOURCES_RUNNING.write().remove(&self.0);
        }
    }
    let _guard = DropGuard(breed());

    // Wait for a future to finish. The resource may cancel
    // without warning if breed is changed while the future is running. If
    // it does, then it will be dropped and the breed will be popped
    reqwest::Client::new()
        .get(format!("https://dog.ceo/api/breed/{breed}/images"))
        .send()
        .await?
        .json::<BreedResponse>()
        .await
});

异步方法通常会在其文档中说明是否支持安全取消。

使用 suspense 统一加载视图

SuspenseBoundary是将多个异步任务打包为单一加载视图的便捷方式。它接受加载闭包和子组件。您可在子组件中暂停任务,使其渲染在未来任务完成前保持静止。当任何子组件被暂停时,SuspenseBoundary将显示加载视图而非子组件。一旦悬疑任务完成,子组件将重新显示。

通过 SuspenseBoundary,我们无需单独处理每种犬种的加载状态,即可展示不同犬种的网格布局:

rust 复制代码
fn DogGrid() -> Element {
    rsx! {
        SuspenseBoundary {
            // 当任何子组件(如BreedGallery)被挂起时,此闭包将被调用,并渲染加载视图以替代子组件。
            fallback: |_| rsx! {
                div {
                    width: "100%",
                    height: "100%",
                    display: "flex",
                    align_items: "center",
                    justify_content: "center",
                    "Loading..."
                }
            },
            div {
                display: "flex",
                flex_direction: "column",
                BreedGallery {
                    breed: "hound"
                }
                BreedGallery {
                    breed: "poodle"
                }
                BreedGallery {
                    breed: "beagle"
                }
            }
        }
    }
}

#[component]
fn BreedGallery(breed: ReadOnlySignal<String>) -> Element {
    let response = use_resource(move || async move {
        // 人为延迟请求以使加载指示器更易于观察
		gloo_timers::future::TimeoutFuture::new(1000).await;
        reqwest::Client::new()
            .get(format!("https://dog.ceo/api/breed/{breed}/images"))
            .send()
            .await?
            .json::<BreedResponse>()
            .await
    })
    // 调用 .suspend()? 将暂停该组件并提前返回,而未来任务仍在运行中
    .suspend()?;

    rsx! {
        div {
            display: "flex",
            flex_direction: "row",
            match &*response.read() {
                Ok(urls) => rsx! {
                    for image in urls.iter().take(3) {
                        img {
                            src: "{image}",
                            width: "100px",
                            height: "100px",
                        }
                    }
                },
                Err(err) => rsx! { "Failed to fetch response: {err}" },
            }
        }
    }
}

若需在特定任务加载期间切换加载视图,可通过 with_loading_placeholder 方法提供替代加载视图。该方法返回的加载占位符将传递至悬疑边界,系统可能选择渲染该视图而非默认加载视图:

rust 复制代码
fn DogGrid() -> Element {
    rsx! {
        SuspenseBoundary {
            // The fallback closure accepts a SuspenseContext which contains
            // information about the suspended component
            fallback: |suspense_context: SuspenseContext| if let Some(view) = suspense_context.suspense_placeholder() {
                view
            } else {
                rsx! {
                    div {
                        width: "100%",
                        height: "100%",
                        display: "flex",
                        align_items: "center",
                        justify_content: "center",
                        "Loading..."
                    }
                }
            },
            div {
                display: "flex",
                flex_direction: "column",
                BreedGallery {
                    breed: "hound"
                }
                BreedGallery {
                    breed: "poodle"
                }
                BreedGallery {
                    breed: "beagle"
                }
            }
        }
    }
}

#[component]
fn BreedGallery(breed: ReadOnlySignal<String>) -> Element {
    let response = use_resource(move || async move {
        gloo_timers::future::TimeoutFuture::new(breed().len() as u32 * 100).await;
        reqwest::Client::new()
            .get(format!("https://dog.ceo/api/breed/{breed}/images"))
            .send()
            .await?
            .json::<BreedResponse>()
            .await
    })
    .suspend()
    // 你可以通过 with_loading_placeholder 方法将加载占位符传递给最近的 SuspenseBoundary
    .with_loading_placeholder(move || {
        rsx! {
            div {
                width: "100%",
                height: "100%",
                display: "flex",
                align_items: "center",
                justify_content: "center",
                "Loading {breed}..."
            }
        }
    })?;

    rsx! {
        div {
            display: "flex",
            flex_direction: "row",
            match &*response.read() {
                Ok(urls) => rsx! {
                    for image in urls.iter().take(3) {
                        img {
                            src: "{image}",
                            width: "100px",
                            height: "100px",
                        }
                    }
                },
                Err(err) => rsx! { "Failed to fetch response: {err}" },
            }
        }
    }
}

在全栈应用中使用 Suspense

要在全栈应用中使用 Suspense,需使用 use_server_future 钩子替代 use_resourceuse_server_future 会处理未来结果的序列化以供数据填充,并自动执行暂停操作,因此无需在未来对象上调用 .suspend()

rust 复制代码
#[component]
fn BreedGallery(breed: ReadOnlySignal<String>) -> Element {
    // use_server_future 与 use_resource 非常相似,但未来对象返回的值
    // 必须实现 Serialize 和 Deserialize 接口,且会自动暂停
    let response = use_server_future(move || async move {
        // 未来将在服务器端进行服务器端渲染(SSR),然后发送至客户端。
        reqwest::Client::new()
            .get(format!("https://dog.ceo/api/breed/{breed}/images"))
            .send()
            .await
            // reqwest::Result 未实现Serialize接口,因此我们需要将其映射为字符串,该字符串会被序列化
            .map_err(|err| err.to_string())?
            .json::<BreedResponse>()
            .await
            .map_err(|err| err.to_string())
        // use_server_future 会在内部调用 `suspend`,因此无需手动调用,但你需要通过 `?` 将悬疑变体向上传递
    })?;

    // 倘若未来仍悬而未决,它便会带着上方的`?`符号悬停归来
    // 我们可以在此处 unwrap 以获取内部结果
    let response_read = response.read();
    let response = response_read.as_ref().unwrap();

    rsx! {
        div {
            display: "flex",
            flex_direction: "row",
            match response {
                Ok(urls) => rsx! {
                    for image in urls.iter().take(3) {
                        img {
                            src: "{image}",
                            width: "100px",
                            height: "100px",
                        }
                    }
                },
                Err(err) => rsx! { "Failed to fetch response: {err}" },
            }
        }
    }
}

use_resource 不同,use_server_future 仅在闭包中具有响应性,而非未来值本身。若需订阅另一个响应式值,必须在将其传递给未来值之前,先在闭包中读取该值:

rust 复制代码
let id = use_signal(|| 0);
// ❌ 在 use_server_future 内部的 future 不是响应式的
use_server_future(move || {
    async move {
        // 因为 future 并非被动响应的,这意味着未来不会在此订阅任何读取操作。
        println!("{id}");
    }
});
// ✅ 该 use_server_future 的闭包是响应式的
use_server_future(move || {
    // 该关闭操作本身具有响应性,这意味着未来将订阅您在此处读取的任何信号。
    let cloned_id = id();
    async move {
        // 但 future 并非被动响应的,这意味着未来不会在此订阅任何读取操作。
        println!("{cloned_id}");
    }
});

当您在未启用流式传输的情况下使用全栈的悬挂功能时,Dioxus 会等待所有悬挂的未来任务完成解析后,才将解析完成的 HTML 发送给客户端。若启用无序流式传输,Dioxus 会在每个 HTML 片段解析完成后立即将其发送给客户端:

rust 复制代码
fn main() {
    dioxus::LaunchBuilder::new()
        .with_cfg(server_only! {
            // Enable out of order streaming during SSR
            dioxus::fullstack::ServeConfig::builder().enable_out_of_order_streaming()
        })
        .launch(DogGrid);
}

结论

本指南已涵盖 Dioxus 中异步任务的基础知识。有关具体钩子的更详细文档可在 docs.rs 中查阅:

  • use_resource
  • use_server_future
  • SuspenseBoundary
  • spawn
  • spawn_forever

更多关于未来任务和异步任务的示例可在 Dioxus 代码库的示例文件夹中找到。

相关推荐
leiteorz1 天前
Rust环境配置
rust
该用户已不存在2 天前
Rust Web框架大比拼:Actix vs Axum vs Rocket,别再只看跑分了
后端·rust
天翼云开发者社区2 天前
使用 Rust 实现的基础的List 和 Watch 机制
rust·云计算
该用户已不存在5 天前
Mojo vs Python vs Rust: 2025年搞AI,该学哪个?
后端·python·rust
大卫小东(Sheldon)5 天前
写了一个BBP算法的实现库,欢迎讨论
数学·rust
刘立军5 天前
本地大模型编程实战(33)用SSE实现大模型的流式输出
架构·langchain·全栈
echoarts5 天前
Rayon Rust中的数据并行库入门教程
开发语言·其他·算法·rust
前端双越老师5 天前
2025 年还有前端不会 Nodejs ?
node.js·agent·全栈
ftpeak6 天前
从零开始使用 axum-server 构建 HTTP/HTTPS 服务
网络·http·https·rust·web·web app