Javascript高级-async/await、Promise与微任务(终结篇)

✊不积跬步,无以至千里;不积小流,无以成江海。

前言

关于任务队列、微任务的定义可以参考之前的笔记

Javascript高级-任务队列与微任务队列 - 掘金 (juejin.cn)

关于promise,可以参考之前的笔记

Javascript高级 - Promise专题 - 掘金 (juejin.cn)

关于async / await,可以参考之前的笔记

Javascript高级-回调与异步 - 掘金 (juejin.cn)

宏任务、微任务、事件执行顺序

宏任务

宏任务是指在当前同步任务完成后执行的任务。宏任务包括:

  • setTimeout 和 setInterval 定时器
  • DOM 事件

微任务

微任务是指在当前宏任务完成后立即执行的任务。微任务包括:

  • Promise.then

执行顺序

在 JavaScript 引擎中,事件循环会不断执行以下步骤:

  1. 执行所有同步代码
  2. 执行所有微任务
  3. 执行所有宏任务

示例讲解promise与微任务

示例1

有代码:

javascript 复制代码
let p = new Promise(function f1(resolve) {
  setTimeout(function f2(){ 
    resolve(2) 
    console.log(1)
  }, 1000)
})
p.then(function f3(v) { console.log(v) })

关于这段代码,运行的详细流程是:

  • 运行同步代码f1,创建定时器,初始化p.then
  • 1秒之后,f2加入宏队列。此时宏队列:[f2] , 微队列: []
  • 先扫描微队列(为空),再扫描宏队列 (拿出一个任务f2),运行f2。遇到resolve,触发把之前p.then(f3)的f3移入微任务队列(遇到 resolve 表示异步状态可以被激活,事件可以进入队列中来。由于 resolve 是在执行阶段执行的,因此 f3 函数会被加入到微任务队列中。)
  • 之后执行f2函数的最后一步,输出1。此时宏队列:[] , 微队列: [f3]
  • 扫描微队列,依次拿出并运行全部任务,执行f3输出2。

所以最输出结果是1秒之后立即依次出现 1、2

核心思路:

  1. promise中有些是同步执行的,比如说resolve之前的代码;有些是异步执行的,比如说then,setTimeout等。
  2. 在 Promise 中,先扫描微任务,再扫描宏任务
  3. resolve 被调用时,Promise 会将其值传递给 .then 方法中指定的函数。

示例2

有代码:

javascript 复制代码
let p1 = new Promise(function f1(resolve1) {
  setTimeout(resolve1)
})
let p2 = p1.then(function f2(v) { console.log(2) })
let p3 = p2.then(function f3(v) { console.log(3) })
 
let p11 = new Promise(function f11(resolve2) {
  setTimeout(resolve2)
})
let p22 = p11.then(function f22(v) { console.log(22) })
let p33 = p22.then(function f33(v) { console.log(33) })

以上代码的执行流程是:

  • 先执行同步的代码。运行f1,立即得到一个pending状态的Promise对象p1(f1里面的resolve1函数被加入宏任务,还没开始执行)。运行p1.then得到一个pending状态的Promise对象p2。... ,同理得到一个pending状态的Promise对象p33。此时宏队列:[resolve1, resolve2] , 微队列: []
  • 因微队列目前为空,所以扫描宏队列,拿出resolve1运行,导致p1被resolve(从pending变成fulfilled),从而导致p1.then(f2)中的f2被加入微队列。此时宏队列:[resolve2] ,微队列:[f2]
  • 扫描全部微任务,拿出f2运行,输出2 。f2运行结束(函数结束或者遇到return)时,触发p2内部状态的变化(p2从pending变成fulfilled),导致p2.then(f3)中的f3加入微任务队列。此时宏队列:[resolve2] ,微队列:[f3] 。微任务队列不为空,拿出f3,运行输出3 ,此时p3变成fulfilled状态。宏队列:[resolve2] ,微队列:[]
  • 扫描下一个宏任务,拿出resolve2,运行,导致p11被resolve,从而导致p11.then(f22)中的f22被加入微队列。此时宏队列:[] ,微队列:[f22]
  • 扫描全部微任务,拿出f22运行,输出22 。f22运行结束时,触发p22从pending变成fulfilled,导致p22.then(f33)中的f33加入微任务队列。此时宏队列:[] ,微队列:[f33] 。微任务队列还未扫描完,拿出f33,运行输出33 ,此时p33变成fulfilled状态。微队列:[]

