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_memo | use_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”任务(如打点) | spawn 或 async 事件处理器 |
| 根据状态自动刷新的异步数据(如搜索) | use_resource |
| 多个异步组件共享一个加载状态 | SuspenseBoundary + .suspend() |
| 首屏直出内容(SEO/体验优化) | use_server_future + SSR |
| 任务不能被取消(如上传) | spawn_forever |
1781

被折叠的 条评论
为什么被折叠?



