关于async和await,这里或许有你想知道的

async和await

async和await的实现,要从回调地狱开始说起。我们通过之前的文章已经知道了Promise通过链式调用来解决回调地狱的问题,但是就算使用了then()函数来进行链式调用,代码仍然非常沉重,如果有一个非常复杂的异步流程,也还是需要很多的链式调用进行支撑。那有没有方法可以让异步的写法变得像同步一样呢?

这时候就不得不提一种函数——Generator函数

Generator函数

Generator函数是ES6引入的,也称生成器函数。总体结构如下所示,在函数名的前面会有一个*,但是箭头函数是不可以用来定义生成器函数的。

function * funName() {
    yield ***
    yield ***
}
复制代码

它可以通过yield关键字把函数的执行流挂起,提供了让函数可以分步执行的能力。如果遇到了yield关键字,执行就会停止。

当然,它也有一个next()方法,通过调用这个方法可以让生成器开始执行或者继续执行。

有一个需要注意的地方,生成器对象一开始调用的时候并不会直接去执行,而是会处于suspened状态(暂停执行)。所以需要用next()方法来让它执行。

  function* test() {
    console.log(1);
    return 123;
  }
  var value = test();
  console.log(value);
复制代码

如上面所示,我们定义了一个叫test的生成器函数,函数内部会打印1,同时返回123。因此我们像常规的函数一样去调用这个生成器函数,并且定义了一个叫value的变量去接受这个函数的返回值。

当我们执行后,如下图所示:

image.png

我们发现,test函数执行了,但是内部的console并没有执行,也没有返回123。而test是一个显示suspended的对象。因为生成器对象一开始调用的时候并不会直接去执行,而是会处于suspened状态即暂停执行。所以需要用next()方法来让它执行。

next()

然后我们使用了next(),如下面代码所示:

扫描二维码关注公众号,回复: 14259609 查看本文章
  function* test() {
    console.log(1);
    return 123;
  }
  var value = test();
  console.log(value.next());
  console.log(value);
复制代码

image.png 当我们运行后会发现test函数的代码正常执行了,打印test.next()运行的结果时发现,返回的是一个对象,对象内部有一个done属性和一个value属性,value属性就是return回来的值。

当我们再次打印value变量时,发现变成了一个test对象,状态是closed。

因为,生成器函数实际上没有传统的返回值,调用的时候会生成一个实例化的Generator对象。这个对象有一个next方法,可以通过调用next方法来执行生成器函数内部的代码,直到yield会暂停执行,等待下一次的next(),然后会继续执行到下一个的yield。

而next方法的调用会返回一个对象,对象中存在2个属性,分别是donevalue

  • done:该属性的值是一个布尔值,表示是否可以继续执行next(),如果时true则表示已经执行完毕,否则则表示可以继续执行next()。
  • value:如果done属性的值是false,value通常代表下⼀个yield右侧的值。如果done属性的值是true,那么这个属性的值就是return的值。

yield

我们将代码改造一下,加入yield关键字。

  function* test() {
    console.log(1);
    yield 2;
    yield 3;
    yield 4;
    return 5;
  }
  var value = test();
  // 这时候的value是实例化的Generator函数
  console.log(value);
  // 第一次调用next(),代码执行到第一个yield,value的值是2,done是false
  console.log(value.next());
  // 第二次调用next(),value的值是3,done是false
  console.log(value.next());
  // 第三次调用next(),value的值是4,done是false
  console.log(value.next());
  // 第四次调用next(),value的值是5,done是true
  console.log(value.next());
  // 第五次调用next(),value的值是undefined,done是true
  console.log(value.next());
复制代码

image.png 我们发现,当调用next()的时候,代码会执行到yield时暂停,状态为false,值为yield右侧的值。当代码执行完毕后,则done属性变为true,value会是函数return的值。如果代码执行完毕了继续执行next(),会发现done属性并没有发生变化,但是value属性已经变成了undefined了。

传值问题

  function* test() {
    console.log(1);
    var a = yield 2;
    console.log(a);
    var b = yield 3;
    console.log(b);
    var c = a + b;
    console.log(c);
  }
  var value = test();
  console.log(value);
  const step1 = value.next();
  console.log(step1);
  const step2 = value.next();
  console.log(step2);
  const step3 = value.next();
  console.log(step3);
复制代码

image.png 如上述代码所示,想给a b c赋值,但是通过yield实现了分布执行。但是发现a 和 b都变成了undefined。

这是因为在分步执⾏过程中,我们是可以在程序中对运⾏的结果进⾏⼈为⼲预的,也就是说yield返回的结果和他左 侧变量的值都是我们可以⼲预的。

当我们对代码进行下面这样的改造:

  function* test() {
    console.log(1);
    var a = yield 2;
    console.log(a);
    var b = yield 3;
    console.log(b);
    var c = a + b;
    console.log(c);
  }
  var value = test();
  console.log(value);
  const step1 = value.next();
  console.log(step1);
  const step2 = value.next(step1.value);
  console.log(step2);
  const step3 = value.next(step2.value);
  console.log(step3);
复制代码

image.png 当我们这样子改造后,就能正常赋值了。也就是说next函数执⾏的过程中我们是需要传递参数的,当下⼀次next执⾏的时候我们如果不传递参数,那么本次yield左侧变量的值就变成了undefined,所以我们如果想让yield左侧的变量有值就必须在next中传⼊指定的结果。

