Rust in Everything
基于 dioxus 0.7.10

事件委托

主线位置:更新主线的第一段「浏览器里的 click 冒泡到根节点上唯一的监听器 → 从目标往上找带 data-dioxus-id 的元素 → 包装事件,调 Runtime::handle_event」,以及 handle_event 返回之后的 prevent_default。handle_event 里面怎么沿模板冒泡、找监听器在 事件;监听器指令怎么从 diff 里发出来在 Mutations。

sequenceDiagram
  participant B as 浏览器
  participant R as 根元素 main
  participant W as wasm 闭包
  participant RT as Runtime
  participant U as 用户闭包
  B->>R: click 冒泡到根
  R->>W: 同一个 handler
  W->>W: 往上找 data-dioxus-id
  W->>W: 检查事件类型
  W->>RT: handle_event name event id
  RT->>U: data.into 成 MouseData
  U-->>RT: 返回
  RT-->>W: 返回
  W->>B: 需要时 preventDefault

实验和 WebsysDom 与 interpreter 用同一个纯客户端应用(exp-dioxus/web-csr,dx build --platform web 的 debug 构建,同样把构建产物里 initialize 改成先 window.__interp=this,以便读 interpreter 的状态),M2 Max、macOS 27、rustc 1.99.0、Chrome 156 headless,Node 22 的 CDP 脚本 cdp_events.mjs 驱动。脚本用 DOMDebugger.getEventListeners 查元素上真正挂着的监听器,用 Runtime.consoleAPICalled 收 info! 的输出。

监听器挂在哪:createListener

NewEventListener { name, id } 在 web 上执行的是 unified_bindings.rs 里的 new_event_listener:

    fn new_event_listener(event_name: &str<u8, evt>, id: u32, bubbles: u8) {
        r#"
            const node = this.nodes[id];
            if(node.listening){node.listening += 1;}else{node.listening = 1;}
            node.setAttribute('data-dioxus-id', `\${id}`);
            this.createListener($event_name$, node, $bubbles$);
        "#
    }

它在节点上做三件事:listening 计数加一、写上 data-dioxus-id、调 createListener。bubbles 是 Rust 侧 create_event_listener 查 event_bubbles 这张表得来的:click、input、submit、keydown、mouseover 等是 true,focus、blur、scroll、mouseenter、mouseleave、load、媒体事件等是 false,表里没有的名字一律当 true。

  createListener(event_name: string, element: HTMLElement, bubbles: boolean) {
    if (event_name == "resize") {
      this.createResizeObserver(element);
    } else if (event_name == "visible") {
      this.createIntersectionObserver(element);
    }

    if (bubbles) {
      if (this.global[event_name] === undefined) {
        this.global[event_name] = { active: 1, callback: this.handler };
        this.root.addEventListener(event_name, this.handler);
      } else {
        this.global[event_name].active++;
      }
    } else {
      const id = element.getAttribute("data-dioxus-id");
      if (!this.local[id]) {
        this.local[id] = {};
      }
      element.addEventListener(event_name, this.handler);
    }
  }
  • 冒泡事件:每种事件名在根元素上只 addEventListener 一次,global[event_name].active 记有多少个元素在听;removeBubblingListener 减到 0 才从根上摘掉。
  • 不冒泡事件:只能挂在元素自己身上。local[id] 只是建了一个空对象,监听器并没有存进去,它实际上只是「这个 id 有过不冒泡监听器」的记号。
  • 两种情况挂的都是同一个 this.handler,也就是 WebsysDom::new 里造的那个 wasm 闭包。
  • resize 和 visible 不是浏览器原生会在元素上派发的事件,createListener 先给元素建一个 ResizeObserver / IntersectionObserver,观察到变化时由 JS 在元素上派发一个不冒泡的 CustomEvent,再走普通的不冒泡路径。

类定义上的两行注释和用法是反的:global 上面写的是「non bubbling events listen at the element the listener was created at」,local 上面写的是「bubbling events can listen at the root element」(ts/core.ts)。以代码为准:global 存冒泡事件,local 管不冒泡的。

应用里和委托有关的元素:

    div { id: "outer", onclick: move |_| info!("#outer onclick"),
      button { id: "inner",
        onclick: move |e| {
          info!("#inner onclick -> stop_propagation");
          e.stop_propagation();
        },
        "inner"
      }
      span { id: "plain", "plain" }
    }
    input { id: "focusme", onfocus: move |_| info!("#focusme onfocus") }

