8.4 异步任务管理

> **本节目标**:掌握 RML 中异步任务的启动、取消、错误处理和生命周期管理。

8.4.1 异步任务的场景

RML 应用中常见的异步任务:

| 场景 | 示例 | | -------- | --------------------------- | | 网络请求 | 获取数据、提交表单、上传文件 | | 定时任务 | 心跳、轮询、倒计时 | | 文件操作 | 读写文件、解析大文件 | | 计算密集型任务 | 图像处理、数据分析 | | 动画 | 过渡动画、连续动画 |

8.4.2 启动异步任务

基本用法

use rml::prelude::*;

#[derive(IModel)]
#[component]
pub struct DataView {
    pub data: Vec<Item>,
    pub is_loading: bool,
}

impl DataView {
    #[command]
    pub fn refresh(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
        self.is_loading = true;
        cx.notify();

        cx.spawn(|this, mut cx| async move {
            let data = fetch_data().await;

            let _ = this.update(&mut cx, |this, cx| {
                this.data = data;
                this.is_loading = false;
                cx.notify();
            });
        }).detach();
    }
}

spawn 的参数

cx.spawn(|this, mut cx| async move {
    // this: WeakView<Self> - 弱引用,避免循环引用
    // cx: AsyncViewContext<Self> - 异步上下文

    // 异步操作
    let result = some_async_operation().await;

    // 更新视图
    let _ = this.update(&mut cx, |this, cx| {
        // 在这里可以访问 &mut Self 和 &mut ViewContext<Self>
        this.data = result;
        cx.notify();
    });
}).detach();

detach vs 保留 Task

// detach:任务独立运行,无法取消
cx.spawn(|this, mut cx| async move {
    // ...
}).detach();

// 保留 Task:可以取消
let task = cx.spawn(|this, mut cx| async move {
    // ...
});
self.task = Some(task);  // 保存到字段

// 取消任务
if let Some(task) = self.task.take() {
    task.abort();
}

8.4.3 异步任务的取消

基本取消

#[derive(IModel)]
#[component]
pub struct SearchView {
    pub search_text: SharedString,
    pub search_results: Vec<SearchResult>,
    search_task: Option<Task<()>>,
}

impl SearchView {
    #[command]
    pub fn on_search_input(&mut self, ev: &InputEvent, cx: &mut ViewContext<Self>) {
        self.search_text = ev.value.clone();
        cx.notify();

        // 取消之前的搜索任务
        if let Some(task) = self.search_task.take() {
            task.abort();
        }

        // 启动新的搜索任务
        let query = self.search_text.clone();
        self.search_task = Some(cx.spawn(|this, mut cx| async move {
            cx.background_executor()
                .timer(std::time::Duration::from_millis(300))
                .await;

            let results = search(&query).await;
            let _ = this.update(&mut cx, |this, cx| {
                this.search_results = results;
                cx.notify();
            });
        }));
    }

    #[on_unloaded]
    pub fn on_unloaded(&mut self, _cx: &mut ViewContext<Self>) {
        // 视图卸载时取消任务
        if let Some(task) = self.search_task.take() {
            task.abort();
        }
    }
}

取消正在运行的请求

impl SearchView {
    #[command]
    pub fn cancel_search(&mut self, _: &ClickEvent, _cx: &mut ViewContext<Self>) {
        if let Some(task) = self.search_task.take() {
            task.abort();
            // 任务被取消,不会执行后续的 update
        }
    }
}

8.4.4 错误处理

Result 处理

cx.spawn(|this, mut cx| async move {
    match fetch_data().await {
        Ok(data) => {
            let _ = this.update(&mut cx, |this, cx| {
                this.data = data;
                this.error = None;
                cx.notify();
            });
        }
        Err(e) => {
            let _ = this.update(&mut cx, |this, cx| {
                this.error = Some(e.to_string().into());
                cx.notify();
            });
        }
    }
}).detach();

多个错误处理

cx.spawn(|this, mut cx| async move {
    let user_result = fetch_user().await;
    let posts_result = fetch_posts().await;

    let _ = this.update(&mut cx, |this, cx| {
        match (user_result, posts_result) {
            (Ok(user), Ok(posts)) => {
                this.user = Some(user);
                this.posts = posts;
                this.error = None;
            }
            (Ok(user), Err(e)) => {
                this.user = Some(user);
                this.error = Some(format!("加载帖子失败: {}", e).into());
            }
            (Err(e), _) => {
                this.error = Some(format!("加载用户失败: {}", e).into());
            }
        }
        this.is_loading = false;
        cx.notify();
    });
}).detach();

重试机制

async fn fetch_with_retry<T, F, Fut>(max_retries: usize, f: F) -> Result<T, String>
where
    F: Fn() -> Fut,
    Fut: std::future::Future<Output = Result<T, String>>,
{
    let mut retries = 0;
    loop {
        match f().await {
            Ok(result) => return Ok(result),
            Err(e) => {
                retries += 1;
                if retries >= max_retries {
                    return Err(e);
                }
                tokio::time::sleep(std::time::Duration::from_secs(retries as u64)).await;
            }
        }
    }
}

