Dioxus异步

Dioxus 异步任务:从“发请求”到“统一加载”的完整指南

在现代应用中,异步操作无处不在

  • 点击按钮后去服务器查数据
  • 页面加载时自动获取用户信息
  • 搜索框输入时实时显示结果

Dioxus 提供了多种方式来处理这些异步任务,每种方式都有其“最佳使用场景”。下面我们一一拆解。


一、spawn:后台悄悄干活,干完就走

适用场景:

“我只需要启动一个任务,不关心它什么时候结束,也不需要它的结果。”

比如:

  • 用户点击“点赞”后,偷偷发个日志给分析系统
  • 页面加载完成后,上报一次“页面访问”事件

基础用法(显式调用 spawn):

let mut status = use_signal(|| "点击开始".to_string());

rsx! {
    button {
        onclick: move |_| {
            status.set("正在发送...".into());
            spawn(async move {
                // 模拟发请求
                let res = reqwest::get("https://httpbin.org/get").await;
                if res.is_ok() {
                    status.set("发送成功!".into());
                } else {
                    status.set("发送失败!".into());
                }
            });
        },
        "{status}"
    }
}

💡 spawn 启动的任务会在组件卸载时自动取消,避免内存泄漏。


✨ 更简洁的写法:直接返回 async 闭包

Dioxus 很聪明:如果你在事件处理函数里返回一个 async 闭包,它会自动帮你 spawn

onclick: move |_| async move {
    status.set("正在发送...".into());
    let res = reqwest::get("https://httpbin.org/get").await;
    status.set(if res.is_ok() { "成功!" } else { "失败!" }.into());
}

这种写法更简洁,适合“一次性异步操作”。


注意:需要“一直运行”的任务?

如果任务不能被取消(比如上传大文件),请用 spawn_forever

spawn_forever(async move {
    // 即使页面跳走了,这个任务也会继续运行
    upload_big_file().await;
});

二、use_resource:异步状态管理器

适用场景:

“我需要一个会自动更新的异步数据,比如根据用户输入实时搜索。”

它就像一个“智能缓存”:

  • 第一次加载 → 发请求
  • 依赖变了(比如搜索词变了)→ 自动重新发请求
  • 组件自动根据“加载中 / 成功 / 失败”状态更新 UI

示例:根据品种搜索狗狗图片

let breed = use_signal(|| "poodle".to_string());
let dogs = use_resource(move || async move {
    // 注意:这里读取了 breed(),所以 use_resource 会“监听”breed 的变化
    let url = format!("https://dog.ceo/api/breed/{}/images", breed());
    reqwest::get(&url).await?.json::<DogResponse>().await
});

rsx! {
    input {
        value: "{breed}",
        oninput: move |e| breed.set(e.value()),
    }

    // 根据 dogs 的状态显示不同内容
    match dogs.read().as_ref() {
        None => rsx! { "加载中..." },
        Some(Ok(data)) => rsx! {
            for img in data.images.iter().take(3) {
                img { src: "{img}" }
            }
        },
        Some(Err(e)) => rsx! { "出错了:{e}" }
    }
}

关键点:只要在 use_resource 的闭包里读了某个 Signal(如 breed()),它就会自动订阅这个 Signal。一旦 breed 改变,请求就自动重发!


重要区别:use_resource vs use_memo

use_memouse_resource
同步/异步同步异步
是否比较结果是(值不变就不更新)(只要重新运行,就触发更新)
适用场景计算派生值(如格式化)获取远程数据

所以,即使两次请求返回完全相同的数据use_resource 也会让 UI 重新渲染(因为它不比较结果)。


警告:Future 必须是“可取消安全”的!

use_resource 的任务可能在中途被取消(比如用户快速切换搜索词)。 如果你在任务中修改了全局状态,必须确保“即使任务被取消,状态也能恢复”。

错误示例(有内存泄漏风险):
static ACTIVE_TASKS: GlobalSignal<i32> = Signal::global(|| 0);

let data = use_resource(move || async move {
    ACTIVE_TASKS += 1; // 开始任务
    let result = fetch_data().await;
    ACTIVE_TASKS -= 1; // 结束任务
    result
});

如果任务在 fetch_data() 时被取消,ACTIVE_TASKS -= 1 就不会执行!计数器就错了。

