首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >为什么在以编程方式单击按钮与单击DOM时任务/微任务执行顺序有差异?

为什么在以编程方式单击按钮与单击DOM时任务/微任务执行顺序有差异?
EN

Stack Overflow用户
提问于 2019-04-16 13:35:54
回答 3查看 1.3K关注 0票数 24

在DOM中单击按钮时,microtask/任务队列的执行顺序不同,与按编程单击按钮的顺序不同。

代码语言:javascript
复制
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');
});
代码语言:javascript
复制
<button id='btn'>Click me !</button>

我的理解是,当调用堆栈为空时,事件循环将接受来自microtask队列的回调,以放置在回调堆栈上。当调用堆栈和微任务队列都为空时,事件循环开始从任务队列接受回调。

当单击带有id btn的按钮时,两个“单击”事件侦听器都被放置在任务队列中,以便在任务队列中声明。

代码语言:javascript
复制
// representing the callstack and task queues as arrays
callstack: []
microtask queue: []
task queue: ["click-1", "click-2"]

事件循环将"click-1" callback放在调用堆栈上。它承诺立即解决问题,将"resolved-1" callback放在微任务队列中。

代码语言:javascript
复制
callstack: ["click-1"]
microtask queue: ["resolved-1"]
task queue: ["click-2"]

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

代码语言:javascript
复制
callstack: ["resolved-1"]
microtask queue: []
task queue: ["click-2"]

"resolved-1" callback被执行。现在调用堆栈和微任务队列都是空的。

代码语言:javascript
复制
callstack: []
microtask queue: []
task queue: ["click-2"]

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

代码语言:javascript
复制
// "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: []

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

代码语言:javascript
复制
"hello click1"
"resolved click1"
"hello click2"
"resolved click2"

我希望它是相同的,然后我用btn.click()编程单击按钮。

代码语言:javascript
复制
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');
});

btn.click()
代码语言:javascript
复制
<button id='btn'>Click me!</button>

然而,输出是不同的。

代码语言:javascript
复制
"hello click1"
"hello click2"
"resolved click1"
"resolved click2"

为什么以编程方式单击按钮时执行顺序有差异?

EN

回答 3

Stack Overflow用户

回答已采纳

发布于 2019-04-16 13:57:53

有趣的问题。

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

代码语言:javascript
复制
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");
});
代码语言:javascript
复制
<input type="button" id="btn" value="Direct Click">
<input type="button" id="btn-simulate" value="Call click()">

由于处理程序是同步运行的,所以只有在两个处理程序完成之后才会处理微任务。更早地处理它们将需要打破JavaScript的运行到完成语义。

相反,当事件是通过DOM发送时,更有趣的是:每个处理程序都是被调用。调用处理程序包括运行脚本后进行清理 (包括执行微任务检查点 ),运行任何挂起的微任务。因此,被调用的处理程序调度的微任务在下一个处理程序运行之前得到运行。

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

看“为什么”略有不同:为什么处理程序在使用click()时被同步调用?主要是因为历史的原因,这是早期浏览器所做的,所以不能改变。但是如果使用dispatchEvent,它们也是同步的

代码语言:javascript
复制
const e = new MouseEvent("click");
btn.dispatchEvent(e);

在这种情况下,处理程序仍然同步运行,因为使用它的代码可能需要查看e,以查看默认操作是否被阻止或类似。(它本来可以有不同的定义,为事件完成分派时提供回调或类似的回调,但事实并非如此。我猜这既不是简单性的,也不是与click兼容的,或者两者兼而有之。)

票数 19
EN

Stack Overflow用户

发布于 2019-04-16 15:11:04

所以,Chrome回答仅仅是因为它很有趣(参见T.JCrowder对一般DOM答案的出色回答)。

代码语言:javascript
复制
btn.click();

HTMLElement::click()中调用C++,这是与DOMElement对应的:

代码语言:javascript
复制
void HTMLElement::click() {
  DispatchSimulatedClick(nullptr, kSendNoEvents,
                         SimulatedClickCreationScope::kFromScript);
}

它主要是围绕dispatchMouseEvent进行一些工作,并处理边缘案例:

代码语言:javascript
复制
void EventDispatcher::DispatchSimulatedClick(
    Node& node,
    Event* underlying_event,
    SimulatedClickMouseEventOptions mouse_event_options,
    SimulatedClickCreationScope creation_scope) {
  // This persistent vector doesn't cause leaks, because added Nodes are removed
  // before dispatchSimulatedClick() returns. This vector is here just to
  // prevent the code from running into an infinite recursion of
  // dispatchSimulatedClick().
  DEFINE_STATIC_LOCAL(Persistent<HeapHashSet<Member<Node>>>,
                      nodes_dispatching_simulated_clicks,
                      (MakeGarbageCollected<HeapHashSet<Member<Node>>>()));

  if (IsDisabledFormControl(&node))
    return;

  if (nodes_dispatching_simulated_clicks->Contains(&node))
    return;

  nodes_dispatching_simulated_clicks->insert(&node);

  if (mouse_event_options == kSendMouseOverUpDownEvents)
    EventDispatcher(node, *MouseEvent::Create(event_type_names::kMouseover,
                                              node.GetDocument().domWindow(),
                                              underlying_event, creation_scope))
        .Dispatch();

  if (mouse_event_options != kSendNoEvents) {
    EventDispatcher(node, *MouseEvent::Create(event_type_names::kMousedown,
                                              node.GetDocument().domWindow(),
                                              underlying_event, creation_scope))
        .Dispatch();
    node.SetActive(true);
    EventDispatcher(node, *MouseEvent::Create(event_type_names::kMouseup,
                                              node.GetDocument().domWindow(),
                                              underlying_event, creation_scope))
        .Dispatch();
  }
  // Some elements (e.g. the color picker) may set active state to true before
  // calling this method and expect the state to be reset during the call.
  node.SetActive(false);

  // always send click
  EventDispatcher(node, *MouseEvent::Create(event_type_names::kClick,
                                            node.GetDocument().domWindow(),
                                            underlying_event, creation_scope))
      .Dispatch();

  nodes_dispatching_simulated_clicks->erase(&node);
}

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

这只是一个直接的电话,没有任务调度涉及。一般来说,EventTarget是一个同步接口,它不延迟任何事情,它先于微滴答的语义和承诺:]

票数 5
EN

Stack Overflow用户

发布于 2019-04-16 13:51:58

dispatchEvent

不同于由DOM触发并通过事件循环异步调用事件处理程序的“本机”事件,dispatchEvent同步调用事件处理程序。在调用dispatchEvent之后,所有适用的事件处理程序都将执行并返回。 dispatchEvent是创建init-分派过程的最后一步,用于将事件分派到实现的事件模型中。可以使用事件构造函数创建事件。

票数 4
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/55709512

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档