最终输出顺序为 2、3、22、33。

核心思想:

  1. 在 Promise 对象刚创建时,其状态默认是 pending。
  2. 调用 resolve 方法时,Promise 对象会从 pending 变为 fulfilled。
  3. Promise 对象所代表的操作成功完成时,Promise 对象会从 pending 变为 fulfilled。
  4. Promise 对象的状态一旦变为 fulfilled,就不会再改变。
  5. 如果 Promise 对象所代表的操作失败,则 Promise 对象的状态会变为 rejected。

示例1 & 2总结

关于上面两个例子,总结的解题思路:

  1. promise对象创建时,状态总是默认为pending,但一旦状态转变为resolve(reject),状态就会变为fulfilled(rejected)。一旦状态改变,.then中的事件立刻加入微队列。
  2. 总是优先执行微队列中的任务,之后再考虑宏队列中的任务。

对于Promise我们需要知道,链式调用.then之后会返回一个新的Promise对象。

因此示例2中的示例有时也写做:

javascript 复制代码
new Promise( resolve => setTimeout(resolve) )
  .then( v => console.log(2) )
  .then( v => console.log(3) )
 
new Promise( resolve => setTimeout(resolve) )
  .then( v => console.log(22) )
  .then( v => console.log(33) )

示例三

对于上面提到的两点,再用一个例子:

javascript 复制代码
let p1 = Promise.resolve(1)
let p2 = p1.then(function f2() {
  console.log(2)
})
let p3 = p2.then(function f3() {
  console.log(3)
})
 
let p11 = new Promise(function f11(resolve) {
  resolve(11)
})
let p22 = p11.then(function f22() {
  console.log(22)
})
let p33 = p22.then(function f33() {
  console.log(33)
})

以上代码的执行流程是:

  • 先执行同步的代码。运行resolve1,立即得到一个fulfilled状态的Promise对象p1。p11是立即执行的同步函数,运行resolve11,立即得到一个fulfilled状态的Promise对象p11
  • 因为resolve已经执行,所以立刻执行p1then & p11 then;p2是p1的then,p22是p11的then,所以f2 & f22放到微任务队列中。目前宏队列:[] ;微队列: [f2,f22]
  • p3是p2的then,暂时还不会运行;p33是p22的then,暂时还不会运行。
  • 之后立刻执行微队列,打印2 & 22。
  • 由于p2 / p22状态从pending变为fulfilled,所以p3 & p33的then被激活,进入到微队列。目前宏队列:[] ;微队列: [f3,f33]
  • 之后立刻执行微队列,打印3 & 33。

最后输出结果为 2,22,3,33。以上代码等价于常见链式写法:

javascript 复制代码
Promise.resolve(1)
  .then(() => console.log(2))
  .then(() => console.log(3))
 
new Promise(resolve => resolve())
  .then(() => console.log(22))
  .then(() => console.log(33))

示例四

这是一个关于Promise 微任务的题目:

javascript 复制代码
let p1 = Promise.resolve()
  .then(function f1(v) { console.log(1) })
  .then(function f2(v) { console.log(2) })
  .then(function f3(v) { console.log(3) })
 
p1.then(function f4(v) { console.log(4) })
p1.then(function f5(v) { console.log(5) })
 
let p2 = Promise.resolve()
  .then(function f11(v) { console.log(11) })
  .then(function f22(v) { console.log(22) })
  .then(function f33(v) { console.log(33) })
 
p2.then(function f44(v) { console.log(44) })
p2.then(function f55(v) { console.log(55) })

