协程
在你的异步工具箱中,另一个工具是协程。协程是可以向它们发送值的future。
像常规的future一样,协程中的代码会在下一个await点之前运行并让出控制权。这种对异步任务的低级控制非常强大,允许进行无限循环任务,如WebSocket轮询、后台计时器和其他周期性操作。
use_coroutine
use_coroutine钩子允许你创建一个协程。我们编写的大多数协程将使用await的轮询循环。
use futures_util::StreamExt;
fn app() {
let ws: Coroutine<()> = use_coroutine(|rx| async move {
// 连接到某种服务
let mut conn = connect_to_ws_server().await;
// 等待服务上的数据
while let Some(msg) = conn.next().await {
// 处理消息
}
});
}
对于许多服务来说,一个简单的异步循环将处理大多数用例。
产生值
要从协程中产生值,只需引入一个Signal句柄,并在协程完成工作时设置值。
协程必须是'static的——因此,任务捕获的任何值都不能携带对cx的引用,例如Signal。
你可以使用to_owned来创建钩子句柄的副本,可以将其移动到异步闭包中。
let sync_status = use_signal(|| Status::Launching);
let sync_task = use_coroutine(|rx: UnboundedReceiver<SyncAction>| {
let mut sync_status = sync_status.to_owned();
async move {
loop {
tokio::time::sleep(Duration::from_secs(1)).await;
sync_status.set(Status::Working);
}
}
});
为了使这更简洁,Dioxus导出了to_owned!宏,它将创建如上所示的绑定,这在处理许多值时非常有用。
let sync_status = use_signal(|| Status::Launching);
let load_status = use_signal(|| Status::Launching);
let sync_task = use_coroutine(|rx: UnboundedReceiver<SyncAction>| {
async move {
// ...
}
});
发送值
你可能已经注意到use_coroutine闭包接受一个名为rx的参数。那是什么?在复杂应用中,一个常见的模式是同时处理一堆异步代码。使用像Redux Toolkit这样的库,同时管理多个承诺可能是具有挑战性的,并且是常见的错误来源。
使用协程,我们可以集中我们的异步逻辑。rx参数是一个通道,允许协程外部的代码向协程发送数据。我们不是在外部服务上循环,而是在通道本身上循环,处理来自我们应用内部的消息,而不需要生成新的future。要向协程发送数据,我们将在句柄上调用"send"。
use futures_util::StreamExt;
enum ProfileUpdate {
SetUsername(String),
SetAge(i32),
}
let profile = use_coroutine(|mut rx: UnboundedReceiver<ProfileUpdate>| async move {
let mut server = connect_to_server().await;
while let Some(msg) = rx.next().await {
match msg {
ProfileUpdate::SetUsername(name) => server.update_username(name).await,
ProfileUpdate::SetAge(age) => server.update_age(age).await,
}
}
});
rsx! {
button { onclick: move |_| profile.send(ProfileUpdate::SetUsername("Bob".to_string())),
"Update username"
}
}
注意:为了使用/运行
rx.next().await语句,你需要通过将futures_util作为依赖项添加到你的项目中,并将use futures_util::stream::StreamExt;添加到你的代码中,来扩展[Stream]特征(由[UnboundedReceiver]使用)。
对于足够复杂的应用,我们可以构建许多不同的有用“服务”,这些服务在通道上循环以更新应用。
let profile = use_coroutine(profile_service);
let editor = use_coroutine(editor_service);
let sync = use_coroutine(sync_service);
async fn profile_service(rx: UnboundedReceiver<ProfileCommand>) {
// 做一些事情
}
async fn sync_service(rx: UnboundedReceiver<SyncCommand>) {
// 做一些事情
}
async fn editor_service(rx: UnboundedReceiver<EditorCommand>) {
// 做一些事情
}
我们可以将协程与全局状态结合使用,以模仿Redux Toolkit的Thunk系统,但要少得多的麻烦。这让我们可以将所有应用状态存储在任务内,然后只需更新存储在原子中的“视图”值。这种技术的强大之处在于:我们获得了原生Rust任务的所有好处,以及全局状态的优化和人体工程学。这意味着你的实际状态不需要被绑定在像Signal::global或Redux这样的系统中——唯一需要存在的原子是那些用来驱动显示/UI的原子。
static USERNAME: GlobalSignal<String> = Signal::global(|| "default".to_string());
fn app() -> Element {
use_coroutine(sync_service);
rsx! { Banner {} }
}
fn Banner() -> Element {
rsx! { h1 { "Welcome back, {USERNAME}" } }
}
现在,在我们的同步服务中,我们可以按我们想要的方式构建我们的状态。我们只需要在准备好时更新视图值。
use futures_util::StreamExt;
static USERNAME: GlobalSignal<String> = Signal::global(|| "default".to_string());
static ERRORS: GlobalSignal<Vec<String>> = Signal::global(|| Vec::new());
enum SyncAction {
SetUsername(String),
}
async fn sync_service(mut rx: UnboundedReceiver<SyncAction>) {
while let Some(msg) = rx.next().await {
match msg {
SyncAction::SetUsername(name) => {
if set_name_on_server(&name).await.is_ok() {
*USERNAME.write() = name;
} else {
*ERRORS.write() = vec!["Failed to set username".to_string()];
}
}
}
}
}
自动注入到上下文API
协程句柄通过上下文API自动注入。你可以使用use_coroutine_handle钩子和消息类型作为泛型来获取句柄。
fn Child() -> Element {
let sync_task = use_coroutine_handle::<SyncAction>();
sync_task.send(SyncAction::SetUsername);
todo!()
}