【问题标题】:event loop prefers microtask queue over callback queue?事件循环更喜欢微任务队列而不是回调队列?
【发布时间】:2018-11-18 02:59:21
【问题描述】:

我在 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 !')

预期输出

  • 我先!
  • 从 fetch 回来
  • 你好

实际输出:

  • 我先!
  • 你好
  • 从 fetch 回来

请告诉我这里的情况。

谢谢

【问题讨论】:

    标签: javascript asynchronous event-loop


    【解决方案1】:

    这似乎比我们想象的要容易:

    如果在初始执行期间,它会旋转事件循环,则可以将微任务移动到常规任务队列。 HTML Living Standard #queue a microtask

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

    在这种情况下,下一步选择的任务最初是一个微任务,但它作为旋转事件循环的一部分被移动了。 HTML Living Standard #event loop processing model

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

    1. 并行:
    1. 等到满足条件目标。

    HTML Living Standard #spin-the-event-loop

    【讨论】:

      【解决方案2】:

      Using Visualization

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

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

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

      【讨论】:

        【解决方案3】:

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

        "你的函数 blockfor300ms 没有阻塞线程足够长的时间 获取响应”

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

        不幸的是,我发现的所有博客和资源都没有描述 fetch 的工作原理……这表明

        Fetch 会将它的 Promise 链添加到微任务队列中,并在事件循环的下一个周期的任何回调之前运行。

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

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

        那么,如果在计时器任务完成(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!");

        【讨论】:

          【解决方案4】:

          我认为问题在于您的函数blockfor300ms 没有阻塞线程足够长的时间以使获取接收响应。 当事件循环看到它可以调用printHello 时,作业队列中(还)不会有任何东西。

          【讨论】:

            【解决方案5】:

            futureData 实际上是一个 fetch 承诺,因此当调用 fetch 时,绝对有一个网络任务排队到任务队列中。结果,printHello 肯定会在网络任务之前执行,因为它们都是任务。而方法display只会在网络任务的promise解决后才会被放入微任务队列。根据定义,微任务仅在每个任务结束时执行。所以display会在网络任务结束时被调用,而printHello已经被调用了很久。

            如果您希望在printHello 之前调用display,则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的前一个任务。

            【讨论】:

            • 是的,在这种情况下,promise 会立即解析,因此它会进入微任务队列。如果 Promise 被链接,事件循环会查看其他队列 { 回调队列 } 还是会一直监视微任务队列直到它结束。如果是,那么一旦切换到回调队列,事件循环如何返回到微任务队列
            • @Bhupendra 更新了我的答案以获得更详细的解释。
            猜你喜欢
            • 2013-12-13
            • 2022-10-06
            • 2021-05-17
            • 2015-08-20
            • 2018-09-18
            • 1970-01-01
            • 1970-01-01
            • 2012-12-03
            • 1970-01-01
            相关资源
            最近更新 更多