prevent_default 没拦住跳转或提交
主线位置:更新主线的第一步,浏览器事件进入根上的委托监听器、
Runtime::handle_event返回的那一刻。web 的事件闭包在handle_event返回后读一次「用户有没有调prevent_default」,读完就交还给浏览器。委托监听器见事件委托,async回调怎么被第一次 poll 见事件。
sequenceDiagram participant Br as 浏览器 participant L as 根上的监听器 participant H as 用户回调 Br->>L: click 事件 L->>H: handle_event,第一次 poll H-->>L: 第一个 await 处返回 Pending L->>Br: 读 default_action_enabled Br->>Br: 执行默认行为 H->>H: await 之后 prevent_default Note over H: 已经晚了
本页实验在 M2 Max、macOS 27、rustc 1.99.0、Chrome 156 上运行,依赖 dioxus = "=0.7.10"(router、fullstack),dx 0.7.9 --release 构建的 fullstack 应用。headless Chrome 经 DevTools Protocol 打开页面,等水合完成后派发事件。
现象
两个页内锚点,区别只在 prevent_default 在 .await 之前还是之后;两个表单,一个的 onsubmit 什么都不调,一个先调 prevent_default 再做异步的事。YieldOnce 第一次 poll 时唤醒自己并返回 Pending,第二次返回 Ready,代表任何一次真正的 .await(请求、定时器):
#[component]
fn Pd() -> Element {
rsx! {
button { id: "inc", onclick: move |_| {}, "+" }
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"
}
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: FormEvent| async move {
e.prevent_default();
YieldOnce(false).await;
info!("#f2 onsubmit prevent_default, values = {:?}", e.values());
},
input { name: "q", value: "two" }
button { id: "s2", r#type: "submit", "submit 2" }
}
}
}
链接用 dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })) 派发,它的返回值就是「默认行为没被取消」;表单是先在 window 上放一个标记,点提交按钮,再看标记还在不在(整页重新加载后标记就没了)。release 构建:
--- links ---
#pa: dispatchEvent returned false, defaultPrevented=true, hash right after: ""
[console.log] INFO src/main.rs:323 #pa prevent_default before await
#pa: location.hash after 200ms: ""
#pb: dispatchEvent returned true, defaultPrevented=false, hash right after: "#b"
[navigatedWithinDocument] /pd#b
[console.log] INFO src/main.rs:331 #pb prevent_default after await
#pb: location.hash after 200ms: "#b"
--- forms ---
[console.log] INFO src/main.rs:343 #f2 onsubmit prevent_default, values = [("q", Text("two"))]
#f2: marker = "still the same document" url = /pd
[console.log] INFO src/main.rs:335 #f1 onsubmit (no prevent_default)
[navigated] /pd?q=one
[navigatedWithinDocument] /pd?q=one
#f1: marker = undefined url = /pd?q=one
#pb的prevent_default确实执行了(日志在),但dispatchEvent返回时默认行为没被取消,地址已经变成#b。换成真实链接,就是整页跳走。#f1写了onsubmit,处理函数也跑了,但浏览器照常按 GET 提交,向服务端请求/pd?q=one,整页重新加载,wasm 应用和它的所有状态从头开始。#pa和#f2在第一个.await之前调prevent_default,都拦住了;#f2的回调在.await之后照样能读表单的值。
出处
web 的事件闭包(dom.rs):
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();
}
prevent_default 只是在 dioxus 的 Event 上记一个标记,真正调浏览器的 preventDefault() 是这几行,而且只有一次机会:Runtime::handle_event 返回之后。浏览器在派发事件的这次 JS 调用返回后就决定执不执行默认行为。async 回调在 handle_event 里被当场 poll 一次,第一个 .await 之前的代码同步执行;之后的部分变成任务,在别的微任务里跑,那时这几行早就执行完了。
表单的情况是 0.7 的改动。0.6 的 web 渲染器里,元素上只要有 onsubmit,默认就不提交,调 prevent_default 反而提交;0.7 删掉了这段特殊处理,submit 和其他事件一样只看 prevent_default。来历见事件委托的冷知识(PR #4422)。从 0.6 升上来的表单如果没调 prevent_default,升级后会变成整页提交。
修法
- 要拦默认行为,就在回调的第一行、任何
.await之前调e.prevent_default(),像#pa和#f2那样。要不要拦得在同步部分决定:如果「拦不拦」取决于一个异步结果(比如先问服务端能不能离开),就无条件先拦下,等结果出来再自己导航(navigator().push(..))或自己提交。 - 单页应用里的表单,
onsubmit一律先prevent_default,数据从e.values()取,交给 server function。 - 站内链接用 router 的
Link,它在同步回调里调prevent_default,见站内跳转整页刷新。
怎么确认没中招
- 搜
onsubmit:,看每一个是不是在第一行就prevent_default。 - 搜
async move的事件回调里的prevent_default,确认它前面没有.await。 - 测试里用
dispatchEvent的返回值判断:返回false表示默认行为被取消。上面的#pb返回true,#pa返回false。也可以像实验那样,在window上放一个标记,提交之后看它还在不在。
小结
prevent_default只在handle_event返回之前有效;async回调里.await之后再调,浏览器已经执行了默认行为,日志照样打印。- 0.7 里写了
onsubmit不等于不提交,不调prevent_default表单就整页提交,应用状态全丢(0.6 的行为相反)。 - 修法:在第一个
.await之前同步调用;需要异步判断时先无条件拦下,之后自己导航或提交。