Why is the code in Promise executed before setTimeout?

First, look at the execution order of a piece of code

setTimeout(() => console.log("a"), 0)
let r = new Promise(function (resolve, reject) {
  resolve()
});
r.then(() => {
  let begin = Date.now();
  while (Date.now() - begin < 1000){
    console.log('b')
  }
  console.log("c")
  new Promise(function (resolve, reject) {
    resolve()
  }).then(() => console.log("d"))
});

macro and micro tasks

The JavaScript engine waits for the host environment to assign macro tasks. In the operating system, the usual waiting behavior is an event loop, so in Node terminology, this part is also called an event loop.

What the whole loop does is basically "wait-execute" over and over again. Of course, there are logics such as judging whether the loop is over, macro task queue, etc.

Each execution process here is actually a macro task. We can roughly understand that the queue of macro tasks is equivalent to the event loop.

Common macro tasks: I/O, setTimeout, setInterval; micro tasks: Promise.then catch finally, process.nextTick

In macro tasks, JavaScript's Promise also generates asynchronous code, and JavaScript must ensure that these asynchronous codes are completed in a macro task. Therefore, each macro task contains a micro task queue:


task.jpg

With the macro-task and micro-task mechanism, we can implement JavaScript engine-level and host-level tasks, for example: Promise always adds micro-tasks at the end of the queue. Host APIs such as setTimeout will add macro tasks.

Next, let's introduce Promise in detail.

Promise

Promise is a standardized asynchronous management method provided by the JavaScript language. Its general idea is that functions that need to perform io, wait, or other asynchronous operations do not return real results, but return a "promise", and the caller of the function can be in When the time is right, choose to wait for this promise to be fulfilled (through the callback of Promise's then method).

A basic usage example of Promise is as follows:

function sleep(duration) {
        return new Promise(function(resolve, reject) {
            setTimeout(resolve,duration);
        })
    }
    sleep(1000).then( ()=> console.log("finished"));

This code defines a function sleep, its function is to wait for the time specified by the incoming parameter.

Promise's then callback is an asynchronous execution process. Next, let's study the execution sequence in the Promise function. Let's look at a code example:

var r = new Promise(function(resolve, reject){
        console.log("a");
        resolve()
    });
    r.then(() => console.log("c"));
    console.log("b")

我们执行这段代码后,注意输出的顺序是 a b c。在进入 console.log(“b”) 之前,毫无疑问 r 已经得到了 resolve,但是 Promise 的 resolve 始终是异步操作,所以 c 无法出现在 b 之前。

接下来我们试试跟 setTimeout 混用的 Promise。

在这段代码中,我设置了两段互不相干的异步操作:通过 setTimeout 执行 console.log(“d”),通过 Promise 执行 console.log(“c”)。

var r = new Promise(function(resolve, reject){
        console.log("a");
        resolve()
    });
    setTimeout(()=>console.log("d"), 0)
    r.then(() => console.log("c"));
    console.log("b")

不论代码顺序如何,d 必定发生在 c 之后,因为 Promise 产生的是 JavaScript 引擎内部的微任务,而 setTimeout 是浏览器 API,它产生宏任务。

通过一系列的实验,我们可以总结一下如何分析异步执行的顺序:

  • 首先我们分析有多少个宏任务;
  • 在每个宏任务中,分析有多少个微任务;
  • 根据调用次序,确定宏任务中的微任务执行次序;
  • 根据宏任务的触发规则和调用次序,确定宏任务的执行次序;
  • 确定整个顺序。

我们再来看一个稍微复杂的例子:

function sleep(duration) {
        return new Promise(function(resolve, reject) {
            console.log("b");
            setTimeout(resolve,duration);
        })
    }
    console.log("a");
    sleep(5000).then(()=>console.log("c"));

这是一段非常常用的封装方法,利用 Promise 把 setTimeout 封装成可以用于异步的函数。

我们首先来看,setTimeout 把整个代码分割成了 2 个宏观任务,这里不论是 5 秒还是 0 秒,都是一样的。

第一个宏观任务中,包含了先后同步执行的 console.log(“a”); 和 console.log(“b”);。

setTimeout 后,第二个宏观任务执行调用了 resolve,然后 then 中的代码异步得到执行,所以调用了 console.log(“c”),最终输出的顺序才是: a b c。

Promise 是 JavaScript 中的一个定义,但是实际编写代码时,我们可以发现,它似乎并不比回调的方式书写更简单,但是从 ES6 开始,我们有了 async/await,这个语法改进跟 Promise 配合,能够有效地改善代码结构。

新特性:async/await

async/await 是 ES2016 新加入的特性,它提供了用 for、if 等代码结构来编写异步的方式。它的运行时基础是 Promise,面对这种比较新的特性,我们先来看一下基本用法。

async 函数必定返回 Promise,我们把所有返回 Promise 的函数都可以认为是异步函数。

async 函数是一种特殊语法,特征是在 function 关键字之前加上 async 关键字,这样,就定义了一个 async 函数,我们可以在其中使用 await 来等待一个 Promise。

function sleep(duration) {
    return new Promise(function(resolve, reject) {
        setTimeout(resolve,duration);
    })
}
async function foo(){
    console.log("a")
    await sleep(2000)
    console.log("b")
}

这段代码利用了我们之前定义的 sleep 函数。在异步函数 foo 中,我们调用 sleep。

async 函数强大之处在于,它是可以嵌套的。我们在定义了一批串联操作的情况下,可以利用 async 函数组合出新的 async 函数。

function sleep(duration) {
    return new Promise(function(resolve, reject) {
        setTimeout(resolve,duration);
    })
}
async function foo(name){
    await sleep(2000)
    console.log(name)
}
async function foo2(){
    await foo("a");
    await foo("b");
}

这里 foo2 用 await 调用了两次异步函数 foo,可以看到,如果我们把 sleep 这样的异步操作放入某一个框架或者库中,使用者几乎不需要了解 Promise 的概念即可进行异步编程了。

总结

今天主要了解了 JavaScript 执行部分的知识,首先介绍了 JavaScript 的宏观任务和微观任务相关的知识。我们把宿主发起的任务称为宏观任务,把 JavaScript 引擎发起的任务称为微观任务。许多的微观任务的队列组成了宏观任务。

然后还介绍了用 Promise 来添加微观任务的方式,并且介绍了 async/await 这个语法。

总而言之一个js脚本本身对于浏览器而言就是一个宏任务,也是第一个宏任务,而处于其中的代码可能有3种:非异步代码、产生微任务的异步代码(promise等)、产生宏任务的异步代码(settimeout、setinterval等)。
我们知道宏任务处于一个队列中,应当先执行完一个宏任务才会执行下一个宏任务,所以在js脚本中,会先执行非异步代码,再执行微任务代码,最后执行宏任务代码。这时候我们进行到了下一个宏任务中,又按照这个顺序执行。
微任务总是先于宏任务这个说法不准确,应该是处于同一级的情况下才能这么说。实际上微任务永远是宏任务的一部分,它处于一个大的宏任务内。

Guess you like

Origin blog.csdn.net/HashTang/article/details/108019525