为何触发CustomEvent后再注册监听器仍能捕获该事件?
这个问题其实挺有意思的——首先得明确一个关键点:你贴的这段代码在同步执行的情况下,是绝对不会触发后面的监听器的。如果你的实际运行环境里它真的生效了,大概率是代码的执行顺序被悄悄改变了,或者有其他隐藏因素在起作用。
先搞懂同步代码里的事件逻辑
浏览器的dispatchEvent在同步代码中是立即执行的:
- 当你调用
window.dispatchEvent(event)时,浏览器会立刻遍历当前已经注册在window上的customEvent监听器,逐个执行回调。 - 而你的监听器是在
dispatchEvent之后才注册的,这时候事件已经“完成触发”,相当于你错过了这趟车,自然不会被捕获到。
那为什么你看到它生效了?可能的原因:
异步执行悄悄改变了顺序:如果你的
dispatchEvent被包裹在了异步任务里(比如setTimeout、Promise.then或者requestAnimationFrame),事件循环的机制会让同步代码先执行完。比如:let event = new CustomEvent("customEvent", { detail: { book: "FullStack Vue " } }); // 放到宏任务队列,延后执行 setTimeout(() => window.dispatchEvent(event), 0); // 同步注册监听器,先于宏任务执行 window.addEventListener("customEvent", e => console.log("This book is " + e.detail.book) );这种情况下,监听器会先完成注册,之后异步队列里的事件才会触发,自然就能被捕获到。你可能不小心把
dispatchEvent放到了异步上下文里却没注意到?页面中已有同名监听器:如果你的页面其他地方已经注册了
customEvent的监听器,这次dispatchEvent触发的是那个已存在的监听器,让你误以为是后面注册的这个生效了。重复执行代码的误判:比如你在浏览器控制台里分两次执行代码——第一次执行前两行(创建+触发事件),没输出;第二次执行第三行(注册监听器),这时候再触发事件才会生效?或者不小心重复执行了整个代码块,第二次执行时监听器已经存在了?
关于你提到的“竞态条件”
竞态条件的核心是执行顺序不确定。只有当事件触发和监听器注册都是异步任务时,才会出现谁先谁后的不确定性:
// 宏任务1:触发事件 setTimeout(() => window.dispatchEvent(event), 0); // 宏任务2:注册监听器 setTimeout(() => window.addEventListener("customEvent", callback), 0);
这种情况下,两个宏任务的执行顺序取决于浏览器调度,才会出现监听器没注册就触发事件的风险。但你的同步代码里顺序是完全确定的,所以不属于竞态条件。
正确的写法
如果想要监听器稳定捕获到事件,一定要先注册监听器,再触发事件:
// 先注册监听器 window.addEventListener("customEvent", e => console.log("This book is " + e.detail.book) ); // 再触发事件 let event = new CustomEvent("customEvent", { detail: { book: "FullStack Vue " } }); window.dispatchEvent(event);
内容的提问来源于stack exchange,提问作者codepleb

