8.3 on_unloaded 与清理

> **本节目标**:深入掌握 `#[on_unloaded]` 回调的使用场景和资源清理最佳实践。

8.3.1 on_unloaded 的触发时机

`#[on_unloaded]` 在视图被销毁前触发:

视图被销毁
    │
    ▼
#[on_unloaded]  ← 此时触发
    │
    ▼
从视图树移除
    │
    ▼
销毁 Entity

8.3.2 on_unloaded 的用途

用途一:取消异步任务

use std::time::Duration;

#[derive(IModel)]
#[component]
pub struct DataView {
    pub data: Vec<Item>,
    load_task: Option<Task<()>>,
    refresh_timer: Option<Task<()>>,
}

impl DataView {
    #[on_loaded]
    pub fn on_loaded(&mut self, cx: &mut ViewContext<Self>) {
        self.load_data(cx);
        self.start_refresh_timer(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(timer) = self.refresh_timer.take() {
            timer.abort();
        }
    }

    fn load_data(&mut self, cx: &mut ViewContext<Self>) {
        self.load_task = Some(cx.spawn(|this, mut cx| async move {
            let data = fetch_data().await;
            let _ = this.update(&mut cx, |this, cx| {
                this.data = data;
                cx.notify();
            });
        }));
    }

    fn start_refresh_timer(&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(30))
                    .await;
                let _ = this.update(&mut cx, |this, cx| {
                    this.load_data(cx);
                });
            }
        }));
    }
}

用途二:取消订阅

#[derive(IModel)]
#[component]
pub struct NotificationView {
    pub notifications: Vec<Notification>,
    subscription: Option<Subscription>,
}

impl NotificationView {
    #[on_loaded]
    pub fn on_loaded(&mut self, cx: &mut ViewContext<Self>) {
        let service = cx.global::<Entity<NotificationService>>();
        self.subscription = Some(cx.subscribe(&service, |this, _, event, cx| {
            this.handle_notification(event, cx);
        }));
    }

    #[on_unloaded]
    pub fn on_unloaded(&mut self, _cx: &mut ViewContext<Self>) {
        // 取消订阅
        if let Some(sub) = self.subscription.take() {
            sub.unsubscribe();
        }
    }
}

用途三:保存状态

#[derive(IModel)]
#[component]
pub struct EditorView {
    pub content: SharedString,
    pub cursor_position: usize,
}

impl EditorView {
    #[on_loaded]
    pub fn on_loaded(&mut self, cx: &mut ViewContext<Self>) {
        // 从本地存储加载
        if let Some(saved) = cx.local_storage().get("editor_content") {
            self.content = saved.into();
            cx.notify();
        }
    }

    #[on_unloaded]
    pub fn on_unloaded(&mut self, cx: &mut ViewContext<Self>) {
        // 保存到本地存储
        cx.local_storage().set("editor_content", &self.content);
        cx.local_storage().set("editor_cursor", &self.cursor_position.to_string());
    }
}

用途四:释放资源

#[derive(IModel)]
#[component]
pub struct VideoPlayerView {
    pub video_url: SharedString,
    pub is_playing: bool,
    video_handle: Option<VideoHandle>,
}

impl VideoPlayerView {
    #[on_loaded]
    pub fn on_loaded(&mut self, cx: &mut ViewContext<Self>) {
        // 加载视频
        self.video_handle = Some(VideoPlayer::load(&self.video_url, cx));
    }

    #[on_unloaded]
    pub fn on_unloaded(&mut self, _cx: &mut ViewContext<Self>) {
        // 释放视频资源
        if let Some(handle) = self.video_handle.take() {
            handle.release();
        }
    }
}

用途五:通知其他视图

#[derive(IModel)]
#[component]
pub struct ChatView {
    pub is_active: bool,
    pub user_id: u64,
}

impl ChatView {
    #[on_loaded]
    pub fn on_loaded(&mut self, cx: &mut ViewContext<Self>) {
        self.is_active = true;
        cx.notify();

        // 通知服务器用户上线
        let user_id = self.user_id;
        cx.spawn(|_, mut cx| async move {
            notify_user_online(user_id).await;
        }).detach();
    }

