事件循环更喜欢微任务队列而不是回调队列?

event loop prefers microtask queue over callback queue?

我正在测试 JS 中异步代码的概念。在回调队列和微任务队列顺序之间感到困惑。每当 promise 对象被解析时,实现方法 { then } 被推入微任务队列,同时浏览器定时器函数(如 setTimeout )的回调被推入回调队列。事件循环不断检查队列,并在调用堆栈变空时将函数从队列推入调用堆栈。事件循环应该更喜欢微任务队列而不是普通的回调队列,但在示例中:https://jsfiddle.net/BHUPENDRA1011/2n89ftmp/ 否则会发生。

function display(data) {
    console.log('hi back from fetch');
}

function printHello() {
    console.log('hello');
}

function blockfor300ms() {
    for (let i = 0; i < 300; i++) {
        // just delaying logic
    }
}
// this sets the printHello function in callback queue { event loop queue }
setTimeout(printHello, 0);

const futureData = fetch('https://api.github.com/users/xiaotian/repos');
// after promise is resolved display function gets added into other queue : Microtask queue { job queue}
futureData.then(display);
// event loop gives prefrence to Microtask queue ( untill  its complete) 

blockfor300ms();
// which runs first 
console.log('Me first !')

预期输出

实际输出:

请告诉我这里的情况。

谢谢

futureData 实际上是一个获取承诺,因此当 fetch 被调用时,绝对有一个网络任务排队进入任务队列。结果, printHello 肯定会在网络任务之前执行,因为它们都是任务。而方法 display 只会在网络任务的承诺被 resolved 时被放入微任务队列。根据定义,微任务仅在每个任务结束时执行。因此 display 将在网络任务结束时被调用,而 printHello 之前已经调用了很长时间。

如果您希望 displayprintHello 之前被调用,futureData 必须只对微任务进行排队。让我们稍微修改一下您的示例。

function display(data) {
    console.log('hi back from fetch');
}

function printHello() {
    console.log('hello');
}

let now = Date.now();
function executeFutureDataWithMicrotasksOnly() {
    // Execute microtasks continually in 300ms.
    return Promise.resolve().then(() => Date.now() - now < 300 && executeFutureDataWithMicrotasksOnly());
}

function blockfor300ms() {
    for (let i = 0; i < 300; i++) {
        // just delaying logic
    }
}
// this sets the printHello function in callback queue { event loop queue }
setTimeout(printHello, 0);

const futureData = executeFutureDataWithMicrotasksOnly();
// after promise is resolved display function gets added into other queue : Microtask queue { job queue}
futureData.then(display);
// event loop gives prefrence to Microtask queue ( untill  its complete) 

blockfor300ms();
// which runs first 
console.log('Me first !')

从上面的例子可以看出,如果将 fetch 替换为只有微任务的方法,执行顺序会按预期更改,尽管 fetchexecuteFutureDataWithMicrotasksOnly 都是在类似的时间间隔内执行。当futureData不再排队任务时,包括display在内的所有微任务都会在当前执行的任务结束时执行,也就是任务printHello的前一个任务。

我认为问题出在这样一个事实,即您的函数 blockfor300ms 没有将线程阻塞足够长的时间以使 fetch 无法接收到响应。 当事件循环发现它可以调用 printHello.

时,作业队列中(还)没有任何内容

虽然这是真的,但“kib”说的是:

"your function blockfor300ms doesn't block the thread long enough for the fetch to receive a response"

遗憾的是,这无关紧要,因为即使您在收到对提取调用的响应之前确实阻止执行,您仍然会看到相同的结果... (请参阅下面的示例代码片段,您可以使用警告框或非异步 XMLHttpRequest 调用的长循环来阻止执行,我收到了相同的结果)

不幸的是,fetch 并不像我找到的所有博客和资源所描述的那样工作......其中指出

Fetch will add it's promise chain to the micro-tasks queue and run before any callbacks on the next tick of the event loop.

从下面的示例代码中可以看出,fetch 并不像其他人描述的那样简单地将其解析处理函数添加到微任务队列,因为正如 Lewis 所说,因为它需要network activity 它正在由网络任务源处理。但是,我不认为这是由于 printHello“阻塞”了网络任务,因为在我下面的示例代码中 fetch 在 printHello 之前被触发,并且网络响应也会在计时器完成之前发生。

正如您在下面的示例中看到的那样,我将 printHello 延迟添加到任务队列中很长时间后才收到获取响应(2000 毫秒),但是如果我们阻止代码执行超过 2000 毫秒(因此仍然有 运行 执行上下文)然后将首先打印“Hello”。这意味着 fetch resolve() 处理程序不会简单地添加到微任务队列中,它会与其他承诺处理程序一起触发。

那么,如果在计时器任务完成之前(在 2000 毫秒)收到响应并理论上将其添加到任务队列中,为什么在回调之后仍会记录它? 好吧,我的猜测是定时器任务源的优先级一定高于网络任务源。因此两者都在他们的任务队列中,但定时器任务队列在网络任务队列之前触发...

规格链接:

计时器任务源 - https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#timer-task-source 网络任务源 - https://html.spec.whatwg.org/multipage/webappapis.html#networking-task-source

function display(data){console.log("Fetch resolved!")}
function printHello(){console.log("Callback Time")}
function blockExecution() {
  console.log("Blocking execution...");
  alert('Wait at least 2000ms before closing');
}

const futureData = fetch('https://jsonplaceholder.typicode.com/todos/1');
futureData.then(response => response.json()).then(display);

setTimeout(printHello, 2000);

const p = new Promise(
    // this is called the "executor"
    (resolve, reject) => {
        console.log("I'm making a promise...");
        resolve("Promise resolved!");
        console.log("Promise is made...");
    }
);

p.then(
    // this is called the success handler
    result => console.log(result)
);

blockExecution();

console.log("Execution ended!");

Using Visualization

这将帮助您更好地理解 javascript 的工作原理。

在这种情况下,fetch(Promise) 比 setTimeout 花费更多时间,所以当事件循环周期为 运行 fetch(Promise) 仍在progress 和 setTimeout 首先执行,因为它花费的时间更少并且从任务队列中出来并得到处理,当 fetch(Promise) 结束时它从微任务队列中出来。

如果您要增加 setTimeout 时间,那么第一个提取 (Promise) 将首先发生,然后 setTimeout 将发生。希望这能解决您的问题。

似乎比大家想象的要简单:

It is possible for a microtask to be moved to a regular task queue, if, during its initial execution, it spins the event loop. HTML Living Standard #queue a microtask

当循环正在从任务队列中选择任务时,它可以选择执行之前排入微任务队列并且现在是任务队列一部分的任务:

In that case, the task chosen in the next step was originally a microtask, but it got moved as part of spinning the event loop. HTML Living Standard #event loop processing model

旋转循环的代码是任何包含并行操作的代码:

  1. In parallel:
  1. Wait until the condition goal is met.

HTML Living Standard #spin-the-event-loop