页面上一共有 11 个 onclick、2 个 onsubmit、1 个 onmouseover、2 个 onfocus。加载完成后查到的监听器:

--- where the listeners are ---
#main     listeners: click,mouseover,submit  data-dioxus-id=0
#outer    listeners: (none)  data-dioxus-id=8
#inner    listeners: (none)  data-dioxus-id=9
#focusme  listeners: focus  data-dioxus-id=10
#two      listeners: (none)  data-dioxus-id=17
#three    listeners: focus  data-dioxus-id=18
body      listeners: (none)  data-dioxus-id=null
interp.global: {"click":11,"submit":2,"mouseover":1}
interp.local: {"10":{},"18":{}}

根元素 #main 上一共三个监听器,对应三种冒泡事件;11 个 onclick 只体现为 global.click.active = 11。带 onfocus 的两个输入框各自挂着 focus。带监听器的元素都有 data-dioxus-id,值就是它的 ElementId;#main 的 0 是 initialize 写的。

从事件目标到 ElementId

监听器是 WebsysDom::new 里的这个闭包:

        let handler: Closure<dyn Fn(&Event)> = Closure::wrap(Box::new({
            let runtime = runtime.clone();
            move |web_sys_event: &web_sys::Event| {
                let name = web_sys_event.type_();
                let element = walk_event_for_id(web_sys_event);
                let bubbles = web_sys_event.bubbles();

                let Some((element, target)) = element else {
                    return;
                };

                // Some browser features (e.g. datalist autocomplete) dispatch
                // a plain Event with a typed name like "keydown" that isn't
                // actually a KeyboardEvent. Drop events whose JS type doesn't
                // match what the converters will unchecked-cast to.
                if !event_type_matches(name.as_str(), web_sys_event) {
                    return;
                }

                let data = virtual_event_from_websys_event(web_sys_event.clone(), target);

                let event = dioxus_core::Event::new(Rc::new(data) as Rc<dyn Any>, bubbles);
                runtime.handle_event(name.as_str(), event.clone(), element);

                // Prevent the default action if the user set prevent default on the event
                let prevent_default = !event.default_action_enabled();
                if prevent_default {
                    web_sys_event.prevent_default();
                }
            }
        }));

事件名用浏览器给的 type_();冒不冒泡也用浏览器的 bubbles(),而不是 event_bubbles 那张表。表只决定监听器挂在哪,handle_event 里走冒泡还是不冒泡的分支,看的是这次浏览器事件本身。

walk_element_for_id 从 event.target() 起往上走:

fn walk_element_for_id(target: &Node) -> Option<(ElementId, web_sys::Element)> {
    let mut current_target_element = target.dyn_ref::<web_sys::Element>().cloned();

    loop {
        match (
            current_target_element
                .as_ref()
                .and_then(|el| el.get_attribute("data-dioxus-id").map(|f| f.parse())),
            current_target_element,
        ) {
            // This node is an element, and has a dioxus id, so we can stop walking
            (Some(Ok(id)), Some(target)) => return Some((ElementId(id), target)),

            // Walk the tree upwards until we actually find an event target
            (None, target_element) => {
                let parent = match target_element.as_ref() {
                    Some(el) => el.parent_element(),
                    // if this is the first node and not an element, we need to get the parent from the target node
                    None => target.parent_element(),
                };
                match parent {
                    Some(parent) => current_target_element = Some(parent),
                    _ => return None,
                }
            }

            // This node is an element with an invalid dioxus id, give up
            _ => return None,
        }
    }
}
  • 目标不是元素(比如文本节点)时,先换成它的父元素。
  • 遇到第一个带 data-dioxus-id 的元素就停。只有执行过 NewEventListener 的元素(以及根)才有这个属性,所以停下来的地方是「离目标最近的、挂过监听器的元素」。它挂的可能是别的事件:点 #plain(没有监听器的 span)会停在 #outer;如果 #outer 只有 onmouseover 而没有 onclick,handle_event 从 #outer 出发按路径冒泡,照样能找到它外层元素上的 onclick,见 事件。
  • 一路走到根都没有别的 id 时停在根,得到 ElementId(0);core 里 0 号没有登记任何监听器,handle_event 直接返回。
  • 返回的 target 是停下来的那个元素,不是浏览器的 event.target。表单事件的 value() 读的就是它。

