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
相关推荐
珹洺42 分钟前
从 HTML 到 CSS:开启网页样式之旅(三)—— CSS 三大特性与 CSS 常用属性
前端·javascript·css·网络·html·tensorflow·html5
T^T尚5 小时前
uniapp H5上传图片前压缩
前端·javascript·uni-app
出逃日志6 小时前
JS的DOM操作和事件监听综合练习 (具备三种功能的轮播图案例)
开发语言·前端·javascript
XIE3926 小时前
如何开发一个脚手架
前端·javascript·git·npm·node.js·github
GISer_Jing6 小时前
React渲染相关内容——渲染流程API、Fragment、渲染相关底层API
javascript·react.js·ecmascript
山猪打不过家猪6 小时前
React(五)——useContecxt/Reducer/useCallback/useRef/React.memo/useMemo
前端·javascript·react.js
前端青山6 小时前
React事件处理机制详解
开发语言·前端·javascript·react.js
科技D人生6 小时前
Vue.js 学习总结(14)—— Vue3 为什么推荐使用 ref 而不是 reactive
前端·vue.js·vue ref·vue ref 响应式·vue reactive
对卦卦上心6 小时前
React-useEffect的使用
前端·javascript·react.js
练习两年半的工程师6 小时前
React的基本知识:事件监听器、Props和State的区分、改变state的方法、使用回调函数改变state、使用三元运算符改变state
前端·javascript·react.js