头图

问题描述

在看事件循环相关视频的时候发现其中有一个例子不理解,查了资料才明白其中的缘由,遂以志之。问题是这样的:

<button id="button">button</button>
button.addEventListener("click", () => {
    Promise.resolve().then(() => console.log("Microtask 1"));
    console.log("Listener 1");
});
button.addEventListener("click", () => {
    Promise.resolve().then(() => console.log("Microtask 2"));
    console.log("Listener 2");
});

要问的是:

  1. 你手动点击button,打印的log顺序是啥?
  2. 如果改成用js调用lick的方式,那么输出顺序又是啥?

    // 在上面的js代码最后一行新增
    button.click();

问题解析

首先,公布一下答案。
第一种情况输出是:

Listener 1
Microtask 1
Listener 2
Microtask 2

第二种情况的输出是:

Listener 1
Listener 2
Microtask 1
Microtask 2

先回顾一下时间循环中的部分知识点:
在事件循环中,点击事件的回调属于Macrotask,Promise.then的回调属于Microtask,先执行一个Macrotask,然后等调用栈为空之后就会执行所有Microtask。

结合上述知识,情况1的答案是完全符合这个说法的。
但是情况2就不能理解了,为何连着执行了两个Macrotask,然后才执行的Microtask,难道这个这个逻辑有特殊情况?非也!
问题的核心在于手动点击的方式回调是异步的,而button.click()这种调用方式它是同步的,没错,它是同步调用的。

和经由浏览器触发,并通过事件循环异步调用事件处理程序的“原生”事件不同,dispatchEvent() 会同步调用事件处理函数。在 dispatchEvent() 返回之前,所有监听该事件的事件处理程序将在代码继续前执行并返回。
来源文档MDN:EventTarget.dispatchEvent()

明白这一点之后再看情况2就很好理解了,就相当于:

function fn1() {
    Promise.resolve().then(() => console.log("Microtask 1"));
    console.log("Listener 1");
});
function fn2 {
    Promise.resolve().then(() => console.log("Microtask 2"));
    console.log("Listener 2");
});

// button.click();
fn1();
fn2();

最后

这个题目出现在一个与事件循环相关的视频中,于是有不少文章在解释情况2的时候都会解释说此时调用栈不为空,所以不能执行Microtask,但却没有解释为啥不为空,以及调用栈里有啥。我看了解释完全摸不着头脑,最后在了解了dispatchEvent同步调用的知识点后才恍然大悟,这根本就是两码事嘛!吃了没文化的亏。


aqiongbei
2k 声望283 粉丝

人生路上,你走的每一步都算数