async/await = Promise + Generator:ES6异步的三重境界

·  阅读 385

前言

异步是JavaScript中的重要内容,异步的主要作用是把耗时间的代码“放一边”,让其不阻塞同步代码,等到异步代码出结果了,再通过回调函数来处理其结果。但是在实际工作中使用异步的时候有一个重要的问题,即多个异步代码的顺序问题。

假如我们要做多个Ajax请求,第二个请求的参数是一个请求的结果,这就要求异步代码之间需要有序,保证第二个请求在第一个请求执行完之后再执行。其实细考虑起来这是个略显诡异的需求,也就是使异步代码“同步化”。不过ES6的许多新特性使得这一操作变得更为简化。我们可以用Promise,Generator,或async/await分别实现上述“同步化异步代码”的需求。三者对照,可以更好地将ES6中的异步理解。

话不多说,上需求。首先模拟一个取数据的异步操作,返回的数据类型是Promise(比较典型的用法),代码如下

function getData(params) {
  return new Promise((resolve) => {
    // 模拟ajax
    setTimeout(() => {
      resolve("result: " + params);
    }, 500);
  });
}
复制代码

我们的需求是,先用getData取到数据,再对取到的数据进行作一次getData,两次操作需要有序,不然第二次的参数就空了。------以上是实习面试腾讯时的一道题,当时初涉前端,连题都没搞懂😭。

后面将用三种方法实现这个需求,领略ES6异步三境界。

一重境界:用Promise使异步代码有序

Promise是为异步而生的,用.then可以大大缓解以前“回调地狱”的情况。直接上代码,详情见注释。

getData("start").then((res) => {
  // 取到结果 result: start
    console.log("stage1", res);
    getData(res).then((res) => {
      // 取到结果 result: result: start
        console.log("stage2", res);
        console.log("all Done!!");
    })
})
复制代码

打印结果如下:

stage1 result: start
stage2 result: result: start
all Done!!
复制代码

如上所见,简单易懂,但是多次异步写起来还是有点难受,“回调地狱”的问题没有彻底解决。不过没关系,后面还有新办法。

二重境界:用async/await简化Promise

其实async/await可以算是上述问题的最终形态,本来应该放在最后说的,但相比Generator而言,async/await更好简洁,也更常用,于是便放在第二重了,毕竟Generator理解起来确实有点费劲。

async/await两兄弟,一个一个来介绍。

async:放于某函数function关键字之前时,可以将该函数的返回包裹上一个成一个fulfilled状态的Promise对象。看代码:

async function get1() {
  return 1;
}

get1(); // 返回结果为Promise {<fulfilled>: 1}
复制代码

await:你async能包一个promise,我await就能解开一个promise😊。上面代码别删,接着用。

await get1(); // 结果为 1
复制代码

两兄弟最简单的功能是对立的,但二者结合起来却能轻松地完成“异步代码同步化”的问题。先上代码:

// async/await 写法,
async function asy_fn() {
  // get到数据之前,下行代码下面的其他代码都得等着(wait)
  let stage1 = await getData("start");
  console.log("stage1", stage1);
  let stage2 = await getData(stage1);
  console.log("stage2", stage2);
  return "all Done!!";
}

let res_asy = asy_fn();
// 因为返回值是Promise,所以最后的结果要then一下再打出来
res_asy.then((res) => {
    console.log(res)
})
复制代码

下面讲点细节:

  1. await只能在async function中起到上面的作用,而且await右边函数的返回值得是promise,setTimeout造成的异步await无效。
  2. await暂停代码时,其左边的赋值也是未进行的,实际上是停在了await右边的函数里。
  3. 因为await产生的异步效应是和promise同级别的,也就是微任务级别。具体细节可见《反复横跳的await与Promise优先级》

三重境界:用Generator和Promise解构async/await

Generator(生成器or迭代器?)是个陌生的家伙,一般业务中用到的比较少(写出来同事看不懂咋办😂),但是他是async/await背后的原理。而且若是能灵活掌握这个知识点的话,可以造出很多东西。本文就把这个当作最后boss了,用这个来理解async/await。

先简述,Generator,通过function*这样的函数声明方式产生一个迭代器函数,执行一下迭代器就产生一个迭代器对象。这个函数里的yield和return是一个个节点,在迭代器上next一下就从一个节点执行到下一个节点。就是会停止,对了,这就是await会停在某行代码上的原理😏。

show me your code,示例如下:

function* gene() {
  yield 1;
  yield 2;
  return 3;
}

let g = gene(); // 生成一个迭代器对象
console.log(g.next());
// { value: 1, done: false }
console.log(g.next());
// { value: 2, done: false }
console.log(g.next());
// { value: 3, done: true }
复制代码

next()产生的结果是{value, done},这个对象里的值value是yield右侧的值或函数返回值,done是布尔值,迭代器是否执行完,即后面是否有其他的yield或return语句

感觉next方法差不多整明白了,其实并没有😏。next函数其实可以传参数,next函数的参数即为上一个yield表达式的返回值(有点绕)。示例如下:

function* gene() {
  let n1 = yield 1;
  console.log(n1);
  let n2 = yield 2; 
  console.log(n2);
  return 3;
}

let g = gene();
console.log(g.next());
console.log(g.next("res for yield 1"));
// yield 函数返回值是下一个next运行时的输入值,
// 每次.next都执行到yield及后面的部分为止,
// 前面的赋值和yield本身都不返回值
console.log(g.next("res for yield 2"));
复制代码

有点难理解,毕竟在下面执行的next里的参数跑到上面去了。但我们把每个next当作独立函数的话会更好理解。在执行到第二个next时,执行的代码其实是:

// console.log(g.next("res for yield 1"));
// 相当于
function next(param) {
  let n1 = param;
  console.log(n1);
  yield 2;
}
复制代码

以块为单位来理解yeild返回值便会比较清晰。

科普完毕,圆规正转,如何用Generator实现“异步代码同步化”。上代码:

function* gen() {
  let stage1 = yield getData("start");
  console.log("stage1", stage1);
  let stage2 = yield getData(stage1);
  console.log("stage2", stage2);
  return "all Done!!";
}

function run(generator, v) {
  let { value, done } = generator.next(v);
  if (!done) {
    value.then((res) => {
      run(generator, res);
    });
  } else {
    console.log(value);
  }
}

let res = run(gen());
复制代码

先实现一个gen迭代器,生成一个迭代器实例放在run里自动化地跑,毕竟一个一个地写next也太二了。

发现没有,现在的用生成器实现方式已经很接近async/await的实现方式了,不过async函数返回的是promise,run返回的是字符串。稍作改变就可以达到相同的效果。所以下面代码就是“如何用Generator和Promise模拟async/await?”,即标题async/await = Promise + Generator

function* gen(stage0) {
  let stage1 = yield getData("start");
  console.log("stage1", stage1);
  let stage2 = yield getData(stage1);
  console.log("stage2", stage2);
  return "all Done!!";
}

function run(generator, v) {
  let { value, done } = generator.next(v);
  return new Promise ((resolve, reject) => { // 包个promise
    if (!done) {
        value.then((res) => {
        return run(generator, res).then(resolve);
        });
    } else {
        return resolve(value); // gen函数return时的处理
    }
  });
}

let res = run(gen());
res.then((res) => {
    console.log(res)
})
复制代码

上述代码可与二重境界中代码等价,这便是async/await背后的实现原理了。于是就有了这种说法。

async/await是一个promise+generator+run函数的语法糖。

后记

ES6异步Generator使用较少,但这又是理解async/await的关键一步,填上这个空缺,认知才够完整。

分类:
前端
标签: