为什么focusIn处理器中的setTimeout先于click处理器执行?
问题描述
我原本认为不管浏览器内部事件序列如何,DOM事件处理都应该是同步的。但我发现点击(click)处理器存在严重延迟(在我的Macbook Pro M1 Max、Chrome 126.0.6478.63上延迟30-90ms不等),导致setTimeout和requestAnimationFrame中的代码都先于click处理器执行。有人能解释这种行为吗?
最小可复现示例(MVCE)
JavaScript代码
const css = `:host(:focus-within) div { outline: 1px solid blue; display: inline-block; }`; const sheet = new CSSStyleSheet(); sheet.replaceSync(css); customElements.define( "a-b", class extends HTMLElement { input = document.createElement("input"); btn = Object.assign(document.createElement("button"), { textContent: "Click me" }); constructor() { super().attachShadow({ mode: "open" }).adoptedStyleSheets.push(sheet); this.shadowRoot.append(this.input, this.btn); this.addEventListener("focusin", this.handleFocusIn); this.addEventListener("click", this.handleClick); } handleFocusIn() { output.value += `focusin: ${Date.now()}\n`; setTimeout(() => { // 为什么这段代码会在click监听器之前执行? output.value += `timeout: ${Date.now()}\n`; }, 0); requestAnimationFrame(() => { // 为什么这段代码会在click监听器之前执行? output.value += `rAF: ${Date.now()}\n`; }); } handleClick() { output.value += `click: ${Date.now()}\n----------------------\n`; } } );
CSS代码
textarea { height: 12em; margin-top: 2em; width: 16em; }
HTML代码
<a-b></a-b> <br> <textarea id="output"></textarea>
问题解答
这是浏览器的双击检测机制和事件队列执行顺序共同作用的结果:
- 点击按钮时,浏览器先触发
focusin事件(按钮获得焦点),这个事件同步执行,handleFocusIn立刻运行,此时setTimeout(0延迟)被加入宏任务队列,requestAnimationFrame被加入渲染前的回调队列。 - 为了判断这次点击是不是双击的第一次点击,浏览器会故意延迟
click事件的触发(通常在200ms内),这段等待时间里,浏览器会优先处理队列中已有的任务。 requestAnimationFrame的回调会在下一次页面重绘前执行,而setTimeout的回调会在当前宏任务结束后立即执行,两者都会在延迟的click事件触发前完成,所以就出现了timeout和rAF的输出早于click的情况。
如果要避免这个延迟,可以用pointerdown或mousedown事件替代click,这类事件没有双击检测的延迟,但要注意它们的触发时机更早,可能会引发其他交互问题(比如鼠标按下后未松开就移动的场景)。
内容的提问来源于stack exchange,提问作者connexo
相关产品推荐
相关产品推荐

