JavaScript事件循环详解(async/await、promise、setTimeout的执行顺序)

1、默认的理论基础

  • 执行上下文(Execution context)

  • 函数调用栈(call stack)

  • 队列数据结构(queue)

  • Promise

  • async/await

2、比较难懂的部分基础知识回顾

(1)async/await与Promise的相互转化(这里主要关注的是await(thenable)这种情况下是如何转化为Promise的)

async/await可视为 Promise 的语法糖,两者可以相互转化为彼此的写法:

async function async1() {
    console.log('async1 start')
    await async2()
    console.log('async1 end')
}
    
async function async2() {
    console.log('async2')
}
    
async1();

转化为Promise的写法:

function async1(){
    console.log('async1 start');
    const p = async2();
    return new Promise((resolve) => {
        Promise.resolve().then(() => {
            p.then(resolve)
        })
    })
    .then(() => {
        console.log('async1 end')
    });
}
    
function async2(){
    console.log('async2');
    return Promise.resolve();
}
    
async1();

这里面主要是await v的处理有点出乎意料,可能暂时你对上面的转化有点不太理解,不要紧,待我慢慢道来~~

首先一个通用的转化步骤是:

async function async1() {
    await fn()
}

这里的fn有可能返回的是Promise(async默认返回的就是Promise),也可能直接返回的结果(return 'something'),但是不论如何,上面的这小段代码都可以转化为:

async function async1() {
    return new Promise((resolve) => {resolve(fn())})
}

若await后面还有别的需要执行的语句:

async function async1() {
    await fn()
    console.log('async1 end')
}

await v 后续的代码的执行类似于传入then()中的回调:

function async1(){
    const p = async2();
    return new Promise((resolve) => {
        resolve(fn())
    })
    .then(() => {
        console.log('async1 end')
    });
}

若fn是thenable对象,那么就还可以进一步转化为:

function async1(){
    const p = async2();
    return new Promise((resolve) => {
        Promise.resolve().then(() => {
            p.then(resolve)
        })
    })
    .then(() => {
        console.log('async1 end')
    });
}

我相信到这,应该能够理解文章开始的那段代码的转化结果了吧,如果还是不能够理解,那么可以留言,我一定改到你明白。

3、任务队列

这部分网上也有很多相关的文章,有的讲的也很好,我这边就尽量把我觉得最有助于理解的部分内容说明一下:

  • 一个线程中,事件循环是唯一的,但是任务队列可以拥有多个

  • 任务队列又分为macro-task(宏任务)与micro-task(微任务),在最新标准中,它们被分别称为task与jobs。

  • macro-task大概包括:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。

  • micro-task大概包括: process.nextTick, Promise, Object.observe(已废弃), MutationObserver(html5新特性)

  • setTimeout/Promise等我们称之为任务源。而进入任务队列的是他们指定的具体执行任务。

  • 来自不同任务源的任务会进入到不同的任务队列。其中setTimeout与setInterval是同源的。

  • 其中每一个任务的执行,无论是macro-task还是micro-task,都是借助函数调用栈来完成。

上面这些点基本上包含了分析事件循环时所需的基本知识,下面再先给出事件循环的一个过程:

从script(整体代码)开始第一次循环。之后全局上下文进入函数调用栈。直到调用栈清空(只剩全局),然后执行所有的micro-task。当所有可执行的micro-task执行完毕之后。循环再次从macro-task开始,找到其中一个任务队列执行完毕,然后再执行所有的micro-task,这样一直循环下去。

4、小栗子(一步步的执行顺序)

请看下面一段代码

setTimeout(function() {
    console.log('timeout1');
})
 
new Promise(function(resolve) {
    console.log('promise1');
    for(var i = 0; i < 1000; i++) {
        i == 99 && resolve();
    }
    console.log('promise2');
 }).then(function() {
    console.log('then1');
 })
 
 console.log('global1');

这段代码可能很多人能够给出打印的结果,但是对于其中的执行顺序是否了然于心?下面我们一步步的从任务队列、函数调用栈的角度出发,来分析这段代码:


JavaScript事件循环详解(async/await、promise、setTimeout的执行顺序)_第1张图片
1.png
JavaScript事件循环详解(async/await、promise、setTimeout的执行顺序)_第2张图片
2.png
JavaScript事件循环详解(async/await、promise、setTimeout的执行顺序)_第3张图片
3.png
JavaScript事件循环详解(async/await、promise、setTimeout的执行顺序)_第4张图片
4.png
JavaScript事件循环详解(async/await、promise、setTimeout的执行顺序)_第5张图片
5.png
JavaScript事件循环详解(async/await、promise、setTimeout的执行顺序)_第6张图片
6.png
JavaScript事件循环详解(async/await、promise、setTimeout的执行顺序)_第7张图片
7.png
JavaScript事件循环详解(async/await、promise、setTimeout的执行顺序)_第8张图片
8.png
JavaScript事件循环详解(async/await、promise、setTimeout的执行顺序)_第9张图片
9.png
JavaScript事件循环详解(async/await、promise、setTimeout的执行顺序)_第10张图片
10.png

经过上面这些步骤讲解,相信大家对于事件循环有了一定的认识,当然上面的这段代码是比较简单的,只是为了让大家快速的理解事件循环整个的一个机制,下面有一个综合的小测验希望大家都可以轻易的给出正确的答案。

5、终极考验

setTimeout(function() {
    console.log('timeout1');
})
async function async1(){
  console.log('async1 start')
  await async2()
  console.log('async1 end')
}
async function async2(){
  console.log('async2')
}
async1();

Promise.resolve(
    new Promise(function(resolve) {
        console.log('qqq');
        resolve();
    }).then(function(){
        console.log('www');
    })
)
.then(()=>{
    console.log('promise resolve')
})
new Promise(function(resolve) {
    console.log('promise1');
    for(var i = 0; i < 1000; i++) {
        i == 99 && resolve();
    }
    new Promise(function(resolve) {
        console.log('ddd');
        resolve();
    }).then(function(){
        console.log('dcdcd');
    })
    console.log('promise2');
}).then(function() {
    console.log('then1');
}).then(function() {
    console.log('then2');
})
 
console.log('global1');

大家不要对这段程序有什么恐惧心理,就按照此前的分析步骤一步步的去剖解它,就可以得出正确的答案。这里需要提醒的是async1 end的输出位置,请大家留意。

当然如果大家还是感觉一步步的分析有什么困难的话,可以给我留言,我会补一个分析的过程,这里就不赘述了,因为比较长。。

谢谢大家。

你可能感兴趣的:(JavaScript事件循环详解(async/await、promise、setTimeout的执行顺序))