异步非全解 async、await
大纲链接 §
[toc]
了解async和await的基本使用 ⇧
async/await是 ES8 (ECMAScript 2017)引入的新语法,用来简化 Promise 异步操作
async和await是 Generator 函数的语法糖使用关键字 async 来修饰 function 或者箭头函数
async() => {}在函数内部使用 await 来表示 等待异部操作的结果,用变量来接收
在
async/await出现前,开发者只能通过 链式 .then() 的方式处理 Promise 异步操作1 2 3 4 5 6 7 8 9 10 11 12 13 14import thenFs from 'then-fs' thenFs.readFile('./files/1.txt', 'utf8') .then(rt1 => { console.log(rt1) return thenFs.readFile('./files/2.txt', 'utf8') }) .then(rt2 => { console.log(rt2) return thenFs.readFile('./files/3.txt', 'utf8') }) .then(rt3 => { console.log(rt3) }).then() 的链式调用解决了回调地狱的问题
.then() 的链式调用缺点: 代码仍旧冗余、阅读性差,不易理解
async/await的基本使用 ⇧
|
|
- 在一个方法返回的 Promise 实例对象前加
await可以直接返回异步的结果- 例如直接获取读取文件的内容
const r1 = await thenFs.readFile('./files/1.txt', 'utf8')
await的外层函数前必须有async修饰- 不再需要使用
.then()的方法拿到异步结果 async/await可以使异步代码在形式上更接近于同步代码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;(async function testAsync() { const p3 = Promise.resolve(3) p3 .then(data => { console.log("p3data", data) }) // 第一种 await 后面是个Promise对象 const data1 = await p3 console.log("data1", data1) // 第二种 await 后面是个普通的值 const data2 = await 4 // await Promise.resolve(4) // 自动封装成 promise 对象 console.log("data2", data2) // 第三种 await 后面是个函数 const data3 = await test1() console.log("data3", data3); } )() ;(async () => { const a = await (() => {console.log('hi')} ) console.log(typeof a) // function console.log('a', a) // () => {console.log('hi')} })();
参考
async/await注意事项 ⇧
async 函数的 返回值也是一个 Promise 实例
- 使用
async声明一个 异步函数,并总是 返回一个 promise 对象,其他值将自动被包装在一个 resolved 的 promise 中- 因此可以直接 return 变量或值,无需手动显式地调用
Promise.resolve()进行转换 - 使用 return 关键字返回了值,就会被
Promise.resolve()包装成一个 promise 对象 - 如果不显式地 return 一个值,默认返回
Promise.resolve(undefined) - 被
Promise.resolve(undefined)包装后得到 一个fulfilled状态的Promise {<fulfilled>: undefined}实例对象
- 因此可以直接 return 变量或值,无需手动显式地调用
调用
.then()可以得到返回值1 2 3 4 5 6 7 8 9 10 11 12async function f() { return 1; } f().then(alert); // 1 // 也可以显式地返回一个 promise,结果是一样的 async function f() { return Promise.resolve(1); } f().then(alert); // 1async 确保了函数返回一个 promise,也会将非 promise 的值包装进 Promise 里去
1 2 3 4 5 6 7 8 9 10 11 12async function foo() { console.log(1) return 3 } // 返回的promise调用 then 方法 foo().then(console.log) ;(async () => { const a = await foo() console.log(a) })();
await
async总是与await一起使用- 并且
await只能在 async 修饰的异步函数体内 使用 - 不能在 顶级作用域 中,如
<script>标签或模块(*P.S. 新特性:从 V8 引擎 8.9+ 版本开始,顶层 await 可以在 模块 中工作)中使用 因为顶级作用域不是一个
async方法1 2 3 4 5 6;(async () => { // 只在 async 函数内工作 const value = await (Promise.resolve(1)); console.log(value) return value })();
- 并且
关键字
await让 JavaScript 引擎等待直到 promise 完成(settle)并返回结果1 2 3 4 5 6 7 8 9 10 11 12 13 14async function f() { const promise = new Promise((resolve/*, reject*/) => { setTimeout(() => resolve("done!"), 1000) }); const result = await promise; // 等待,直到 promise resolve (*) alert(result); // "done!" return result // 相当于 // return Promise.resolve(result) } f(); // 这个函数在执行的时候,“暂停”在了 (*) 那一行 // 并在 promise settle 时,拿到 result 作为结果继续往下执行 // 所以上面这段代码在一秒后显示 “done!” f().then(res => {console.log(res)}) // done!
async函数执行的过程
- 未遇到
await的代码同步执行 - 一旦遇到
await就会先在内部 得到 pending(进行中) 状态的 Promise 实例 - 将第一个
await表达式,看做一个微任务,推送到微任务队列,等待调用 - 第一个
await表达式后的所有语句统一看做这个 微任务中的(宏/微/同步)任务(嵌套) - 并且暂停执行
await表达式后的所有语句 - 等到主线程中同步任务执行完毕,开始抓取微任务队列的任务执行
- 当执行到此
await表达式时- 如果
await表达式后是同步代码,则紧跟await表达式一起执行 - 如果
await表达式后是异步代码,再将await表达式之后的异步代码,按照微/宏任务,分别推送到微/宏任务队列,等待依次调用
- 如果
await是个运算符,用于组成表达式,它会 阻塞 后面的代码,即暂停执行await表达式后的所有语句
async函数执行的结果
- 如果
await调用异步函数后,等到的是Promise对象- 则 得到其 resolve 值
- 否则,会将非
Promise对象得值进行Promise.resolve()包装
- 执行
async函数,返回的都是一个Promise对象- 该
Promise对象 最终resolve的值就是在函数中return的内容
- 该
async语句 全部执行完毕 且 无错误 的情况下- 则返回的 Promise 实例会变为已成功
- 否则会变为已失败
async函数其他注意点
async内部不用写.then及其回调函数- 这会减少代码行数,也避免了代码书写形式上的嵌套,但运行机制上还是嵌套
- 所有异步调用,可以写在 同一个代码块 中,无需定义多余的中间变量
- 比如
.then()链式调用中的resolve和reject
- 比如
- 一个
await就是一层嵌套,嵌套包裹await表达式后面的代码,一层嵌套就是 一整个微任务,代码形式上是 同步- 相似的,一个
.then()就是一层回调嵌套,就是 一整个微任务,只不过代码形式上是 链式调用
- 相似的,一个
async/await使 异步代码,在形式上,等同于同步代码- 好比将套娃展开陈列,而回调函数则是层层嵌套
- 在
async方法中,首个 await 之前 的代码会 同步执行 - 首个
await之后的代码包括表达式内的JS代码,会 异步执行 async/await是建立在Promises上的,不能被使用在普通回调函数以及节点回调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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46async function foo() { console.log(2) console.log(await Promise.resolve('8-1')) console.log(await Promise.resolve('8-2')) console.log(9) } async function bar() { console.log(4) console.log(await '6-1') console.log(await '6-2') console.log(7) } console.log(1) foo() console.log(3) bar() console.log(5) /* console.log(1) // sync code console.log(2) // sync // add to microtask queue // [{console.log(await Promise.resolve('8-1)')...}] console.log(3) // sync console.log(4) // sync // add to microtask queue // [{console.log(await Promise.resolve('8-1'))...}, {console.log(await '6-1')...}] console.log(5) // sync code all over // grab one microtask from microtask queue console.log('8-1') // invoke microtask {console.log(await Promise.resolve(8-1))...} // add to microtask queue // microtask queue [{console.log(await 6-1)...}, {console.log(await Promise.resolve(8-2))...}] // grab one microtask from microtask queue console.log('6-1') // invoke microtask {console.log(await Promise.resolve(6-1))...} // add to microtask queue // microtask queue [{console.log(await Promise.resolve(8-2))...}, {console.log(await 6-2)...}] // grab one microtask from microtask queue console.log('8-2') // invoke microtask {console.log(await Promise.resolve(8-2)), console.log(9)} console.log(9) // microtask queue [{console.log(await 6-2)...}] // grab one microtask from microtask queue console.log('6-2') // invoke microtask {console.log(await 6-2), console.log(7)} console.log(7) */await 实际上会暂停函数的执行,直到 promise 状态变为 settled
然后以 promise 的结果继续执行
这个行为不会耗费任何 CPU 资源,因为 JavaScript 引擎可以同时处理其他任务:执行其他脚本,处理事件等
相比于 promise.then,它只是获取 promise 的结果的一个更优雅的语法,同时也更易于读写
以读取文件为例:
|
|
不要是异步就要去用 async/await,要满足两点,优势才能体现:
- 1️⃣要执行多个异步任务
- 2️⃣并且这些个任务都有前后依赖的关系(比如后者依赖前者的结果)
- 主要就是用来解套,让代码更清晰
- 没有前后依赖那不如用 promise 了
假设有一个 getJSONData 方法,返回一个 promise 对象
该 promise对象 会被 resolve 为一个 JSON 对象
调用该方法,输出得到的 JSON 对象,最后返回”done”。
|
|
- 不需要为 .then 编写一个匿名函数来处理返回结果
(resolve) => {}
async/await 错误处理
|
|
- try/catch 不能捕获 成功回调resolve 中 JSON.parse 抛出的异常
改用 async/await
|
|
- 此处catch代码块可以捕获JSON.parse抛出的异常
在条件分支中进一步解套 Promise
|
|
- 请求数据,然后根据返回数据中的某些内容决定
- 是直接返回这些数据
- 还是继续请求更多数据
使用 async/await 改写
|
|
promise 结果逐层依赖,省略中间值
请求 promise1,使用它的返回值请求 promise2,最后使用这两个 promise 的值请求 promise3
|
|
如果 promise3 没有用到 value1,则可以把这几个 promise 改成嵌套的模式
也可以把 value1 和 value2 封装在一个 Promise.all 调用中以避免深层次的嵌套
|
|
- 这种方式为了保证可读性而牺牲了语义
- 除了避免嵌套的 promise,没有其它理由要把 value1 和 value2 放到一个数组里
async
|
|
判断执行顺序
第一题
|
|
第二题
|
|
其他使用示例 ⇧
配合 Promise.all 使用 ⇧
|
|
- Promise 是并发的,但如你一个一个地等待它们,会太费时间
- Promise.all() 可以节省很多时间
为何说 async 函数是语法糖
async 函数的实现,其实就是将 Generator 函数和自动执行器,包装在一个函数里。
- 参考 非前端阮一峰写的 《async 函数的含义和用法》 一文
- 转码器 Babel 已经支持,转码后就能使用
async 相较于 Promise 的优势
- 相较于 Promise,能更好地处理 then 链
- 中间值
- 调试
Promise
- ECMAScript 6 新增的引用类型 Promise
- 可以通过 new 操作符来实例化
- 创建新期约时需要传入执行器(executor)函数作为参数
- 三个状态,分别是pending(执行中)、success(成功)、rejected(失败)
- 无论
resolve()和reject()中的哪个被先调用,状态转换都不可撤销- 后调用的方法继续修改状态会静默失败
- 为避免期约卡在待定状态,可以添加一个定时退出功能
- 比如,可以通过 setTimeout 设置一个 10000 秒钟后无论如何都会拒绝期约的回调
const p = new Promise((resolve, reject) => { /*...*/ setTimeout(reject, 10000);}- 10 秒后调用 reject() // 执行函数的逻辑
相较于 Promise,能更好地处理 then 链
假设有三个表示处理一系列连续步骤的函数
stepFn.js
|
|
现在用 Promise 方式来实现这三个步骤的处理
step.js
|
|
用 async/await 来实现
|
|
- 结果和之前的 Promise 实现是一样的
- 但代码更清晰,和同步代码一样
中间值
仍然是三个步骤,但每一个步骤都需要之前所有步骤的结果
Promise的实现看着很晕,传递参数太过麻烦
|
|
用 async/await 来写:
|
|
更易调试
较 Promise 更易于调试
- 因为没有代码块,所以不能在一个返回的箭头函数中设置断点
- 如果在一个 .then 代码块中使用调试器的步进(step-over)功能,调试器并不会进入后续的 .then 代码块
因为调试器只能跟踪同步代码的每一步
1 2 3 4 5 6 7const makeRequest = () => { return callOnePromise() .then(() => callOnePromise()) // step-over 将跳过以下的 .then() .then(() => callOnePromise()) .then(() => callOnePromise()) }
使用 async/await,就不必再使用箭头函数
可以对 await 语句执行步进操作,就好像他们都是普通的同步语句一样
1 2 3 4 5const makeRequest = async () => { await callOnePromise() await callOnePromise() await callOnePromise() }
JavaScript的异步编写方式,从 回调函数 到 Promise、Generator 再到 Async/Await。表面上只是写法的变化,但本质上则是语言层的一次次抽象。让我们可以用更简单的方式实现同样的功能,而不需要去考虑代码是如何执行的
无法替代 Promise 的场景
前端进行并发请求,都请求完执行操作A; 否则执行操作B。
- 这种情况就是用的 Promise.all()
- Async/Await 对 Generator是取代关系,但不能完全取代 Promise
- 处理并发请求(只是处理并发请求,而不是并发)的时候,可采用 axios.all(),拿到的最终结果还是个Promise
- 然后再去业务层处理A,在这时与 Promise.all() 的效果相同
用 await/async 是串行,用 Promise.all() 是处理并发
1 2 3 4 5 6 7 8 9 10 11 12const serial = async () => { const promiseA = promiseFnA() const promiseB = promiseFnB() try { // do something on valueA and valueB const valueA = await promiseA const valueB = await promiseB } catch (error) { throw error } }
串行/并发
|
|
- 当调用
promise = delay(2000)的时候,已发起 delay 的异步处理 setTimeout - 此时 promise 的状态是 pending
- await 需要等 promise 状态变成 resolved(rejected则抛出异常),才会继续后面的操作,可以理解为阻塞
- 如果是后一次的 delay 调用是等待前一次 resolve 才发起的,那么两次 delay 表现出来是串行的
- 在 await 之前就已经调用了两次 delay,所以表现出来的就是并发
- 就像调用 Promise.all,要求传入的参数是promise对象数组
- 调用
Promise.all([delay(2000), delay(2000)]) - 其实相当于分两步走:
const p1 = delay(2000) // 已经发起异步处理const p2 = delay(2000) // 已经发起异步处理Promise.all([p1, p2]);
- step2 其实在传给 Promise.all 之前,异步处理已经都发起了
- 所以并发并不是 Promise.all 处理的
Promise.all 只是做了类似如下操作:
1 2 3 4 5 6 7async all(promiseList) { const result = []; for(const promise of promiseList) { result.push(await promise); } return result; }
示例
|
|
- await 需要等待后面的 promise 实例是状态为成功时,才会执行之后的代码
- 首先 当前上下文中,await 之后的代码都是异步微任务 @aw
- 如果已经知道 await 后面的实例状态是成功的话
- 则 @aw 直接放在 Event Queue 中,等待执行即可
- 如果 await 后面实例状态是失败的话
- 则 @aw 在 Web API 中永远不会进入到 Event Queue 中,因为永远不会执行
- 如果暂时还不知道是成功还是失败,则 @aw 先放置在 Web API 中
- 等到知道实例状态是成功后,再挪至到 Event Queue 中等待执行
async/await 的错误使用
|
|
几个错误点
pizzaData与drinkData之间没有依赖- 顺序的 await 会最多让执行时间增加一倍的
getPizzaData函数时间 - 因为 getPizzaData 与 getDrinkData 应该并行执行
- 顺序的 await 会最多让执行时间增加一倍的
正确的做法应该是先同时执行函数,再 await 返回值,这样可以并行执行异步函数
1 2 3 4 5 6 7;(async () => { const pizzaPromise = selectPizza(); // sync call const drinkPromise = selectDrink(); // sync call await pizzaPromise; // async call await drinkPromise; // async call orderItems(); // async call })();使用 Promise.all 并行处理
1 2 3 4 5 6 7;(async () => { Promise.all([ selectPizza(), selectDrink() ]) .then(orderItems); // async call })();async/await 虽然层级形式上一致了,但逻辑上还是嵌套关系,转换还是隐式的
async/await 只能实现一部分回调函数支持的功能,也就是仅能方便应对 层层嵌套 的场景
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23// 比如两对回调 a(() => { b(); }); c(() => { d(); }); // 性能低效的执行方式 await a(); await b(); await c(); await d(); // 翻译成回调 a(() => { b(() => { c(() => { d(); }); }); });原始代码中,函数 c 可以与 a 同时执行
async/await 语法在 b 执行完后,再执行 c
async/await 性能地狱
完全脱离无关依赖
|
|
- 大部分场景代码是非常复杂的,同步与 await 混杂在一起
- 不用一昧追求 async/await 语法,在必要情况下适当使用回调,是可以增加代码可读性的
参考文章
- JavaScript, Promise creation and Promise chains
- 使用 Promise MDN
- Promise MDN
- 现代 JavaScript 教程 / JavaScript 编程语言 / Promise,async/await / 简介:回调
- 现代 JavaScript 教程 / JavaScript 编程语言 / Promise,async/await / Promise
- 现代 JavaScript 教程 / JavaScript 编程语言 / Promise,async/await / Promise 链
- 现代 JavaScript 教程 / JavaScript 编程语言 / Promise,async/await / 使用 promise 进行错误处理
- 现代 JavaScript 教程 / JavaScript 编程语言 / Promise,async/await / Promise API
- 现代 JavaScript 教程 / JavaScript 编程语言 / Promise,async/await / Promisification
- 现代 JavaScript 教程 / JavaScript 编程语言 / Promise,async/await / 微任务(Microtask)
- 现代 JavaScript 教程 / JavaScript 编程语言 / Promise,async/await / Async/await
- 现代 JavaScript 教程 / JavaScript 编程语言 / Generator,高级 iteration / 异步迭代和 generator
- Wangdoc 异步操作概述
- 阮一峰 ES6 Promise 对象
- axios 中文文档
- axios/axios
- https://axios-http.com/
- Promise源码
- PromiseA+规范
- ecma262: Promise Abstract Operations
相关文章
- JS异步编程模型与Promise初探
- 使用 Promise 时的5个常见错误
- axios, ajax和fetch的比较
- 如何中断Promise?
- Promise深度解析10个常用模块
- 前端 Promise 常见的应用场景
- async/await 之于 Promise,正如 do 之于 monad(译文)
- 理解 JavaScript 的 async/await
- 精读《async/await 是把双刃剑》