为什么在点击事件监听器中触发click()不会导致无限循环?

83

能否有人解释一下这段 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 id="leave-button">Leave Room</button>

The Output was:
1
check
2
check
3
4

这个问题可能听起来很蠢,但我是JavaScript的新手。我无法理解这段代码的程序流程。我想知道我怎么得到3和4的输出。


2
你正在处理一个递归类型的函数(一个调用自己的函数)。虽然不完全是这样,但有点类似。当点击1并检查时输出结果。然后程序调用click函数,然后输出2和check。然后第一个click函数的第二部分输出3,最后内部调用click函数的第二部分输出4。 - DCR
1
@DCR "然后第一次点击的第二部分输出" - 但为什么它不会在那里触发新的点击?为什么会退出? - T J
3
这段代码为什么不是传统的递归? - PM 77-1
3
我不确定 OP 在问什么。这段代码最有趣的部分似乎是“为什么在元素自己的事件处理程序中调用 el.click() 不会导致无限触发事件链?”投票下降可能是因为不清楚 OP 在这里感到惊讶的是什么,所以我假设是点击调用而不是递增或延迟的最终 console.log,这可以在没有事件的情况下同样说明。 - ggorlen
3
典型递归会有一个结束情况,并在每次迭代中通过某种方法减少问题的规模,直到达到结束情况。 - DCR
显示剩余6条评论
3个回答

54
这个问题的关键在于每个element.click()方法都有一个隐藏标志

每个元素都有一个相关联的点击进度标志,最初未设置。

文档链接: https://html.spec.whatwg.org/multipage/interaction.html#dom-click

一旦激活此方法,该标志从progess Status == unset更改为progess Status == active (伪代码)

(然后一旦其中的代码完全执行完毕,它就会返回到初始状态)

当此标志处于active状态时,任何对此方法的调用都将被忽略。


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}`)
  })
<button id="leave-button">Leave Room</button>

隐藏标志的作用类似于我这样编码:
将此行替换为:
if (source !== 'call') bt_leaveRoom.click()
而不是:
bt_leaveRoom.click()
但实际上,系统使用了隐藏标志的方法(称为progress flag?)
可以是(伪代码)
if (progress_flag_of_bt_leaveRoom.click() is unset) do { bt_leaveRoom.click() }  

3
这并没有回答问题。为什么在函数内部调用$leaveRoom.click()不会创建无限循环?基本上,似乎第二个console.log()语句永远不应该被执行。 - DCR
3
明白了,进度中的点击标志是关键。在你的答案中让这更加突出怎么样?比如在开头清晰地引用规范中的语句"If this element's click in progress flag is set, then return.",并提醒调用HTMLElement.click()会同步调用事件处理程序?甚至为什么不把其余内容删除掉,因为它们并没有多大帮助? - Kaiido
如果可以的话,你应该真正地执行以下三个步骤:1.将“click in progress”部分移到开头,这是问题的关键,不要将其隐藏在无关的内容下面。2.更改引用。我们不关心最初是否设置了一个标志为false,重要的是当调用click()时,此标志被用于提前退出,因此引用那部分说的是这点。3.解释由click()触发的事件(例如dispatchEvent)是同步触发的,因此标志在期间保持升高,这也解释了日志的顺序,如果您想将其保留为页脚,则需要说明。 - Kaiido
@Mister Jojo 嗯,兄弟,我已经做了。 - Tushar Agarwal
3
@TusharAgarwal 的 setTimeout 在一个单独的进程中运行,即使所选延迟为零,此调用也要足够晚以完成所有指令,使得 Flag 能被返回到其 unset state。在他的示例中,setTimeout 是最后一条指令,足以使 Flag 返回到 unset。另一方面,如果后面还有其他指令,则 Flag 不会返回到 unset,该调用将被拒绝,除非您在 setTimeout 中设置足够长的延迟时间。 - Mister Jojo
显示剩余5条评论

14

我尝试了一些方法来找到这个问题的答案。我并没有找到一个明确的答案来解释这里发生了什么,但我相信我分享的内容会有助于解决这个问题。

我简化了代码,以便集中关注事件的递归触发。

简化后的代码

const $leaveRoom = document.querySelector('#leave-button');
let a = 1;
$leaveRoom.addEventListener('click', () => {
    console.log(a++);
    $leaveRoom.click();
});
<button id="leave-button">Leave Room</button>

我们可以在这里看到有两个对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'));
});
<button id="leave-button">Leave Room</button>

这里,事件会被多次触发(对我来说是44次),这可能是由于您的计算机速度较快导致的。不过它似乎最终会停止触发,所以我认为这里也出现了同样的现象。

使用 setTimeout

const $leaveRoom = document.querySelector('#leave-button');
let a = 1;
$leaveRoom.addEventListener('click', () => {
    console.log(a++);
    setTimeout(() => { $leaveRoom.click(); });
});
<button id="leave-button">Leave Room</button>

如果你正在寻找一种无限触发点击事件的方法,而不管之前的方法失败原因是什么。这个方法似乎可以解决问题。

说了这么多,我仍然不清楚为什么之前的方法的递归会被隐藏的力量停止。也许有人可以给予一些启示。


4
那个数字44次真的很可疑,我想知道是什么引起了这个限制。 - Bergi
3
可以,但翻译会依赖于使用的引擎。我猜这是一些内部队列或堆栈大小限制。 - Bergi
5
正如@Bergi所说,这肯定是与引擎相关的。在Chrome和Edge中,我得到了44次计数,然后在控制台上记录了一个错误“堆栈溢出”。在FireFox中,它进行了383次计数,然后在控制台上记录了一个错误“未定义”。 - Dustin Hodges

2

const click1 = document.querySelector('#click1')
const click2 = document.querySelector('#click2')
const click3 = document.querySelector('#click3')

click1.addEventListener('click', (event) => {
  console.log("click1")
  click2.click()
});
click2.addEventListener('click', (event) => {
  console.log("click2")
  click3.click()
});
click3.addEventListener('click', (event) => {
  console.log("click3")
  click1.click()
});
<button id="click1">Click 1</button>
<button id="click2">Click 2</button>
<button id="click3">Click 3</button>

实际上,在事件处理中存在循环中断逻辑。处理程序将愉快地链接用户输入事件,直到初始交互分派重复事件。然后它将不再分派任何事件。


然而,Chrome对来自脚本的用户输入事件施加了限制。有来源吗?此外,这种行为在各种浏览器中似乎是一致的。 - Kaiido

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接