限时10分钟,你会怎么实现这段async/await代码?
🧑💻 写在开头
点赞 + 收藏 === 学会🤣🤣🤣
本文用于记录在React课程中学习时,课程中留下的一个关于
async/await
原理的思考题(默认读者熟悉Promise
)
思考题
这个思考题就是:请将以下async/await
代码,换一种方式实现,保证异步等待功能和输出顺序:
function delay(ms, data) { return new Promise(resolve => setTimeout(resolve, ms, data)); } const func = async() => { const data = await delay(2000, 'A'); console.log(data); const res = await delay(2000, 'B'); console.log(res); }; func();
这里可以先暂停去实现一下,以下内容从async/await
基本知识开始。
async/await基本介绍
async/await
是一种以更舒服的方式使用promise
的特殊语法,让异步逻辑更加简洁可读,避免promise
的链式写法。
async
首先来介绍async
,该关键字代表函数总是返回promise
,返回的promise
有resolved
的情况和rejected
的情况:
resolved
情况如下:- 若函数返回了值,则该值会被
Promise.resolve
包装,被解决的值就是该函数返回的值 - 若函数没有返回值,则
promise
中被解决的值为undefined
- 若函数返回了值,则该值会被
// 返回值 const func = async () => { return 1; } // 控制台打印:Promise {<fulfilled>: 1} 'func' console.log(func(), 'func'); // ------------------------------------------------------ // 没有返回 const func1 = async () => { } // 控制台打印:Promise {<fulfilled>: undefined} 'func1' console.log(func1(), 'func1');
rejected
情况:- 返回错误或是抛出错误,会导致这个
promise
为rejected
- 返回错误或是抛出错误,会导致这个
// ---------------------------返回错误--------------------------- const func = async () => { return new Error('error'); } // 控制台打印:Promise {<fulfilled>: Error: error console.log(func(), 'func') // ---------------------------抛出错误--------------------------- const func1 = async () => { throw new Error('error'); } // 控制台打印:Promise {<fulfilled>: Error: error console.log(func1(), 'func1')
await
与async
配对使用的就是await
,并且await
只能在async
函数内工作,作用是等待promise
完成并返回结果,这里也分resolved
的情况和rejected
的情况:
resolved
情况:promise
的resolved
情况,被解决的值作为await
表达式的值
const func = async () => { // await表达式的值就是被解决值'done',然后被赋值给data const data = await Promise.resolve('done'); }
rejected
情况:promise
的rejected
情况,如果不使用try/catch
捕获,则语句(1)等同于语句(2)的效果,都会抛出错误
const func = async () => { // 控制台:Uncaught (in promise) error const data = await Promise.reject('error'); (1) throw 'error'; (2) }
关键点
熟悉async/await
之后,就是要准备实现它了;在实现它之前,不妨将目前的特点总结一下:
- 处理的是
Promise
- 能够暂停函数执行
- 能够等待
Promise
解决之后,取出解决值,恢复函数执行
纵观以上的特点,关键点就在于函数的暂停和恢复执行,只要解决它,就能够实现async/await
一样的效果;
查阅资料能发现,在JavaScript
中有一个能够实现函数的暂停与执行的,那就是Generator(生成器)
,所以接下来先了解一下Generator
的基本语法。
Generator简介
Generator
:译为生成器,是ECMAScript 6
新增的一个极为灵活的结构,拥有在一个函数块内暂停和恢复代码执行的能力;基础代码示例如下:
const func = function* (){ yield 1; yield 2; yield 3; } const iterator = func(); iterator.next(); // {value: 1, done: false} iterator.next(); // {value: 2, done: false} iterator.next(); // {value: 3, done: false} iterator.next(); // {value: undefined, done: true}
Generator
有以下特点:
- 声明生成器函数需要使用
function* 函数名()
语法,其实function *函数名()
也可以,因为是函数的特殊语法,所以建议使用前者*
靠近function
的写法 - 生成器函数被调用的时候,函数并不会执行,而是返回一个生成器实例
Generator
是Iterator的子类,所以生成器实例具有迭代器的特性- 生成器实例具有
next、return、throw
方法,其主要方法就是next
;当next
被调用时,会恢复函数执行,执行到最近的yield
,然后暂停,并将yield
后的结果返回到外部,也就是next
调用后的value
值 yield
既可以产出值,也可以输入值;给next
方法传入的值,作为上一个yield表达式
的值
接下来看一个使用next
传入值,yield
接收值的例子,也请思考一下打印结果:
const func = function* () { console.log(1); const data = yield 2; console.log(data); yield 4; } const it = func(); console.log(it.next()); console.log(it.next(3)); console.log(it.next());
可能这里的打印顺序以及逻辑处理,对之前没有接触过生成器知识的朋友有点不知所以,接下来,我来对代码的执行做一个解释(这里用「」
代表行数,例如:「8」
表示第8行):
- 执行
「8」
:执行生成器函数,生成生成器实例,此时函数内部并未执行 - 执行
「10」
:- 先调用
next
方法,函数开始执行 - 执行
「2」
,打印1 - 执行
「3」
,遇到yield 2
,暂停执行,返回内容 - 执行
「10」
,打印{value: 2, done: false}
- 先调用
- 执行
「11」
:- 先调用
next
方法,函数从上一次暂停处「3」
恢复执行 - 执行
「3」
,next中的参数作为yield 2
表达式的值;data被赋值为3 - 执行
「4」
,打印3 - 执行
「5」
,遇到yield 4
,暂停执行,返回内容 - 执行
「11」
,打印{value: 4, done: false}
- 先调用
- 执行
「12」
:- 先调用
next
方法,函数从上一次暂停处恢复执行 - 无执行内容,迭代结束
- 执行
「12」
,打印{value: undefined, done: true}
- 先调用
实现
思路
经过以上的步骤,对Generator
的暂停和执行的特点有了认识,现在来讲解一下实现思考题的思路。
观察之前的这段代码:
const func = function* () { console.log(1); const data = yield 2; console.log(data); yield 4; } const it = func(); console.log(it.next()); console.log(it.next(3)); console.log(it.next());
可以发现「3」
就比较类似业务代码中的const {data} = await API.xxx()
形式,两者都有等待后表达式的值赋值给左侧的特点;
- 等待后赋值
关键点就在这个“等待后赋值”
上,将上面代码改造为的让data
等待一会儿再被赋值,如下:
const func = function* () { console.log(1); const data = yield 2; console.log(data); yield 4; } const it = func(); console.log(it.next()); // 等待3s再执行 setTimeout(() => { console.log(it.next(3)); console.log(it.next()); }, 3000);
以上代码让3s之后再执行next(3)
给data
赋值,也就是再被赋值之前,操作空间很大,完全可以等待一些事件完成之后再调用next(3)
将值传入函数内部,且让函数内部继续执行。
如果读者的思路一直跟到这里,那么我相信读者对如何用Generator
和Promise
实现async/await
已经有了一些思路了,不妨先去动手试试,再来看下面的具体代码。
具体代码
那么用Generator
和Promise
实现文章开头的思考题,如下所示:
function delay(ms, data) { return new Promise(resolve => setTimeout(resolve, ms, data)); } const func = function* () { const data = yield delay(2000, 'A'); console.log(data); const res = yield delay(2000, 'B'); console.log(res); } let p1, p2, it = func(); // 接收第一个Promise p1 = it.next().value; p1.then((res) => { // 给data赋值,接收第二个Promise p2 = it.next(res).value; p2.then((res) => { // 执行到最后 it.next(res); }); });