用Generator将Promise异步流程同步化

  function* test() {
    var a = yield 1;
    console.log(a);
    var res = yield setTimeout(function () {
      return 6666666666;
    }, 1000);
    console.log(res);
    var res1 = yield new Promise(function (resolve) {
      setTimeout(function () {
        resolve(456);
      }, 1000);
    });
    console.log(res1);
  }
  var generator = test();
  console.log(generator);
  var step1 = generator.next();
  console.log(step1);
  var step2 = generator.next();
  console.log(step2);
  var step3 = generator.next();
  console.log(step3);
  var step4 = generator.next();
  console.log(step4);
复制代码

image.png 如上所示,我们使用Generator函数并结合了异步函数以及Promise,发现对异步的setTimeout无法正确打印出return返回的值,并且输出没有任何延时。而对于Promise来说,能够拿到Promise本身,展开也能发现Promise状态变成了fulfilled,值就是1s后resolve的参数。

因此,我们是不是可以通过递归调用的方式,通过next()来推动函数的执行,用done属性作为是否结束的依据,如果碰到了Promise,就等promise执行完毕了再执行下一步。

  function generatorFunctionRunner(fn) {
    //定义分步对象
    let generator = fn();
    //执⾏到第⼀个yield
    let step = generator.next();
    //定义递归函数
    function loop(stepArg, generator) {
      //获取本次的yield右侧的结果
      let value = stepArg.value;
      //判断结果是不是Promise对象
      if (value instanceof Promise) {
        //如果是Promise对象就在then函数的回调中获取本次程序结果
        //并且等待回调执⾏的时候进⼊下⼀次递归
        value.then(function (promiseValue) {
          if (stepArg.done === false) {
            loop(generator.next(promiseValue), generator);
          }
        });
      } else {
        //判断程序没有执⾏完就将本次的结果传⼊下⼀步进⼊下⼀次递归
        if (stepArg.done == false) {
          loop(generator.next(stepArg.value), generator);
        }
      }
    }
    //执⾏动态调⽤
    loop(step, generator);
  }
  function* test() {
    var res1 = yield new Promise(function (resolve) {
      setTimeout(function () {
        resolve("第⼀秒运⾏");
      }, 1000);
    });
    console.log(res1);
    var res2 = yield new Promise(function (resolve) {
      setTimeout(function () {
        resolve("第⼆秒运⾏");
      }, 1000);
    });
    console.log(res2);
    var res3 = yield new Promise(function (resolve) {
      setTimeout(function () {
        resolve("第三秒运⾏");
      }, 1000);
    });
    console.log(res3);
  }
  generatorFunctionRunner(test);
复制代码

image.png 上述代码中我们封装了一个generatorFunctionRunner函数用来将Promise的异步流程同步化。打印结果会每隔1s进行打印,符合我们的预期。yield后面的Promise对象在运行时,在运行到当前行会等待Promise对象编程完成状态,然后我们通过判断Generator函数是否执行完了确定是否要继续递归调用。

Async和Await

后来,在ES8中实现了全新的异步控制流程,通过Async和Await,async和await其实就相当于是Promise的语法糖。

// 使用方式
async function test() {
    await ...
    await ...
}
复制代码

我们可以发现,这种写法和Generator函数结构很像,我们可以通过async修饰一个函数,被修饰过的函数的子作用域中,我们可以是用await来控制函数的流程,await右侧可以编写任意变量或者对象,如果右侧是一个Promise时,会等待Promise的状态变成fulfilled,才会继续往下执行。

我们尝试写一个被async修饰的函数:

  async function test() {
    console.log(123);
    return "这是一个测试函数";
  }
  const result = test();
  console.log(result); // Promise { '这是一个测试函数' }
复制代码

image.png 当我们接受到被async修饰的函数的返回值时,我们可以发现被async修饰的函数,本身是一个Promise对象。因此返回了一个值为1的Promise对象。

  async function test() {
    console.log(3);
    return 1;
  }
  console.log(1);
  test();
  console.log(2);
复制代码

我们知道,new Promise是一个同步的流程,回调函数执行实际上也是同步执行的。而Promise.then则是一个微任务。因此,我们可以大胆的猜测,上面的打印结果是1 3 2

image.png

接下来看下面的代码:

  async function test() {
    console.log(3);
    var a = await 4;
    console.log(a);
    return 1;
  }
  console.log(1);
  test();
  console.log(2);
复制代码

很多人会想,他会按照顺序打印1 3 4 2。

image.png 但是实际上输出的是1 3 2 4,为什么呢?

因为async把函数翻译成了Promise对象,所以实际上上述的代码类似于

  console.log(1);
  new Promise(function (resolve) {
    console.log(3);
    resolve(4);
  }).then(function (a) {
    console.log(a);
  });
  console.log(2);
复制代码

async函数中有⼀个最⼤的特点,就是第⼀个await会作为分⽔岭⼀般的存在,在第⼀个await的右侧和上⾯的代码,全部是同步代码区域相当于new Promise的回调,第⼀个await的左侧和下⾯的代码,就变成了异步代码区域相当于then的回调.

最终的Promise流程控制

  async function test() {
    var res1 = await new Promise(function (resolve) {
      setTimeout(function () {
        resolve("第⼀秒运⾏");
      }, 1000);
    });
    console.log(res1);
    var res2 = await new Promise(function (resolve) {
      setTimeout(function () {
        resolve("第⼆秒运⾏");
      }, 1000);
    });
    console.log(res2);
    var res3 = await new Promise(function (resolve) {
      setTimeout(function () {
        resolve("第三秒运⾏");
      }, 1000);
    });
    console.log(res3);
  }
  test();
复制代码

猜你喜欢

转载自juejin.im/post/7107827526927384612
今日推荐