正确做法:用 Drop 自动清理
struct TaskGuard;
impl Drop for TaskGuard {
    fn drop(&mut self) {
        ACTIVE_TASKS -= 1;
    }
}

let data = use_resource(move || async move {
    ACTIVE_TASKS += 1;
    let _guard = TaskGuard; // 任务结束或取消时自动调用 drop
    fetch_data().await
});

三、SuspenseBoundary:统一加载体验

适用场景:

“我有多个异步组件,不想每个都写‘加载中…’,想用一个统一的加载动画。”

比如:一个页面同时加载“用户信息”、“订单列表”、“推荐商品”——只要有一个没加载完,就显示“整体加载中”。


基础用法:包裹多个异步组件

fn UserProfilePage() -> Element {
    rsx! {
        SuspenseBoundary {
            fallback: |_| rsx! { "正在拼命加载中,请稍候..." },
            div {
                UserInfo {}
                OrderList {}
                Recommendations {}
            }
        }
    }
}

#[component]
fn UserInfo() -> Element {
    let user = use_resource(|| async { fetch_user().await });
    user.suspend()?; // ⬅️ 关键:调用 .suspend() 会让组件“暂停”
    
    rsx! { "欢迎,{user.read().unwrap().name}!" }
}

.suspend()? 是关键!它告诉 Dioxus:“这个组件还没准备好,先别渲染,等数据来了再说”。


进阶:每个组件可以有自己的“加载提示”

有时候你希望加载提示更具体,比如:

  • “正在加载用户信息…”
  • “正在获取订单…”

这时可以用 .with_loading_placeholder()

#[component]
fn UserInfo() -> Element {
    let user = use_resource(|| async { fetch_user().await })
        .suspend()
        .with_loading_placeholder(|| rsx! { "正在加载用户信息..." })?;

    rsx! { "欢迎,{user.read().unwrap().name}!" }
}

然后在 SuspenseBoundary 里这样用:

SuspenseBoundary {
    fallback: |ctx| {
        // 如果有组件提供了自定义加载提示,就用它
        if let Some(placeholder) = ctx.suspense_placeholder() {
            placeholder
        } else {
            rsx! { "通用加载中..." }
        }
    },
    // ...
}

四、全栈应用:use_server_future(SSR + 水合)

适用场景:

“我想在服务器端就获取数据,首屏直接显示内容,而不是白屏加载。”

这就是 SSR(服务端渲染) + Suspense 的结合。


基本用法:

#[component]
fn BreedGallery(breed: ReadOnlySignal<String>) -> Element {
    // 在服务器上执行请求,结果序列化后发给浏览器
    let response = use_server_future(move || async move {
        fetch_dog_images(&breed()).await
    })?; // 自动 suspend,无需手动调用

    // 此时 response 肯定有值(因为 SSR 已完成)
    let data = response.read().as_ref().unwrap();
    rsx! { /* 渲染图片 */ }
}

优势:用户打开页面时,直接看到狗狗图片,而不是“加载中…”。

注意:响应式范围不同!

use_server_future闭包是响应式的,但里面的 async block 不是

let search = use_signal(|| "poodle".to_string());

// 正确:在闭包里读取信号
use_server_future(move || {
    let q = search(); // ← 这里会订阅 search
    async move {
        fetch(q).await // ← 这里不会订阅任何信号
    }
});

// 错误:在 async block 里读信号(不会触发更新!)
use_server_future(move || async move {
    fetch(search()).await // ← search() 不会被追踪!
});

启用流式 SSR

默认 SSR 会等所有数据加载完才返回 HTML。 但你可以开启 “乱序流式传输”,让先加载完的部分先显示:

fn main() {
    LaunchBuilder::new()
        .with_cfg(server_only! {
            ServeConfig::builder().enable_out_of_order_streaming()
        })
        .launch(App);
}

效果:页面像“拼图”一样,一块一块地出现,而不是等全部拼完才显示。

五、总结:如何选择?

你的需求推荐方案
启动一个“fire-and-forget”任务(如打点)spawnasync 事件处理器
根据状态自动刷新的异步数据(如搜索)use_resource
多个异步组件共享一个加载状态SuspenseBoundary + .suspend()
首屏直出内容(SEO/体验优化)use_server_future + SSR
任务不能被取消(如上传)spawn_forever
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

编码浪子

您的支持将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值