// 使用
cx.spawn(|this, mut cx| async move {
    let result = fetch_with_retry(3, || async {
        fetch_data().await.map_err(|e| e.to_string())
    }).await;

    let _ = this.update(&mut cx, |this, cx| {
        match result {
            Ok(data) => {
                this.data = data;
                this.error = None;
            }
            Err(e) => {
                this.error = Some(e.into());
            }
        }
        cx.notify();
    });
}).detach();

8.4.5 并发任务

并行执行

cx.spawn(|this, mut cx| async move {
    // 并行执行多个任务
    let (user, posts, friends) = tokio::join!(
        fetch_user(),
        fetch_posts(),
        fetch_friends()
    );

    let _ = this.update(&mut cx, |this, cx| {
        this.user = user.ok();
        this.posts = posts.unwrap_or_default();
        this.friends = friends.unwrap_or_default();
        this.is_loading = false;
        cx.notify();
    });
}).detach();

竞速执行

cx.spawn(|this, mut cx| async move {
    // 从多个数据源获取,用最快的那个
    let result = tokio::select! {
        result = fetch_from_cache() => result,
        result = fetch_from_api() => result,
    };

    let _ = this.update(&mut cx, |this, cx| {
        this.data = result.ok();
        cx.notify();
    });
}).detach();

顺序执行

cx.spawn(|this, mut cx| async move {
    // 顺序执行
    let user = fetch_user().await?;
    let posts = fetch_posts(user.id).await?;
    let comments = fetch_comments(user.id).await?;

    let _ = this.update(&mut cx, |this, cx| {
        this.user = Some(user);
        this.posts = posts;
        this.comments = comments;
        cx.notify();
    });
    Ok(())
}).detach();

8.4.6 定时任务

一次性定时

use std::time::Duration;

cx.spawn(|this, mut cx| async move {
    cx.background_executor()
        .timer(Duration::from_secs(5))
        .await;

    let _ = this.update(&mut cx, |this, cx| {
        this.show_notification = false;
        cx.notify();
    });
}).detach();

循环定时

use std::time::Duration;

#[derive(IModel)]
#[component]
pub struct ClockView {
    pub current_time: SharedString,
    timer: Option<Task<()>>,
}

impl ClockView {
    #[on_loaded]
    pub fn on_loaded(&mut self, cx: &mut ViewContext<Self>) {
        self.timer = Some(cx.spawn(|this, mut cx| async move {
            loop {
                cx.background_executor()
                    .timer(Duration::from_secs(1))
                    .await;

                let _ = this.update(&mut cx, |this, cx| {
                    this.current_time = chrono::Local::now()
                        .format("%H:%M:%S")
                        .to_string()
                        .into();
                    cx.notify();
                });
            }
        }));
    }

    #[on_unloaded]
    pub fn on_unloaded(&mut self, _cx: &mut ViewContext<Self>) {
        if let Some(timer) = self.timer.take() {
            timer.abort();
        }
    }
}

条件循环

self.timer = Some(cx.spawn(|this, mut cx| async move {
    let mut count = 0;
    while count < 10 {
        cx.background_executor()
            .timer(Duration::from_secs(1))
            .await;

        let should_continue = this.update(&mut cx, |this, cx| {
            this.count = count;
            cx.notify();
            this.should_continue
        }).unwrap_or(false);

        if !should_continue {
            break;
        }

        count += 1;
    }
}));

8.4.7 进度跟踪

#[derive(IModel)]
#[component]
pub struct UploadView {
    pub upload_progress: f64,
    pub is_uploading: bool,
    upload_task: Option<Task<()>>,
}

impl UploadView {
    #[command]
    pub fn start_upload(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
        self.is_uploading = true;
        self.upload_progress = 0.0;
        cx.notify();

        self.upload_task = Some(cx.spawn(|this, mut cx| async move {
            let total = 100;
            for i in 1..=total {
                // 模拟上传
                cx.background_executor()
                    .timer(Duration::from_millis(50))
                    .await;

                let _ = this.update(&mut cx, |this, cx| {
                    this.upload_progress = (i as f64 / total as f64) * 100.0;
                    cx.notify();
                });
            }

            let _ = this.update(&mut cx, |this, cx| {
                this.is_uploading = false;
                cx.notify();
            });
        }));
    }

    #[command]
    pub fn cancel_upload(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
        if let Some(task) = self.upload_task.take() {
            task.abort();
        }
        self.is_uploading = false;
        self.upload_progress = 0.0;
        cx.notify();
    }
}

8.4.8 异步任务的注意事项

1. 弱引用避免循环

cx.spawn(|this, mut cx| async move {
    // this 是 WeakView<Self>,不会阻止视图被销毁
    // 如果视图已销毁,this.update() 会返回 Err
    let _ = this.update(&mut cx, |this, cx| {
        // ...
    });
}).detach();

2. 检查 update 返回值

cx.spawn(|this, mut cx| async move {
    let data = fetch_data().await;

    let result = this.update(&mut cx, |this, cx| {
        this.data = data;
        cx.notify();
    });

    if result.is_err() {
        // 视图已被销毁,无法更新
        println!("视图已销毁,无法更新");
    }
}).detach();

