为什么在单击事件侦听器内触发click()不会导致无限循环?
有人可以解释一下这段 JavaScript 代码的程序流程:
const $leaveRoom = document.querySelector('#leave-button');
let a = 1;
$leaveRoom.addEventListener('click', () => {
console.log(a);
console.log("check");
a++;
$leaveRoom.click();
console.log(a);
a++;
});
<button>Leave Room</button>
<button>Leave Room</button>
<button>Leave Room</button>
<button>Leave Room</button>
<button>Leave Room</button>
回答
这个问题的关键是每个方法上都有一个隐藏的标志
element.click()。每个元素都有一个关联的点击进行中标志,该标志最初未设置。
文档:https : //html.spec.whatwg.org/multipage/interaction.html#dom-click
一旦这个方法被激活,这个标志就会从progess Status == unset到progess Status == active (伪代码)
(然后一旦它包含的代码被完全执行,它就会返回到它的初始状态)
当此标志处于该active状态时,将忽略对该方法的任何调用。
这是我原来的帖子,展示了`console.log()`的执行顺序
const bt_leaveRoom = document.querySelector('#leave-button')
var counter = 0
var origin = 'event clic'
bt_leaveRoom.addEventListener('click', () =>
{
let source = origin
console.log(`_first console.log(): counter = ${ ++counter }, origin = ${source}`)
origin = 'call'
bt_leaveRoom.click()
console.log(`second console.log(): counter = ${ ++counter }, origin = ${source}`)
})
如果我以这种方式编码,则隐藏标志的行为方式相同:
将此行替换
bt_leaveRoom.click()
为:
if (source !== 'call') bt_leaveRoom.click()
但实际上系统使用了隐藏标志(名为progress flag?)的方法
,它可以(在伪代码中)
if (progress_flag_of_bt_leaveRoom.click() is unset) do { bt_leaveRoom.click() }
- This doesn't answer the question. Why doesn't calling $leaveRoom.click() inside the function create an endless loop? Basically, it would seem like the second console.log() statement should never get executed
- You got it, the click in progres flag is the key. What about making this more prominent in your answer? Like at the begining, with a clear quote of the specs "If this element's click in progress flag is set, then return." and with a reminder that calling HTMLElement.click() calls the event handlers synchronously? And then why not even remove the rest because it really doesn't add much?
- @TusharAgarwal the `setTimeout` works in a separate process, and even if the chosen delay is zero, this call is made late enough for all the instructions to be carried out to be completed, and this `Flag` is therefore returned to its `unset state` before. In his example the `setTimeout` is the last instruction, which is enough to make the `Flag` go back to `unset`. On the other hand, if there were other instructions following, then the `Flag` would not be returned to `unset` and the call would have been rejected. unless you put a sufficiently long delay in the `setTimeout`
回答
我尝试了几件事来找到这个问题的答案。对于这里发生的事情,我还没有真正找到明确的答案,但我相信我在这里分享的内容可以解决这个极好的问题。
我简化了代码以专注于事件的递归触发。
简化代码
const $leaveRoom = document.querySelector('#leave-button');
let a = 1;
$leaveRoom.addEventListener('click', () => {
console.log(a++);
$leaveRoom.click();
});
我们在这里可以看到,我们有两次调用console.log,第一个是实际点击按钮,第二个是调用$leaveRoom.click();。它似乎出于某种原因停在那里。
使用 dispatchEvent
const $leaveRoom = document.querySelector('#leave-button');
let a = 1;
$leaveRoom.addEventListener('click', () => {
console.log(a++);
$leaveRoom.dispatchEvent(new Event('click'));
});
在这里,该事件被多次触发(对我来说是 44 次),这可能是由于您的机器有多快。不过它似乎最终会停止触发,所以我认为这里也会发生同样的现象。
使用 setTimeout
const $leaveRoom = document.querySelector('#leave-button');
let a = 1;
$leaveRoom.addEventListener('click', () => {
console.log(a++);
setTimeout(() => { $leaveRoom.click(); });
});
如果你正在寻找一种无限触发点击事件的方法,不管前面的方法为什么会失败。这似乎确实可以解决问题。
说了这么多,我仍然不知道这个隐藏的力量会阻止之前方法的递归。也许有人可以对此有所了解。
- As you said @Bergi, it is definitely engine dependent. In Chrome and Edge, I get the 44 count and then an error is logged to the console "Maximum call stack size exceeded." In FireFox, it goes 383 times and then logs an error "undefined" in the console.
- That 44 times is really suspicious. I wonder what causes this limit.
- Yes, would be engine-dependent though. I guess it's some internal queue or stack size limit.