dioxus_core::Event::new(.., bubbles) 之后的部分就是 core 的事情了:handle_event 同步调完所有监听器才返回,回调里写 signal 只是发消息,重渲染要等 WebsysDom 与 interpreter 里的主循环下一次 poll。

包装,而不是序列化

virtual_event_from_websys_event 只把浏览器事件和上面找到的元素装进一个盒子:

struct GenericWebSysEvent {
    raw: Event,
    element: Element,
}

// todo: some of these events are being casted to the wrong event type.
// We need tests that simulate clicks/etc and make sure every event type works.
pub(crate) fn virtual_event_from_websys_event(
    event: web_sys::Event,
    target: Element,
) -> PlatformEventData {
    PlatformEventData::new(Box::new(GenericWebSysEvent {
        raw: event,
        element: target,
    }))
}

PlatformEventData 是 dioxus-html 里的一个 Box<dyn Any>。到这一步,一个字段都没有读。

转换发生在用户闭包被调用之前。onclick 这类函数由 impl_event! 生成,它登记的外层监听器收 Event<PlatformEventData>,用 e.map(|data| data.into()) 换成 Event<MouseData> 再交给用户。into 用的是 generated.rs 里给每种数据类型生成的 From<&PlatformEventData>:

        $(
            impl From<&PlatformEventData> for $data {
                fn from(val: &PlatformEventData) -> Self {
                    with_event_converter(|c| c.$converter(val))
                }
            }
        )*

with_event_converter 读一个全局的 RwLock<Option<Box<dyn HtmlEventConverter>>>,web 上装的是 WebsysDom::new 里 set_event_converter 进去的 WebEventConverter。它的方法由 with_web_event_converters! 这张表展开,一行一种数据:

            convert_form_data(FormData) => web_sys::Event => |event| {
                let event = downcast_event(event);
                FormData::new(WebFormData::new(event.element.clone(), event.raw.clone()))
            };
            // ...
            convert_mouse_data(MouseData) => web_sys::MouseEvent;

没写转换体的行展开成默认实现:Synthetic::new(downcast_event(event).raw.clone().unchecked_into::<$web_ty>()).into()。Synthetic<T> 只包着一个 web_sys 事件,web/src/events/mouse.rs 等文件给 Synthetic<MouseEvent> 实现 dioxus-html 的 InteractionLocation、ModifiersInteraction、HasMouseData 这些 trait(impl InteractionLocation for Synthetic<MouseEvent>):用户调 e.client_coordinates() 时才去调 MouseEvent::client_x()。MouseData 自己就是 Box<dyn HasMouseData>,里面放什么由转换器决定:web 上是 Synthetic<MouseEvent>,事件 一页的原生实验用 SerializedHtmlEventConverter,放的是 SerializedMouseData。用户闭包只看到 MouseData。

FormData 要的东西更多,所以带着元素:WebFormData 的 value() 按元素类型读 HtmlInputElement::value()(checkbox 读 checked())、HtmlTextAreaElement、HtmlSelectElement,都不是就读 text_content();values() 在元素是 <form> 时用 web_sys::FormData::new_with_form 把整张表单读出来。下面的表单实验里 e.values() 打出来的 [("q", Text("two"))] 就是这么来的。

默认实现里的 unchecked_into 不检查类型,web_sys::Event 其实不是 KeyboardEvent 时,后面调方法会在 JS 里出错。所以闭包在包装之前先过 event_type_matches:按事件名找到它属于哪一组(keydown 属于 Keyboard),用 is_instance_of::<web_sys::KeyboardEvent>() 检查,不符合就打一条 Ignoring "{name}": not the expected type 的警告并丢掉。

📝NOTE
冷知识:类型检查是为浏览器自动填充加的

issue #4079 报的是:在 Android 版 Chrome 上选一条自动填充建议之后,「website stops responding」。原因是这类功能派发的是名字叫 keydown、类型却是普通 Event 的事件,转换器照样 unchecked_into::<KeyboardEvent>() 当键盘事件用,报出 issue 标题里的「Cannot read properties of undefined (reading 'length')」;issue #5375(「BorrowMutError with 'onkeydown' listener」)是同一件事的另一种症状。PR #5379(「Fix autofill event downcast - ignore events of the wrong type」)的说明是「this pr adds checks for event types and ignores events we don't recognize instead of panicing」,并且「introduces a unified source for events and event types」:事件名和数据类型的对应从此只写在 dioxus-html 的 with_html_event_groups! 一处,event_type_matches 和 HtmlEventConverter 都由它展开。首次随 0.7.6 发布。「整个页面不再响应」这个症状本页最后一节还会再遇到。

