TypeError: Cannot convert undefined or null to object
主线位置:更新主线的「diff →
WriteMutations→ interpreter 批量改 DOM」。diff 发出一条RemoveEventListener,interpreter 执行它时把同一元素上其他监听器也一起弄坏了。指令从哪来、interpreter 怎么执行,见事件委托的「移除一个监听器」;本页只讲怎么撞上、怎么绕开。
flowchart TB
diff["diff 发 RemoveEventListener"] --> js["remove_event_listener"]
js --> del["先删 data-dioxus-id"]
del --> rm["再 removeListener"]
rm --> kind{"去掉的事件冒泡吗"}
kind -->|冒泡| b1["剩下的 onclick 找不到 id"]
b1 --> b2["静默失效,应用照常"]
kind -->|不冒泡| n1["读 id 得到 null"]
n1 --> n2["抛 TypeError"]
n2 --> n3["主循环停止,不再渲染"]
本页实验在 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 打开页面、派发事件、读属性。
现象
监听器要单独出现和消失,最直接的写法是把它放进一个按条件变化的 Vec<Attribute>,用 .. 展开到元素上。#two 常驻一个 onclick,按条件多一个 onmouseover(冒泡事件);#three 常驻 onclick,按条件多一个 onfocus(不冒泡):
#[component]
fn Rl() -> Element {
let mut count = use_signal(|| 0);
let mut two_extra = use_signal(|| true);
let mut three_extra = use_signal(|| true);
let mut four_extra = use_signal(|| true);
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![]
};
rsx! {
div { id: "counter", "count = {count}" }
button { id: "inc", onclick: move |_| count += 1, "+" }
button { id: "toggle2", onclick: move |_| two_extra.toggle(), "toggle #two" }
button { id: "toggle3", onclick: move |_| three_extra.toggle(), "toggle #three" }
button { id: "toggle4", onclick: move |_| four_extra.toggle(), "toggle #four" }
div { id: "two", onclick: move |_| info!("#two onclick"), ..extra_bubbling, "two" }
input { id: "three", onclick: move |_| info!("#three onclick"), ..extra_non_bubbling }
// ... #four, see below
}
}
脚本先点一下、悬停一下确认两个监听器都在,再点 toggle 去掉一个,然后点那个元素、点 #inc。release 构建:
--- #two: drop onmouseover (bubbling), keep onclick ---
[console.log] INFO src/main.rs:377 #two onclick
[console.log] INFO src/main.rs:361 #two onmouseover
before: #two data-dioxus-id = 7
after toggle2: #two data-dioxus-id = null
(clicked #two)
after clicking #inc: count = 1
--- #three: drop onfocus (non-bubbling), keep onclick ---
[console.log] INFO src/main.rs:378 #three onclick
before: #three data-dioxus-id = 8 listeners: focus
[console.error] wasm-bindgen: imported JS function that was not marked as `catch` threw an error: Cannot convert undefined or null to object
[exception] TypeError: Cannot convert undefined or null to object
after toggle3: #three data-dioxus-id = null listeners: focus
after two clicks on #inc: count = 0
- 冒泡事件:去掉
onmouseover之后,#two的data-dioxus-id没了。再点#two,#two onclick不再打印,也没有任何报错。应用的其余部分正常(#inc能加一)。 - 不冒泡事件:点 toggle3 的那次渲染抛出
TypeError,focus监听器也没摘掉。之后点两次#inc,计数还是 0:主循环再也没有渲染过。
出处
diff 比较同一元素的动态属性列表,旧列表有、新列表没有的监听器发一条 RemoveEventListener(remove_attribute)。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:
- 冒泡事件的监听器都在根上,靠事件目标往上找
data-dioxus-id来定位元素。#two的属性没了,点击时一路找到根,handle_event拿到根的 id 什么也不做,onclick虽然还登记在 core 里,再也收不到事件。 - 不冒泡事件的监听器挂在元素自己身上,
removeListener走到removeNonBubblingListener,它第一行就读data-dioxus-id,拿到null,接着对undefined取属性,抛TypeError。异常发生在 interpreter 的run()里,一直抛穿 wasm 的flush_edits和主循环,这一批指令后面的部分没执行,主循环也停了。
0.8 分支的重写(PR #5554)里,对应的函数先 removeListener,listening 减到 0 才删属性;0.7.10 没有这个修复。
修法
监听器常驻,条件写进闭包里。#four 和 #two 一样有两个监听器,但 onmouseover 从不消失,开关只决定它做不做事:
// fix: both listeners always exist, the condition lives in the closure
div { id: "four",
onclick: move |_| info!("#four onclick"),
onmouseover: move |_| {
if four_extra() {
info!("#four onmouseover");
}
},
"four"
}
--- #four (fix): listeners stay, the condition is inside the closure ---
[console.log] INFO src/main.rs:384 #four onmouseover
after toggle4: #four data-dioxus-id = 9
[console.log] INFO src/main.rs:381 #four onclick
(mouseover + click on #four)
after clicking #inc: count = 1
toggle 之后 #four 的 data-dioxus-id 还在,悬停不再打印(条件为假),点击照常,计数正常。
怎么确认没中招
- 搜
Vec<Attribute>、..attrs这类展开写法,以及dioxus::html::events::on开头的函数调用:只要展开的列表里有监听器、而且其中的监听器会随条件出现或消失,就可能踩到。 - 浏览器里的检查:做完一次会去掉监听器的操作后,在控制台执行
document.querySelectorAll('[data-dioxus-id]').length,和操作之前比,少了的就是被误删的。 - 控制台里出现
Cannot convert undefined or null to object,堆栈里有removeNonBubblingListener,就是这个问题;之后页面不再更新。
小结
- 0.7.10 的
remove_event_listener去掉一个监听器时,先无条件删data-dioxus-id,同一元素上剩下的监听器跟着失效。 - 去掉的是冒泡事件:剩下的监听器静默不触发。去掉的是不冒泡事件(
focus、blur、scroll、mouseenter、mouseleave等,完整的表在event_bubbles):抛TypeError,主循环停止,页面不再渲染。 - 修法:监听器常驻,条件放进闭包。