    #[on_unloaded]
    pub fn on_unloaded(&mut self, cx: &mut ViewContext<Self>) {
        self.is_active = false;

        // 通知服务器用户下线
        let user_id = self.user_id;
        cx.spawn(|_, mut cx| async move {
            notify_user_offline(user_id).await;
        }).detach();
    }
}

8.3.3 on_unloaded 的最佳实践

1. 必须取消所有异步任务

#[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_task.take() {
        task.abort();
    }
    if let Some(task) = self.upload_task.take() {
        task.abort();
    }
}

不取消会导致任务继续运行,访问已销毁的视图,引发 panic。

2. 必须取消所有订阅

#[on_unloaded]
pub fn on_unloaded(&mut self, _cx: &mut ViewContext<Self>) {
    // ✅ 取消所有订阅
    if let Some(sub) = self.notification_sub.take() {
        sub.unsubscribe();
    }
    if let Some(sub) = self.user_state_sub.take() {
        sub.unsubscribe();
    }
}

3. 保存重要状态

#[on_unloaded]
pub fn on_unloaded(&mut self, cx: &mut ViewContext<Self>) {
    // ✅ 保存用户数据
    cx.local_storage().set("draft_content", &self.draft);
    cx.local_storage().set("scroll_position", &self.scroll_position.to_string());
}

4. 释放外部资源

#[on_unloaded]
pub fn on_unloaded(&mut self, _cx: &mut ViewContext<Self>) {
    // ✅ 释放外部资源
    if let Some(handle) = self.file_handle.take() {
        handle.close();
    }
    if let Some(connection) = self.db_connection.take() {
        connection.close();
    }
}

5. 避免在 on_unloaded 中启动新任务

// ❌ 不要在 on_unloaded 中启动新任务
#[on_unloaded]
pub fn on_unloaded(&mut self, cx: &mut ViewContext<Self>) {
    cx.spawn(|this, mut cx| async move {
        // 视图即将销毁,这个任务可能无法正常完成
        save_data().await;
    }).detach();
}

// ✅ 同步保存或提前保存
#[on_unloaded]
pub fn on_unloaded(&mut self, cx: &mut ViewContext<Self>) {
    cx.local_storage().set("data", &self.data);
}

8.3.4 资源清理清单

在 `#[on_unloaded]` 中需要清理的资源:

| 资源类型 | 清理方式 | 示例 | | ----------- | ------------------ | --------------------- | | 异步任务 | `task.abort()` | `Task<()>` | | 订阅 | `sub.unsubscribe()`| `Subscription` | | 定时器 | `timer.abort()` | `Task<()>` | | 文件句柄 | `handle.close()` | `FileHandle` | | 网络连接 | `conn.close()` | `Connection` | | 数据库连接 | `conn.close()` | `DbConnection` | | 本地存储 | `storage.set(...)` | 保存状态 |

8.3.5 完整示例:聊天应用

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

#[derive(IModel)]
#[component]
pub struct ChatView {
    pub messages: Vec<Message>,
    pub input_text: SharedString,
    pub is_connected: bool,
    pub online_users: Vec<User>,

    #[element]
    pub message_list: ElementRef<Div>,
    #[element]
    pub input_field: ElementRef<Input>,

    websocket: Option<WebSocketConnection>,
    refresh_task: Option<Task<()>>,
    message_subscription: Option<Subscription>,
}

impl ChatView {
    pub fn new() -> Self {
        Self {
            messages: Vec::new(),
            input_text: SharedString::default(),
            is_connected: false,
            online_users: Vec::new(),
            message_list: ElementRef::default(),
            input_field: ElementRef::default(),
            websocket: None,
            refresh_task: None,
            message_subscription: None,
        }
    }

    #[on_loaded]
    pub fn on_loaded(&mut self, cx: &mut ViewContext<Self>) {
        // 1. 连接 WebSocket
        self.connect_websocket(cx);

        // 2. 加载历史消息
        self.load_history(cx);

        // 3. 启动心跳定时器
        self.start_heartbeat(cx);

        // 4. 订阅全局通知
        self.subscribe_notifications(cx);

        // 5. 聚焦输入框
        self.input_field.focus(cx);
    }