prevent_default:只认同步部分

handle_event 返回后,闭包读 event.default_action_enabled(),用户调过 prevent_default 就对浏览器事件调 preventDefault()。浏览器在派发事件的那次 JS 调用返回之后才决定执行不执行默认行为,所以只有在 handle_event 返回之前调的 prevent_default 有用。core 对 async 回调的处理是先当场 poll 一次(事件 一页的 SpawnIfAsync),第一个 .await 之前的代码在这次派发里同步执行,之后的部分变成任务,在别的微任务里跑。

在真实浏览器里看。两个链接,href 都是页内锚点,区别只是 prevent_default 在 .await 之前还是之后:

    a { id: "pa", href: "#a",
      onclick: move |e: MouseEvent| async move {
        e.prevent_default();
        YieldOnce(false).await;
        info!("#pa prevent_default before await");
      },
      "A"
    }
    a { id: "pb", href: "#b",
      onclick: move |e: MouseEvent| async move {
        YieldOnce(false).await;
        e.prevent_default();
        info!("#pb prevent_default after await");
      },
      "B"
    }

YieldOnce 第一次 poll 时唤醒自己并返回 Pending,第二次返回 Ready。脚本用 dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })) 派发点击,dispatchEvent 的返回值就是「默认行为有没有被取消」的反面:

--- prevent_default before / after await (links) ---
#pa: dispatchEvent returned false, defaultPrevented=true, hash right after: ""
[console.log] INFO src/main.rs:66 #pa prevent_default before await
#pa: location.hash after 200ms: ""
#pb: dispatchEvent returned true, defaultPrevented=false, hash right after: "#b"
[navigatedWithinDocument] /#b
[console.log] INFO src/main.rs:74 #pb prevent_default after await
#pb: location.hash after 200ms: "#b"

#pa 的默认行为被取消,地址没变。#pb 的 prevent_default 确实执行了(日志在),但那时 dispatchEvent 早已返回、浏览器已经跳到了 #b。