3. 避免在异步任务中持有锁

// ❌ 在异步任务中持有锁可能导致死锁
cx.spawn(|this, mut cx| async move {
    let _guard = self.mutex.lock().unwrap();  // 持有锁
    let data = fetch_data().await;  // 异步操作时仍持有锁
    // ...
}).detach();

// ✅ 在 update 闭包中访问数据
cx.spawn(|this, mut cx| async move {
    let data = fetch_data().await;
    let _ = this.update(&mut cx, |this, cx| {
        // 在这里安全地访问 self
        this.data = data;
        cx.notify();
    });
}).detach();

4. 取消时清理资源

cx.spawn(|this, mut cx| async move {
    let temp_file = create_temp_file().await;

    // 使用 temp_file...

    // 如果任务被取消,这里不会执行
    // 需要在 cancel 时手动清理
}).detach();

8.4.9 完整示例:数据加载器

use std::time::Duration;
use rml::prelude::*;

#[derive(IModel)]
#[component]
pub struct DataLoader {
    pub data: Vec<Item>,
    pub is_loading: bool,
    pub error: Option<SharedString>,
    pub last_updated: Option<SharedString>,
    pub auto_refresh: bool,

    load_task: Option<Task<()>>,
    refresh_timer: Option<Task<()>>,
}

impl DataLoader {
    pub fn new() -> Self {
        Self {
            data: Vec::new(),
            is_loading: false,
            error: None,
            last_updated: None,
            auto_refresh: false,
            load_task: None,
            refresh_timer: None,
        }
    }

    #[on_loaded]
    pub fn on_loaded(&mut self, cx: &mut ViewContext<Self>) {
        self.load_data(cx);
    }

    #[on_unloaded]
    pub fn on_unloaded(&mut self, _cx: &mut ViewContext<Self>) {
        if let Some(task) = self.load_task.take() {
            task.abort();
        }
        if let Some(task) = self.refresh_timer.take() {
            task.abort();
        }
    }

    fn load_data(&mut self, cx: &mut ViewContext<Self>) {
        // 取消之前的加载任务
        if let Some(task) = self.load_task.take() {
            task.abort();
        }

        self.is_loading = true;
        self.error = None;
        cx.notify();

        self.load_task = Some(cx.spawn(|this, mut cx| async move {
            match fetch_data_with_retry(3).await {
                Ok(data) => {
                    let _ = this.update(&mut cx, |this, cx| {
                        this.data = data;
                        this.is_loading = false;
                        this.error = None;
                        this.last_updated = Some(
                            chrono::Local::now()
                                .format("%Y-%m-%d %H:%M:%S")
                                .to_string()
                                .into()
                        );
                        cx.notify();
                    });
                }
                Err(e) => {
                    let _ = this.update(&mut cx, |this, cx| {
                        this.is_loading = false;
                        this.error = Some(e.into());
                        cx.notify();
                    });
                }
            }
        }));
    }

    fn start_auto_refresh(&mut self, cx: &mut ViewContext<Self>) {
        self.refresh_timer = Some(cx.spawn(|this, mut cx| async move {
            loop {
                cx.background_executor()
                    .timer(Duration::from_secs(60))
                    .await;

                let should_refresh = this.update(&mut cx, |this, _cx| {
                    this.auto_refresh
                }).unwrap_or(false);

                if !should_refresh {
                    break;
                }

                let _ = this.update(&mut cx, |this, cx| {
                    this.load_data(cx);
                });
            }
        }));
    }

    #[command]
    pub fn refresh(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
        self.load_data(cx);
    }

    #[command]
    pub fn toggle_auto_refresh(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
        self.auto_refresh = !self.auto_refresh;

        if self.auto_refresh {
            self.start_auto_refresh(cx);
        } else {
            if let Some(task) = self.refresh_timer.take() {
                task.abort();
            }
        }

        cx.notify();
    }
}

async fn fetch_data_with_retry(max_retries: usize) -> Result<Vec<Item>, String> {
    let mut retries = 0;
    loop {
        match fetch_data().await {
            Ok(data) => return Ok(data),
            Err(e) => {
                retries += 1;
                if retries >= max_retries {
                    return Err(format!("加载失败,已重试 {} 次: {}", max_retries, e));
                }
                tokio::time::sleep(Duration::from_secs(retries as u64)).await;
            }
        }
    }
}

async fn fetch_data() -> Result<Vec<Item>, String> {
    // 模拟网络请求
    Ok(Vec::new())
}

8.4.10 小结

RML 的异步任务管理:

- **启动**:`cx.spawn()` 启动异步任务 - **取消**:保留 `Task`,调用 `task.abort()` - **错误处理**:用 `Result` 处理错误 - **并发**:`tokio::join!` 并行,`tokio::select!` 竞速 - **定时**:`cx.background_executor().timer()` - **生命周期**:在 `#[on_unloaded]` 中取消所有任务

下一节 → [8.5 资源管理](./resource-management.md)