以上代码的执行流程是:

  • 先执行同步的代码。运行resolve,立即得到一个fulfilled状态的Promise对象p1 & p2。
  • 但是p1 & p2必须运行完全部的resolve后的then,才能够被算做是p1完全被resolve。
  • 所以按照顺序,先将f1 & f11放入微任务队列。目前宏队列:[] ;微队列: [f1,f11]
  • 之后优先执行微任务队列,即输出 1 & 11。之后微队列中的f1 & f11状态变为fulfilled被释放后,f2 & f22由于是f1 & f11的then所以马上被推入微队列。
  • 同理f3 & f33。因此在p1 & p11彻底被resolve时,输出为 1,11,2,22,3,33。
  • 当p1 & p2被完全resolve后,p1 / p2 的then被推入微队列。由于优先执行p1的then(代码顺序问题)。导致,宏队列:[] ;微队列: [f4,f5,f44,f55]
  • 所以输出为 4,5,44,55。

因此最后的输出为,1,11,2,22,3,33,4,5,44,55。

示例讲解async/await转换Promise,以及微任务

await 操作符用于等待一个Promise对象。如果该值不是一个 Promise,await 会把该值转换为 resolved 的Promise。

示例一

有一段代码:

javascript 复制代码
async function async1(){
  console.log(1)
  await 1
  console.log(2)
}
let p = async1()
console.log(p)

async函数一定返回Promise对象,上面的代码等价于code 2:

javascript 复制代码
function async1() {
  console.log(1)
  return new Promise(resolve => {
    resolve(1)
  }).then(() => {
    console.log(2)
  })
}
let p = async1()
console.log(p)

已知await = promise对象,所以可以将await语句替换为,return new promise().then() 且 resolve(1)。

因此第一步先打印1,之后返回一个promise对象,当这个对象fulfilled之后,才继续将then加入到微队列中打印2。

所以打印出来的值是1,promise{ < pending > },2。

示例二

有一段代码:

javascript 复制代码
async function async2() {
  console.log(2)
  return 2
}

async function async1(){
  console.log(1)
  await async2()
  console.log(3)
}

async1()

由于await 语句之后的代码是await的等价的Proimise对象的then逻辑,因此代码可被改写为:

scss 复制代码
function async2() {
  console.log(2)
  return Promise.resolve(2)
}
function async1() {
  console.log(1)
  return async2()
    .then(() => console.log(3))
}
async1()

示例三

又有一段代码:

javascript 复制代码
async function async2() {
  console.log('async2 start')
  return new Promise((resolve, reject) => {
    resolve()
    console.log('async2 promise')
  })
}

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

async1()

如果async中又有return promise并且被await,改写方法如下:

javascript 复制代码
function async2() {  
  console.log('async2 start')
  return new Promise(resolve2 => {
    new Promise((resolve, reject) => {
      resolve()
      console.log('async2 promise')
    }).then(() => resolve2())
  })
}
  1. 先假设async2中的return new Promise(...)替换为return 2
  2. 所以await async2()在async2中就会被改写为 return Promise.resolve(2)【示例二】
  3. 这时再把 2 和 new Promise(...)替换回来变为 return Promise.resolve(new Promise(...))
  4. 但由于示例2中的.resolve(2)这种写法不适用于这个例子,要把这个例子再标准化一下,变为return new Promise( () => { new Promise(...) }).then()
  5. 由于替换为resolve的promise,不存在其他情况【通常为(resolve, reject)】,所以标准化后的promise传入的参数为resolve,为区分变量名,命名为resolve2,即改写为return new Promise( resolve2 => { new Promise(...) }).then()
  6. then中使用箭头函数执行resolve2(),即改写为return new Promise( resolve2 => { new Promise(...) }).then( () => resolve2() )

总结示例一二三

  1. await等价于返回一个promise对象,后续的操作用.then来表示
  2. 如果await转换为promise对象时,对应函数中有返回值,则返回值改写为return一个resolve的promise对象
  3. 如果await转换为promise对象时,对应函数中有返回一个promise对象,则返回值也会改写为return一个resolve的promise对象,但格式应转换为new promise().then()的各式。

示例四

有代码:

javascript 复制代码
async function async1() {
  console.log('async1 start');
  await async2();
  console.log('async1 end');
}
async function async2() {
  console.log('async2 start');
  return new Promise((resolve, reject) => {
    resolve();
    console.log('async2 promise');
  })
}

console.log('script start');
setTimeout(function() {
  console.log('setTimeout');
}, 0);  
async1();
new Promise(function(resolve) {
  console.log('promise1');
  resolve();
}).then(function() {
  console.log('promise2');
}).then(function() {
  console.log('promise3');
});
console.log('script end');