表单也一样。#f1 的 onsubmit 什么都不调,#f2 同步调 prevent_default:

    form { id: "f1", onsubmit: move |_| info!("#f1 onsubmit (no prevent_default)"),
      input { name: "q", value: "one" }
      button { id: "s1", r#type: "submit", "submit 1" }
    }
    form { id: "f2",
      onsubmit: move |e| {
        e.prevent_default();
        info!("#f2 onsubmit prevent_default, values = {:?}", e.values());
      },
      input { name: "q", value: "two" }
      button { id: "s2", r#type: "submit", "submit 2" }
    }

脚本先在 window 上放一个标记,点提交按钮,再看标记还在不在(整页重新加载后标记就没了):

--- form submit without / with prevent_default ---
[console.log] INFO src/main.rs:85 #f2 onsubmit prevent_default, values = [("q", Text("two"))]
#f2: marker = "still the same document" url = /
[console.log] INFO src/main.rs:78 #f1 onsubmit (no prevent_default)
[navigated] /?q=one
[navigatedWithinDocument] /?q=one
#f1: marker = undefined url = /?q=one

0.7 里,写了 onsubmit 并不会阻止提交:#f1 的处理函数跑完,浏览器照常按 GET 提交,整页导航到 /?q=one,wasm 应用随页面一起重新加载。要留在单页里,就得像 #f2 那样同步调 prevent_default。

📝NOTE
冷知识:0.6 的 web 上,onsubmit 的 prevent_default 意思是反的

0.6.x 的 web/src/dom.rs 在 handle_event 之后多一段:

                // Prevent forms from submitting and redirecting
                if name == "submit" {
                    // On forms the default behavior is not to submit, if prevent default is set then we submit the form
                    if !prevent_default {
                        web_sys_event.prevent_default();
                    }
                } else if prevent_default {
                    web_sys_event.prevent_default();
                }

只要元素上有 onsubmit,默认就不提交;调了 prevent_default 反而提交。issue #4417(「Form navigate in webview bricks page (0.7 alpha 2)」)报的就是 webview 里一提交表单整个页面变白。PR #4422(「Prevent desktop reloads at the wry level」)的说明:「We currently prevent form submissions if you have a onsubmit handler in all renderers and don't prevent them if you call prevent_default. This behavior can be confusing. This PR removes that behavior and just prevents the webview from reloading instead」。desktop 改在 wry 层拦住重新加载,web 删掉了这段特殊处理,submit 和别的事件一样只看 prevent_default。仓库里第一个包含它的 tag 是 v0.7.0-alpha.3。从 0.6 升上来的表单如果没调 prevent_default,升级后会变成整页提交。

stop_propagation 和浏览器自己的传播

Event::stop_propagation 只改 core 里那份 EventMetadata.propagates,挡住的是 handle_event 里后面的 dioxus 监听器。web 的事件闭包只读 default_action_enabled(),从不调浏览器事件的 stopPropagation()。而 dioxus 的监听器在根上,浏览器事件到达根的时候,从目标到根这一段的传播已经走完了;根以上(body、document、window)还会继续冒泡。

脚本先在 document 上加一个原生监听器,再点 #inner(它的 onclick 调 stop_propagation),然后点没有监听器的 #plain:

--- stop_propagation vs a native listener on document ---
[console.log] INFO src/main.rs:54 #inner onclick -> stop_propagation
[console.log] native document listener: click on #inner, eventPhase=3
(click on the span without listener)
[console.log] INFO src/main.rs:51 #outer onclick
[console.log] native document listener: click on #plain, eventPhase=3

#inner 调了 stop_propagation,#outer 的 dioxus 监听器没有被调用,但 document 上的原生监听器照样收到了(eventPhase=3 是冒泡阶段)。点 #plain 时 walk_element_for_id 停在 #outer,#outer 的 onclick 被调用。

反过来,根以下的原生监听器调 stopPropagation(),事件就到不了根,dioxus 整个看不见它。这正是委托的代价:dioxus 的「冒泡」是在 handle_event 里按模板模拟的,浏览器的冒泡只用来把事件送到根。

❗IMPORTANT
动脑筋:根以下有人 stopPropagation,dioxus 会打印什么?

接着上面的页面,在 #outer 上加一个原生监听器,目标是 #inner 时调 e.stopPropagation(),然后再点一次 #inner:

document.querySelector('#outer').addEventListener('click', (e) => {
  if (e.target.id === 'inner') {
    console.log('native #outer listener: stopPropagation');
    e.stopPropagation();
  }
});
document.querySelector('#inner').click();

控制台里会有几行?#inner onclick 那一行还在不在?document 上的原生监听器呢?(M2 Max、macOS 27、Chrome 156,debug 构建。)

移除一个监听器

监听器也会被单独移除。diff 比较同一元素上的动态属性列表时,旧列表里有、新列表里没有的监听器走 remove_attribute,发 RemoveEventListener。最直接的写法是把监听器放进一个按条件变化的 Vec<Attribute>,用 .. 展开到元素上;原生实验:

fn app() -> Element {
  let extra: Vec<Attribute> = if EXTRA.load(Ordering::SeqCst) {
    vec![dioxus::html::events::onmouseover(|_| {})]
  } else {
    vec![]
  };
  rsx! {
    div { id: "two", onclick: |_| {}, ..extra, "two" }
  }
}
-- rebuild
LoadTemplate { index: 0, id: ElementId(1) }
NewEventListener { name: "click", id: ElementId(1) }
NewEventListener { name: "mouseover", id: ElementId(1) }
AppendChildren { id: ElementId(0), m: 1 }
-- after dropping onmouseover
RemoveEventListener { name: "mouseover", id: ElementId(1) }

同一个元素上的两个监听器共用 ElementId(1),去掉 onmouseover 只发一条 RemoveEventListener,onclick 还在。web 上执行的是 remove_event_listener:

    fn remove_event_listener(event_name: &str<u8, evt>, id: u32, bubbles: u8) {
        "{let node = this.nodes[$id$]; node.listening -= 1; node.removeAttribute('data-dioxus-id'); this.removeListener(node, $event_name$, $bubbles$);}"
    }

它不管 listening 减完是不是 0,都把 data-dioxus-id 删掉,然后才调 removeListener。这带来两个后果,在浏览器里都能复现。应用里的两个元素:

  let extra_bubbling: Vec<Attribute> = if two_extra() {
    vec![dioxus::html::events::onmouseover(move |_| info!("#two onmouseover"))]
  } else {
    vec![]
  };
  let extra_non_bubbling: Vec<Attribute> = if three_extra() {
    vec![dioxus::html::events::onfocus(move |_| info!("#three onfocus"))]
  } else {
    vec![]
  };
  // ...
    button { id: "toggle2", onclick: move |_| two_extra.toggle(), "toggle #two" }
    button { id: "toggle3", onclick: move |_| three_extra.toggle(), "toggle #three" }
    div { id: "two", onclick: move |_| info!("#two onclick"), ..extra_bubbling, "two" }
    input { id: "three", onclick: move |_| info!("#three onclick"), ..extra_non_bubbling }
--- removing one of two listeners on the same element (bubbling: mouseover) ---
[console.log] INFO src/main.rs:92 #two onclick
[console.log] INFO src/main.rs:31 #two onmouseover
before: #two data-dioxus-id = 17  listening = 2  global.mouseover = 1
after toggle2: #two data-dioxus-id = null  listening = 1  global.mouseover = undefined
(clicked #two after toggle2)
app still renders? #counter = count = 1
--- removing a non-bubbling listener (focus) ---
[console.log] INFO src/main.rs:93 #three onclick
before: #three data-dioxus-id = 18  listeners: focus  local = {"10":{},"18":{}}
[exception] TypeError: Cannot convert undefined or null to object
after toggle3: #three data-dioxus-id = null  listeners: focus
[console.log] INFO src/main.rs:51 #outer onclick
(clicked #outer: its handler still runs synchronously)
after two clicks on #inc: #counter = count = 0
  • 冒泡事件:去掉 onmouseover 之后,#two 的 listening 是 1,onclick 在 core 里也还登记着,但 data-dioxus-id 没了。再点 #two,walk_element_for_id 越过它一直走到根,handle_event 拿到 ElementId(0) 什么也不做,#two onclick 不再出现。应用的其余部分正常(#inc 还能加一)。
  • 不冒泡事件:removeListener 走到 removeNonBubblingListener,它第一行就 element.getAttribute("data-dioxus-id"),而属性刚被删掉,拿到 null,delete this.local[id][event_name] 对 undefined 取属性,抛 TypeError。异常发生在 run() 里,穿过 wasm 的 flush_edits 和主循环的 poll 一直抛出去:这批指令后面的部分没执行,focus 监听器也没摘掉。之后同步的事件回调还能跑(#outer onclick 照样打印),但点两次 #inc 计数仍是 0,主循环再也没有渲染过。debug 构建的控制台里,在这条异常之前还有两条 wasm-bindgen 的 imported JS function that was not marked as \catch` threw an error`(实验脚本只保留了异常的第一行)。

两个问题在 0.7.10 里都存在,可以报给上游。0.8 分支的大重写(PR #5554,v0.8.0-alpha.2 起)里,对应的 removeTopEventListener 先 removeListener,listening 减到 0 才删 data-dioxus-id。在 0.7 上,监听器最好写成固定存在、在闭包里判断条件,而不是让它出现又消失。

动脑筋答案

--- a native stopPropagation below the root ---
[console.log] native #outer listener: stopPropagation

只有一行,是原生监听器自己打的。事件在 #outer 上被 stopPropagation(),没有冒泡到 #main,根上那个 wasm 闭包根本没被调用,所以 #inner onclick 和 #outer onclick 都没有;document 在 #main 之上,也收不到。

小结

  • 冒泡事件在根元素上每种名字只 addEventListener 一次(global[name].active 计数),不冒泡的挂在元素自己身上;挂的都是同一个 wasm 闭包。global / local 两个字段的注释和用法是反的。
  • 闭包从 event.target 往上找第一个带 data-dioxus-id 的元素,这个属性只在执行 NewEventListener 时写上;冒不冒泡用浏览器事件自己的 bubbles()。
  • 浏览器事件不序列化:装进 PlatformEventData,用户闭包要 MouseData 时由 WebEventConverter 包成 Synthetic<MouseEvent>,字段按需读。类型不符的事件被 event_type_matches 丢掉(#5379,0.7.6)。
  • prevent_default 只在 handle_event 返回之前调用才有效,链接和表单在真实浏览器里都验证了;0.7 起 onsubmit 不再默认阻止提交(#4422)。
  • stop_propagation 只挡 dioxus 的监听器,根以上的原生监听器照样收到;根以下的原生 stopPropagation 会让 dioxus 完全收不到事件。
  • 0.7.10 的 RemoveEventListener 先删 data-dioxus-id 再摘监听器:同一元素上剩下的监听器失效;摘的是不冒泡事件时 JS 抛异常,主循环停止渲染。

💬 关联讨论

发起讨论
加载中...