关于promise,async,setTimeout的执行顺序

前端100问第10题

参考:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/7 

 

请写出下面代码的运行结果

复制代码
async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
    console.log('async2');
}
console.log('script start');
setTimeout(function() {
    console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});
console.log('script end');
复制代码

我当时写的答案是:

  1. script start
  2. async1 start
  3. async2
  4. promise1
  5. script end
  6. promise2
  7. async1 end
  8. setTimeout

正确答案是:

  1. script start
  2. async1 start
  3. async2
  4. promise1
  5. script end
  6. async1 end
  7. promise2
  8. setTimeout

说明我还是没有真正理解它们的执行顺序。于是看着大牛写的答案来学习。

 

任务队列

首先我们需要明白以下几件事情:

  • JS分为同步任务和异步任务
  • 同步任务都在主线程上执行,形成一个执行栈
  • 主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件
  • 一旦执行栈中的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行

每个任务都有一个任务源(task source),源自同一个任务源的task必须放到同一个任务队列,从不同源来的则被添加到不同队列。setTimeout/Promise等API就是任务源。

 

宏任务 (macro task)

每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)

浏览器为了能够使得JS内部macro task与DOM任务能够有序的执行,会在一个macro task执行结束后,在下一个macro task执行开始前,对页面进行重新渲染。流程如下:

(macro)task -> 渲染 -> (macro)task ->...

(macro)task主要包含:script整体代码、setTimeout、setInterval、I/O、UI交互事件、postMessage、MessageChannel、setImmediate(Node.js环境)

微任务 (microtask)

在当前task执行结束后立即执行的任务。也就是说,在当前task任务后,下一个task之前,在渲染之前

所以它的响应速度相比setTimeout会更快,因为无需等渲染。也就是说,在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前)

microtask主要包含:Promise.then、MutationObserver、process.nextTick(Node.js环境)

 

运行机制

  • 执行一个宏任务(栈中没有就从事件队列中获取)
  • 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
  • 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
  • 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)

 

写在Promise中的代码被当做同步任务立即执行。而在async/await中,在出现await之前,其中的代码也是立即执行。

await是一个让出线程的标志。await后面的表达式会先执行一遍,将await后面的代码加入到microtask中,然后就会跳出整个async函数来执行后面的代码。

await后面的代码是microtask

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

等价于

async function async1() {
  console.log('async1 start')
  Promise.resolve(async2()).then(() => {
    console.log('async1 end')
  })    
}    

 

以上就是本道题涉及到的所有相关知识点了。下面我们再回到这道题来一步一步看看怎么回事

 

1.首先,事件循环从宏任务队列开始,这个时候,宏任务队列中,只有一个script整体代码任务,当遇到任务源时,则会先分发任务到对应的任务队列中去。

2.然后我们看到首先定义了两个async函数,然后遇到了console语句,直接输出script start.输出之后,script任务继续往下执行,遇到setTimeout,其作为一个宏任务源,则会先将其任务分发到对应的队列中

3.script任务继续往下执行,执行了async1()函数,输出async1 start,遇到await时,会将await后面的表达式执行一遍,所以紧接着输出async2,然后将await后面的代码也就是console.log('async1 end')加入到microtask中的Promise队列中。接着跳出async1函数来执行后面的代码

4.接着遇到Promise实例。由于Promise中的函数时立即执行的,而后续的.then则会被分发到microtask的Promise队列中。所以会先输出promise1, 然后执行resolve,将promise2分配到对应队列。

5.最后只有一句输出了script end,至此,全局任务就执行完毕了。

根据上述,每次执行完一个宏任务之后,回去检查是否存在Microtask,如果有,则执行Microtasks直至清空Microtask Queue

因而再script任务执行完毕之后,开始查找清空微任务队列。此时,微任务中,Promise队列有的两个任务async1 end和promise2,因此按先后顺序输出。当所有的Microtasks执行完毕之后,表示第一轮的循环就结束了。

6.第二轮循环依旧从宏任务队列开始,此时宏任务中只有一个setTimeout,取出直接输出即可,至此整个流程结束。

 

最后原答案作者又加了三个变式用来加深印象,非常好。

 

变式一

在第一个变式中将async2中的函数也变成Promise函数,代码如下:

复制代码
async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
    //async2做出如下更改:
    new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
    });
}
console.log('script start');

setTimeout(function() {
    console.log('setTimeout');
}, 0)
async1();

new Promise(function(resolve) {
    console.log('promise3');
    resolve();
}).then(function() {
    console.log('promise4');
});

console.log('script end');
复制代码

这一次我写的答案和作者给出的答案一样,如下:

  1. script start
  2. async1 start
  3. promise1
  4. promise3
  5. script end
  6. promise2
  7. async1 end
  8. promise4
  9. setTimeout

 

变式二

复制代码
async function async1() {
    console.log('async1 start');
    await async2();
    //更改如下:
    setTimeout(function() {
        console.log('setTimeout1')
    },0)
}
async function async2() {
    //更改如下:
    setTimeout(function() {
        console.log('setTimeout2')
    },0)
}
console.log('script start');

setTimeout(function() {
    console.log('setTimeout3');
}, 0)
async1();

new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});
console.log('script end');
复制代码

这道题我给出的答案是:

  1. script start
  2. async1 start
  3. promise1
  4. script end
  5. promise2
  6. setTimeout2
  7. setTimeout1
  8. setTimeout3

正确答案是:

  1. script start
  2. async1 start
  3. promise1
  4. script end
  5. promise2
  6. setTimeout3
  7. setTimeout2
  8. setTimeout1

 

在输出为promise2之后,接下来会按照加入setTimeout队列的顺序来依次输出,通过代码我们可以看到加入顺序为3 2 1,所以会按3,2,1的顺序来输出。

 

变式三

复制代码
async function a1 () {
    console.log('a1 start')
    await a2()
    console.log('a1 end')
}
async function a2 () {
    console.log('a2')
}

console.log('script start')

setTimeout(() => {
    console.log('setTimeout')
}, 0)

Promise.resolve().then(() => {
    console.log('promise1')
})

a1()

let promise2 = new Promise((resolve) => {
    resolve('promise2.then')
    console.log('promise2')
})

promise2.then((res) => {
    console.log(res)
    Promise.resolve().then(() => {
        console.log('promise3')
    })
})
console.log('script end')
复制代码

答案:

  1. script start
  2. a1 start
  3. a2
  4. promise2
  5. script end
  6. promise1
  7. a1 end
  8. promise2.then
  9. promise3
  10. setTimeout

 

posted on   独自去流浪  阅读(869)  评论(0编辑  收藏  举报

编辑推荐:
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
阅读排行:
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

统计

点击右上角即可分享
微信分享提示