当以编程方式单击按钮与单击DOM时,为什么任务/微任务执行顺序有所不同?

peo*_*les 13 javascript

在DOM中单击按钮时,与以编程方式单击时,微任务/任务队列的执行顺序有所不同。

const btn = document.querySelector('#btn');

btn.addEventListener("click", function() {
  Promise.resolve().then(function() { console.log('resolved-1'); });
  console.log('click-1');
});

btn.addEventListener("click", function() {
  Promise.resolve().then(function() { console.log('resolved-2'); });
  console.log('click-2');
});
Run Code Online (Sandbox Code Playgroud)
<button id='btn'>Click me !</button>
Run Code Online (Sandbox Code Playgroud)

我的理解是,当调用栈为空时,事件循环将从微任务队列中获取回调以放置在调用栈中。当调用堆栈和微任务队列均为空时,事件循环开始从任务队列中进行回调。

单击带有id btn的按钮时,两个“ click”事件侦听器都将按声明它们的顺序放入任务队列中。

// representing the callstack and task queues as arrays
callstack: []
microtask queue: []
task queue: ["click-1", "click-2"]
Run Code Online (Sandbox Code Playgroud)

事件循环将其"click-1" callback放置在调用堆栈上。它有一个立即解决的承诺,将其"resolved-1" callback放置在微任务队列上。

callstack: ["click-1"]
microtask queue: ["resolved-1"]
task queue: ["click-2"]
Run Code Online (Sandbox Code Playgroud)

"click-1" callback执行它的console.log,并完成。现在,微任务队列上有内容,因此事件循环接受"resolved-1" callback并将其放置在调用堆栈中。

callstack: ["resolved-1"]
microtask queue: []
task queue: ["click-2"]
Run Code Online (Sandbox Code Playgroud)

"resolved-1" callback被执行。现在,无论是调用堆栈还是微任务队列,都为空。

callstack: []
microtask queue: []
task queue: ["click-2"]
Run Code Online (Sandbox Code Playgroud)

然后,事件循环再次“看”任务队列,并重复该循环。

// "click-2" is placed on the callstack
callstack: ["click-2"]
microtask queue: []
task queue: []

// Immediately resolved promise puts "resolved-2" in the microtask queue
callstack: ["click-2"]
microtask queue: ["resolved-2"]
task queue: []

// "click-2" completes ...
callstack: []
microtask queue: ["resolved-2"]
task queue: []

// "resolved-2" executes ...
callstack: ["resolved-2"]
microtask queue: []
task queue: []

// and completes
callstack: []
microtask queue: []
task queue: []

Run Code Online (Sandbox Code Playgroud)

这将解释上面代码片段的输出

"hello click1"
"resolved click1"
"hello click2"
"resolved click2"
Run Code Online (Sandbox Code Playgroud)

我希望它是相同的,然后以编程方式单击btn.click()

// representing the callstack and task queues as arrays
callstack: []
microtask queue: []
task queue: ["click-1", "click-2"]
Run Code Online (Sandbox Code Playgroud)
callstack: ["click-1"]
microtask queue: ["resolved-1"]
task queue: ["click-2"]
Run Code Online (Sandbox Code Playgroud)

但是,输出是不同的。

"hello click1"
"hello click2"
"resolved click1"
"resolved click2"
Run Code Online (Sandbox Code Playgroud)

为什么以编程方式单击按钮时执行顺序有所不同?

T.J*_*der 10

有趣的问题。

首先,简单一点:调用时click,这是一个同步调用,触发按钮上的所有事件处理程序。您可以看到,如果在呼叫周围添加日志记录:

const btn = document.querySelector('#btn');

btn.addEventListener("click", function() {
  Promise.resolve().then(function() { console.log('resolved-1'); });
  console.log('click-1');
});

btn.addEventListener("click", function() {
  Promise.resolve().then(function() { console.log('resolved-2'); });
  console.log('click-2');
});


document.getElementById("btn-simulate").addEventListener("click", function() {
  console.log("About to call click");
  btn.click();
  console.log("Done calling click");
});
Run Code Online (Sandbox Code Playgroud)
<input type="button" id="btn" value="Direct Click">
<input type="button" id="btn-simulate" value="Call click()">
Run Code Online (Sandbox Code Playgroud)

由于处理程序是同步运行的,因此仅在两个处理程序都完成后才处理微任务。更快地处理它们将需要打破JavaScript的运行到完成语义。

相反,当事件通过DOM分派时,会更有趣:每个处理程序都会被调用。调用处理程序包括在运行脚本后进行清理,这包括执行微任务检查点,运行任何暂挂的微任务。因此,由调用的处理程序调度的微任务将在下一个处理程序运行之前运行。

这就是“为什么”它们在某种意义上不同的原因:因为处理程序回调是在使用时按顺序同步调用的click(),所以没有机会在它们之间处理微任务。

稍微看一下“为什么”:为什么在使用时同步调用处理程序click()?主要是因为历史,这是早期浏览器所做的,因此无法更改。但是,如果您使用它们,它们也是同步的dispatchEvent

const e = new MouseEvent("click");
btn.dispatchEvent(e);
Run Code Online (Sandbox Code Playgroud)

在那种情况下,处理程序仍将同步运行,因为使用该处理程序的代码可能需要查看e以查看是否阻止了默认操作或类似的操作。(它可能已经被不同的定义,提供了一个回调或一些这样的时候做的事件被分派,但事实并非如此。我,这是不是要么简便,兼容性click,或两者兼而有之。)

  • 实际上,“ click”和“ dispatchEvent”之间存在细微的差异(在Testim上玩游戏时,我们经常遇到这些差异,并且必须进行标准化)。我将在今天晚些时候查看是否可以挖掘出铬代码以进行讨论。另外-请注意`.click`是特殊的,而“为什么这样工作的答案”是“旧版,并且因为受信任的事件并不总是一件事情”-`.click`在DOM中非常特殊(不是提及DOMActivate的乐趣)。 (2认同)

Ben*_*aum 5

所以,Chrome 回答只是因为它很有趣(请参阅 TJ Crowder 对一般 DOM 回答的出色回答)。

btn.click();
Run Code Online (Sandbox Code Playgroud)

HTMLElement::click()在 C++ 中调用,它是 DOMElement 的对应物:

btn.click();
Run Code Online (Sandbox Code Playgroud)

它基本上围绕 dispatchMouseEvent 做了一些工作并处理边缘情况:

void HTMLElement::click() {
  DispatchSimulatedClick(nullptr, kSendNoEvents,
                         SimulatedClickCreationScope::kFromScript);
}
Run Code Online (Sandbox Code Playgroud)

它在设计上是完全同步的,以使测试变得简单以及出于遗留原因(想想 DOMActivate 奇怪的事情)。

这只是一个直接调用,不涉及任务调度。EventTarget 通常是一个同步接口,它不会推迟事物,它早于 microtick 语义和承诺:]