则可根据总结转换为:

javascript 复制代码
function async1() {
  console.log('async1 start')
  return new Promise(resolve => {    // async 函数返回一个Promise对象,由async2()得到的Promise对象的resolve来触发自己的resovle
    async2().then(v => resolve(v))
  }).then(()=> {
    console.log('async1 end')
  })
}

function async2() {  
  console.log('async2 start')
  return new Promise(resolve2 => { // 返回一个新的Promise对象,由原来async函数里return的Promise对象的resovle来触发自己的resolve
    new Promise((resolve, reject) => {
      resolve()
      console.log('async2 promise')
    }).then(() => resolve2())
  })
}

console.log('script start')
setTimeout(function() {
  console.log('setTimeout')
}, 0)
async1()
new Promise(function(resolve) {
  console.log('promise1')
  resolve()
}).then(function() {
  console.log('promise2')
}).then(function() {
  console.log('promise3')
});
console.log('script end')

对于运行步骤:

  1. 首先输出'script start',之后向下运行
  2. 执行第22行setTimeout,因此将这句话放入宏队列中,宏队列:[22行] ,微队列:[]
  3. 之后进入到async1中,第一句为同步代码,因此输出'async1 start'
  4. 由于第3行为同步代码,因此执行接下来的函数,进入到async2
  5. 进入到async2中,第一句为同步代码,因此输出'async2 start'
  6. 由于第12行为同步代码,因此进入到async2的new promise
  7. 遇到resolve,因此把第16行中then的事件加入到微队列中,此时,宏队列:[22行] ,微队列:[16行]
  8. 之后继续向下执行第15行,输出'async2 promise'。async2原本的promise结束。
  9. 到这里,两个函数中的立即执行的promise就已经完成了,但是实际上代码才走到第24行,还要继续推进第25行的promise
  10. 进入到26行中,第一句为同步代码,因此输出'promise1'。
  11. 由于27行遇见了resolve,所以28行中then的func被激活,因此,宏队列:[22行] ,微队列:[16行,29行]
  12. 25-32行的promise函数立即执行的部分已经结束,那么则开始执行第33行'script end'。
  13. 至此,所有的同步代码已经结束。下面开始运行微队列中的代码。
  14. 首先运行第16行,执行resolve2(),导致整个async2被solve,因此第4行中的then加入了任务队列,宏队列:[22行] ,微队列:[29行,4行]
  15. 之后运行第29行,输出'promise2 ',导致第30行加入了任务队列,宏队列:[22行] ,微队列:[4行,31行]
  16. 之后运行第4行,第4行结束后,导致async自己的promise状态结束,它的then进入队列,即宏队列:[22行] ,微队列:[31行,第6行]
  17. 之后运行第31行,输出'promise3',25-32行的事件结束。
  18. 之后运行第6行,输出'async1 end'。微队列所有任务结束。
  19. 结束后运行宏队列任务,输出'setTimeout'。所有任务结束。

因此结果为:

sql 复制代码
script start
async1 start
async2 start
async2 promise
promise1
script end
promise2
promise3
async1 end
setTimeout
相关推荐
拾光拾趣录2 分钟前
CSS 深入解析:提升网页样式技巧与常见问题解决方案
前端·css
莫空00003 分钟前
深入理解JavaScript属性描述符:从数据属性到存取器属性
前端·面试
guojl3 分钟前
深度剖析Kafka读写机制
前端
FogLetter4 分钟前
图片懒加载:让网页飞起来的魔法技巧 ✨
前端·javascript·css
Mxuan4 分钟前
vscode webview 插件开发(精装篇)
前端
Mxuan5 分钟前
vscode webview 插件开发(交付篇)
前端
Mxuan7 分钟前
vscode 插件与 electron 应用跳转网页进行登录的实践
前端
拾光拾趣录7 分钟前
JavaScript 加载对浏览器渲染的影响
前端·javascript·浏览器
Codebee7 分钟前
OneCode图表配置速查手册
大数据·前端·数据可视化
然我8 分钟前
React 开发通关指南:用 HTML 的思维写 JS🚀🚀
前端·react.js·html