5.3 事件流

> **本节目标**:掌握 RML 的事件流机制——冒泡、捕获、阻止默认行为、阻止冒泡。

5.3.1 事件流的三个阶段

RML 借鉴 DOM 事件模型,事件流分为三个阶段:

1. 捕获阶段(Capture):从根元素向目标元素传播
2. 目标阶段(Target):事件到达目标元素
3. 冒泡阶段(Bubble):从目标元素向根元素传播
┌─────────────────────────────────────┐
│           根元素 <div>              │  ← 捕获开始
│  ┌─────────────────────────────┐    │
│  │      父元素 <div>           │    │  ← 捕获
│  │  ┌─────────────────────┐    │    │
│  │  │   目标 <button>     │    │    │  ← 目标(点击的元素)
│  │  └─────────────────────┘    │    │  ← 冒泡开始
│  └─────────────────────────────┘    │  ← 冒泡
└─────────────────────────────────────┘  ← 冒泡结束

5.3.2 冒泡机制

默认情况下,RML 事件在冒泡阶段触发。事件从目标元素开始,逐级向上传播到根元素:

<div on-click={on_outer_click}>
    <div on-click={on_middle_click}>
        <button on-click={on_inner_click}>点击我</button>
    </div>
</div>

点击 `<button>` 时,事件触发顺序:

1. `on_inner_click`(目标元素) 2. `on_middle_click`(父元素) 3. `on_outer_click`(祖父元素)

冒泡的用途

冒泡允许父元素统一处理子元素的事件:

<ul on-click={on_list_click}>
    <li each={item in items} key={item.id} data-id={item.id}>
        {item.text}
    </li>
</ul>
#[command]
pub fn on_list_click(&mut self, ev: &ClickEvent, cx: &mut ViewContext<Self>) {
    // 通过事件目标判断点击了哪个 <li>
    if let Some(item_id) = ev.target_data("id") {
        self.select_item(item_id.parse().unwrap(), cx);
    }
}

5.3.3 阻止冒泡

用 `stop_propagation()` 阻止事件继续冒泡:

<div on-click={on_outer_click}>
    <button on-click={on_inner_click}>点击我</button>
</div>
#[command]
pub fn on_inner_click(&mut self, ev: &ClickEvent, cx: &mut ViewContext<Self>) {
    ev.stop_propagation();  // 阻止冒泡,on_outer_click 不会触发

    self.handle_button_click(cx);
}

#[command]
pub fn on_outer_click(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
    // 不会触发,因为内部阻止了冒泡
    self.handle_outer_click(cx);
}

阻止冒泡的场景

- 模态对话框内部点击不应关闭对话框 - 下拉菜单内部点击不应触发外部点击 - 嵌套列表的子项点击不应触发父项点击

<div on-click={close_modal} class="modal-overlay">
    <div on-click={on_modal_content_click} class="modal-content">
        <!-- 点击这里不应关闭对话框 -->
        <p>对话框内容</p>
    </div>
</div>
#[command]
pub fn on_modal_content_click(&mut self, ev: &ClickEvent, _cx: &mut ViewContext<Self>) {
    ev.stop_propagation();  // 阻止冒泡,不关闭对话框
}

#[command]
pub fn close_modal(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
    self.is_modal_open = false;
    cx.notify();
}

5.3.4 阻止默认行为

某些事件有默认行为,可以用 `prevent_default()` 阻止:

#[command]
pub fn on_key_down(&mut self, ev: &KeyDownEvent, cx: &mut ViewContext<Self>) {
    // Ctrl+S 默认会触发浏览器保存,阻止它
    if ev.modifiers.control && ev.key == Key::Character('s') {
        ev.prevent_default();
        self.save(cx);
    }
}

有默认行为的事件

| 事件 | 默认行为 | 阻止后的效果 | | ------------ | -------------- | ------------- | | `on-click`(链接) | 导航到 href | 不导航 | | `onkeydown`(Tab) | 切换焦点 | 不切换焦点 | | `onsubmit` | 提交表单 | 不提交,由 JS 处理 | | `onwheel` | 滚动 | 不滚动 |

5.3.5 事件委托

事件委托是冒泡机制的经典应用:把多个子元素的事件统一交给父元素处理。

传统方式:每个子元素绑定事件

<ul>
    <li each={item in items} key={item.id} on-click={select_item, {item.id}}>
        {item.text}
    </li>
</ul>

事件委托:父元素统一处理

<ul on-click={on_list_click}>
    <li each={item in items} key={item.id} data-id={item.id}>
        {item.text}
    </li>
