限时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,返回的promiseresolved的情况和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情况:
    • 返回错误或是抛出错误,会导致这个promiserejected
// ---------------------------返回错误---------------------------
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情况:
    • promiseresolved情况,被解决的值作为await表达式的值
const func = async () => {
    // await表达式的值就是被解决值'done',然后被赋值给data
    const data = await Promise.resolve('done');
}
  • rejected情况:
    • promiserejected情况,如果不使用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的写法
  • 生成器函数被调用的时候,函数并不会执行,而是返回一个生成器实例
  • GeneratorIterator的子类,所以生成器实例具有迭代器的特性
  • 生成器实例具有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)将值传入函数内部,且让函数内部继续执行。


如果读者的思路一直跟到这里,那么我相信读者对如何用GeneratorPromise实现async/await已经有了一些思路了,不妨先去动手试试,再来看下面的具体代码。

具体代码

那么用GeneratorPromise实现文章开头的思考题,如下所示:

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/awaitGenerator+Promise语法糖

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

 

热门相关:盖世双谐   校花之贴身高手   美漫大幻想   前夫有毒:1000万夺子契约   带着仓库到大明