深入聊聊async await

1,460 阅读3分钟

深入聊聊async await

先来简单说说async await其实就是可以通过同步的方式,编写异步代码,上篇中有用Promise加载一张图片,这里也再次改写这个例子,结合async await实现

写法

function loadImg(src) {
  const p = new Promise((resolve, reject) => {
    const img = document.createElement('img')
    img.onload = () => {
      resolve(img)
    }
    img.onerror = () => {
      const err = new Error('图片加载失败')
      reject(err)
    }
    img.src = src
  })
  return p
}

const url = 'https://assets.huabyte.com/blog/image/cover1.jpg'

;
(async function () {
  try {
    const img = await loadImg(url) // await相当于then
    console.log(img);
  } catch (ex) {
    console.log(ex);
  }
})()

和Promise的关系

可以先来看看这段代码打印结果:

async function fn2() {
  return new Promise(() => {})
}
console.log(fn2());

async function fn1() {
  return 100
}
console.log(fn1());

1643810935135.png

通过以上的例子或许我们可以总结一下:

  • 执行async函数,返回的时Promise对象(实际async await就是语法糖)
  • await相当于Promise的then
  • try...catch可以捕获异常,代替了Promise的catch

异步的本质

async await写是同步语法,但本质还是异步调用

async function async1 () {
  console.log('async1 start') // 2
  await async2()
  console.log('async1 end') // 5 关键在这一步,它相当于放在 callback 中,最后执行
}

async function async2 () {
  console.log('async2') // 3
}

console.log('script start') // 1
async1()
console.log('script end') // 4

for...of的用法

一般循环遍历我们会用forfor...in,而这类遍历一般也是用于常规同步遍历操作,我们这里要说的就是for...of常用于异步的遍历。

// 定时算乘法
function multi(num) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(num * num)
        }, 1000)
    })
}

// // 使用 forEach ,是 1s 之后打印出所有结果,即 3 个值是一起被计算出来的
// function test1 () {
//     const nums = [1, 2, 3];
//     nums.forEach(async x => {
//         const res = await multi(x);
//         console.log(res);
//     })
// }
// test1();

// 使用 for...of ,可以让计算挨个串行执行
async function test2 () {
    const nums = [1, 2, 3];
    for (let x of nums) {
        // 在 for...of 循环体的内部,遇到 await 会挨个串行计算
        const res = await multi(x)
        console.log(res)
    }
}
test2()

基本说完async await的用法和异步本质后,我们再来看一个例子:

console.log(100)
setTimeout(() => {
    console.log(200)
})
Promise.resolve().then(() => {
    console.log(300)
})
console.log(400)

可以去试试打印的顺序,发现是100 400 300 200,这里就会产生一个疑问了,setTimeoutPromise then都是异步,为什么执行顺序和预期不太一样呢?

微任务和宏任务

在前端层面来讲,我们可以有以下几个说法:

  • 宏任务:如setTimeout setInterval DOM事件
  • 微任务:如Promiseasync await
  • 微任务比宏任务执行更早

要解释这些呢我们不得不再次说回event loop机制,因为js中DOM渲染和js代码执行也是一条线程,在每一次call stack结束,都会先触发DOM渲染(不一定非得渲染,只是询问是否需要渲染),然后在进行event loop。具体可以通过以下图示来大概理解

1643812918650.png

  • 宏任务:DOM 渲染后再触发
  • 微任务:DOM 渲染前会触发

具体分析可以使用代码验证

const $p1 = $('<p>一段文字</p>')
const $p2 = $('<p>一段文字</p>')
const $p3 = $('<p>一段文字</p>')
$('#container')
            .append($p1)
            .append($p2)
            .append($p3)

console.log('length',  $('#container').children().length )
alert('本次 call stack 结束,DOM 结构已更新,但尚未触发渲染')
// (alert 会阻断 js 执行,也会阻断 DOM 渲染,便于查看效果)
// 到此,即本次 call stack 结束后(同步任务都执行完了),浏览器会自动触发渲染,不用代码干预

// 另外,按照 event loop 触发 DOM 渲染时机,setTimeout 时 alert ,就能看到 DOM 渲染后的结果了
setTimeout(function () {
    alert('setTimeout 是在下一次 Call Stack ,就能看到 DOM 渲染出来的结果了')
})

深入思考关于为什么会有这种处理区别,可以这么理解:

  • 微任务:ES 语法标准之内,JS 引擎来统一处理。即,不用浏览器有任何关于,即可一次性处理完,更快更及时。
  • 宏任务:ES 语法没有,JS 引擎不处理,浏览器(或 nodejs)干预处理。

面试题练习

判断下列打印顺序:

async function async1() {
  console.log('async start'); // 2
  await async2()
  console.log('async1 end'); // 相当于then回调 微任务 6
}

async function async2() {
  console.log('async2'); // 3
}
console.log('script start'); // 1

setTimeout(function () { // 宏任务
  console.log('setTimeout'); // 8
}, 0)

async1()

new Promise(function (resolve) {
  console.log('promise1'); // 4
  resolve()
}).then(function () { // 微任务 
  console.log('promise2'); // 7
})

console.log('script end'); // 5