限时10分钟,你会怎么实现这段async/await代码?
限时10分钟,你会怎么实现这段async/await代码?
本文将通过对比Generator函数来解释async/await的工作机制。文章结构清晰,从基本概念到具体实现逐步展开,内容详尽且专业,适合有一定JavaScript基础的开发者阅读。
写在开头
本文用于记录在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);
});
});
async/await与Generator/Promise的关系
到这里,思考题的意图就显现出来了;目的就是点出async/await的原理其实就是Generator+Promise;再换一句话描述就是:async/await是Generator+Promise的语法糖。