    #[on_unloaded]
    pub fn on_unloaded(&mut self, cx: &mut ViewContext<Self>) {
        // 1. 保存草稿
        cx.local_storage().set("chat_draft", &self.input_text);

        // 2. 保存滚动位置
        let scroll_pos = self.message_list.scroll_position();
        cx.local_storage().set("chat_scroll", &scroll_pos.to_string());

        // 3. 取消心跳定时器
        if let Some(task) = self.refresh_task.take() {
            task.abort();
        }

        // 4. 取消订阅
        if let Some(sub) = self.message_subscription.take() {
            sub.unsubscribe();
        }

        // 5. 关闭 WebSocket 连接
        if let Some(ws) = self.websocket.take() {
            ws.close();
        }

        // 6. 通知服务器用户下线
        // 注意:这里用同步方式,因为视图即将销毁
        // 实际应用中可能需要提前处理
    }

    fn connect_websocket(&mut self, cx: &mut ViewContext<Self>) {
        cx.spawn(|this, mut cx| async move {
            match WebSocketConnection::connect("wss://chat.example.com").await {
                Ok(ws) => {
                    let _ = this.update(&mut cx, |this, cx| {
                        this.websocket = Some(ws.clone());
                        this.is_connected = true;
                        cx.notify();
                    });
                }
                Err(e) => {
                    let _ = this.update(&mut cx, |this, cx| {
                        this.is_connected = false;
                        cx.notify();
                    });
                }
            }
        }).detach();
    }

    fn load_history(&mut self, cx: &mut ViewContext<Self>) {
        cx.spawn(|this, mut cx| async move {
            let messages = fetch_message_history().await.unwrap_or_default();
            let _ = this.update(&mut cx, |this, cx| {
                this.messages = messages;
                cx.notify();
            });
        }).detach();
    }

    fn start_heartbeat(&mut self, cx: &mut ViewContext<Self>) {
        self.refresh_task = Some(cx.spawn(|this, mut cx| async move {
            loop {
                cx.background_executor()
                    .timer(Duration::from_secs(30))
                    .await;
                let _ = this.update(&mut cx, |this, cx| {
                    if let Some(ws) = &this.websocket {
                        ws.send_heartbeat();
                    }
                });
            }
        }));
    }

    fn subscribe_notifications(&mut self, cx: &mut ViewContext<Self>) {
        let notification_service = cx.global::<Entity<NotificationService>>();
        self.message_subscription = Some(cx.subscribe(
            &notification_service,
            |this, _, event, cx| {
                this.handle_notification(event, cx);
            },
        ));
    }

    fn handle_notification(&mut self, event: &NotificationEvent, cx: &mut ViewContext<Self>) {
        // 处理通知
        cx.notify();
    }

    #[command]
    pub fn send_message(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
        if self.input_text.is_empty() {
            return;
        }

        let message = Message {
            id: generate_id(),
            text: self.input_text.clone(),
            timestamp: chrono::Local::now(),
        };

        self.messages.push(message.clone());
        self.input_text = SharedString::default();
        cx.notify();

        // 发送到服务器
        if let Some(ws) = &self.websocket {
            ws.send_message(&message);
        }
    }
}

8.3.6 on_unloaded 的注意事项

1. 只触发一次

#[on_unloaded]
pub fn on_unloaded(&mut self, _cx: &mut ViewContext<Self>) {
    println!("视图卸载");  // 只打印一次
}

2. 此时还能访问 cx

#[on_unloaded]
pub fn on_unloaded(&mut self, cx: &mut ViewContext<Self>) {
    // ✅ 可以访问 cx
    cx.local_storage().set("data", &self.data);
}

3. 不能启动新的异步任务

#[on_unloaded]
pub fn on_unloaded(&mut self, cx: &mut ViewContext<Self>) {
    // ❌ 视图即将销毁,新任务无法正常完成
    cx.spawn(|this, mut cx| async move {
        save_data().await;
    }).detach();
}

4. 条件渲染会触发

<div if={show_view}>
    <MyView />
</div>

`show_view` 从 `true` 变 `false` 时,`MyView` 的 `#[on_unloaded]` 会被触发。

8.3.7 小结

`#[on_unloaded]` 是资源清理的核心回调:

- **触发时机**:视图被销毁前 - **用途**:取消任务、取消订阅、保存状态、释放资源 - **最佳实践**:必须清理所有资源,避免内存泄漏 - **限制**:不能启动新的异步任务 - **与条件渲染**:`if` 控制视图的销毁

下一节 → [8.4 异步任务管理](./async-tasks.md)