</ul>
#[command]
pub fn on_list_click(&mut self, ev: &ClickEvent, cx: &mut ViewContext<Self>) {
    // 通过 data-id 属性识别点击的项
    if let Some(id_str) = ev.target_attribute("data-id") {
        let id: u64 = id_str.parse().unwrap();
        self.select_item(id, cx);
    }
}

事件委托的优点

| 优点 | 说明 | | ------ | --------------------------- | | 减少绑定数 | 只在父元素绑定一次,子元素无需单独绑定 | | 动态友好 | 新增子元素自动被父元素处理,无需重新绑定 | | 性能优化 | 减少订阅数量,降低内存占用 |

⚠️ **注意**:事件委托适用于点击、键盘等冒泡事件,不适用于 `onmouseenter`、`onmouseleave` 等不冒泡的事件。

5.3.6 事件流的控制

完整示例

<div on-click={on_root_click} class="root">
    <div on-click={on_middle_click} class="middle">
        <button on-click={on_button_click} class="button">
            点击我
        </button>
    </div>
</div>
#[command]
pub fn on_button_click(&mut self, ev: &ClickEvent, cx: &mut ViewContext<Self>) {
    println!("1. 按钮点击");
    // ev.stop_propagation();  // 取消注释则阻止冒泡
    // ev.prevent_default();   // 取消注释则阻止默认行为
    self.handle_button(cx);
}

#[command]
pub fn on_middle_click(&mut self, ev: &ClickEvent, cx: &mut ViewContext<Self>) {
    println!("2. 中间元素点击");
    self.handle_middle(cx);
}

#[command]
pub fn on_root_click(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
    println!("3. 根元素点击");
    self.handle_root(cx);
}

控制选项

| 调用 | 效果 | | ------------------------ | ------------------- | | 无 | 事件正常冒泡,依次触发三个处理函数 | | `ev.stop_propagation()` | 只触发 `on_button_click` | | `ev.prevent_default()` | 阻止默认行为,但事件仍冒泡 | | 两者都调用 | 既阻止默认行为,又阻止冒泡 |

5.3.7 自定义事件与冒泡

自定义组件的事件也会冒泡,可以通过 `stop_propagation()` 控制:

<div on-click={on_outer_click}>
    <SearchBox on_search={on_search} />
</div>
// SearchBox 组件内部
#[command]
pub fn on_search_internal(&mut self, ev: &SearchEvent, cx: &mut ViewContext<Self>) {
    // 处理搜索...
    ev.stop_propagation();  // 阻止冒泡到外部 div
}

详见 [5.4 自定义事件](./custom-events.md)。

5.3.8 事件流的最佳实践

1. 谨慎阻止冒泡

// ❌ 滥用 stop_propagation
#[command]
pub fn on_click(&mut self, ev: &ClickEvent, cx: &mut ViewContext<Self>) {
    ev.stop_propagation();  // 总是阻止,可能影响外部逻辑
    self.handle(cx);
}

// ✅ 只在必要时阻止
#[command]
pub fn on_modal_content_click(&mut self, ev: &ClickEvent, _cx: &mut ViewContext<Self>) {
    ev.stop_propagation();  // 模态框内部点击,明确需要阻止
}

2. 优先用事件委托

<!-- ❌ 每个子元素都绑定 -->
<li each={item in items} on-click={select, {item.id}}>

<!-- ✅ 父元素事件委托 -->
<ul on-click={on_list_click}>
    <li each={item in items} data-id={item.id}>

3. 阻止默认行为要明确

// ❌ 不清楚为什么阻止
#[command]
pub fn on_key_down(&mut self, ev: &KeyDownEvent, cx: &mut ViewContext<Self>) {
    ev.prevent_default();
    self.handle(cx);
}

// ✅ 明确阻止原因
#[command]
pub fn on_key_down(&mut self, ev: &KeyDownEvent, cx: &mut ViewContext<Self>) {
    if ev.modifiers.control && ev.key == Key::Character('s') {
        ev.prevent_default();  // 阻止浏览器保存,使用应用内保存
        self.save(cx);
    }
}

5.3.9 小结

RML 的事件流机制:

- **三个阶段**:捕获 → 目标 → 冒泡 - **默认冒泡**:事件从目标元素向上传播 - **`stop_propagation()`**:阻止事件继续冒泡 - **`prevent_default()`**:阻止事件的默认行为 - **事件委托**:父元素统一处理子元素事件

掌握事件流,你就能精确控制事件的传播路径,实现复杂的交互逻辑。

下一节 → [5.4 自定义事件](./custom-events.md)