Dioxus框架中协程(Coroutines)

247 阅读4分钟

协程

在你的异步工具箱中,另一个工具是协程。协程是可以向它们发送值的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!()
}

img