async/await 详解

async/await 详解

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情

前言

今天让我们来聊聊JS对异步处理的终极操作:async
在ES7中提出了async这个函数 它是Generator函数的语法糖,用await关键字来表示异步 接下来我们来一步步剖析:

传统promise.then解决的异步

比如这里,它是先执行foo()函数,再来执行then中的回调函数bar()

new Promise (()=>{
    foo()
}).then(()=>{
    bar()
})
复制代码

promise.then在解决异步方面是挺好用,then方法返回的一个新的promise实例,来实现链式调用,然后,将传给 then 的函数和新 promise 的 resolve 一起 push 到前一个 promise 的 callbacks 数组中,达到承前启后的效果

console.log(100);
setTimeout(() => {
  console.log(200);
})
Promise.resolve().then(() => {
  console.log(300);
})
console.log(400); 
打印结果:
100
400
300
200
复制代码

但是总感觉它不是那么优雅,所以ES7便诞生了async函数,让一切优雅易懂

理解一下Generator函数

Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。Generator 函数将 JavaScript 异步编程带入了一个全新的阶段。

function* gen() {
    yield 1
    yield 2
    yield 3
    return 4
}

const g = gen()
console.log(g.next()); // { value: 1, done: false }
console.log(g.next()); // { value: 2, done: false }
console.log(g.next()); // { value: 3, done: false }
console.log(g.next()); // { value: 4, done: true }

复制代码

它里面给了一个封印,yield,也就是暂停,当函数运行到这里时,暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。 当return之后,也就是函数执行完毕,它的done值就会由false变成true。

next方法可以有参数

一句话说,next方法参数的作用,是为上一个yield语句赋值。由于yield永远返回undefined,这时候,如果有了next方法的参数,yield就被赋了值,比如下例,原本a变量的值是0,但是有了next的参数,a变量现在等于next的参数,也就是11。

屏幕截图 2022-12-01 101515.png

手写Generator核心原理

关注它的核心也就是看看done如何变成true,看看下面的例子,用swith__case模仿一下

var context = {
    next: 0,
    prev: 0,
    done: false,
    // 新增代码
    stop: function stop() {
        this.done = true
    }
}
function gen$(context) {
    while (1) {
        switch (context.prev = context.next) {
            case 0:
                context.next = 2;
                return 'result1';

            case 2:
                context.next = 4;
                return 'result2';

            case 4:
                context.next = 6;
                return 'result3';

            case 6:
                // 新增代码
                context.stop();
                return undefined
        }
    }
}
let foo = function () {
    return {
        next: function () {
            value = gen$(context);
            done = context.done
            return {
                value,
                done
            }
        }
    }
}
复制代码

第一次执行gen$(context),swtich判断的时候,是用prev来判断这一次应该执行哪个case,执行case时再改变next的值,next表示下次应该执行哪个case。第二次执行gen$(context)的时候,将next的值赋给prev。以及给context添加一个stop方法。用来改变自身的done为true。在执行$gen的时时候让context执行stop就好,这样执行到case为6的时候就会改变done的值了。
从中我们可以看出,「Generator实现的核心在于上下文的保存,函数并没有真的被挂起,每一次yield,其实都执行了一遍传入的生成器函数,只是在这个过程中间用了一个context对象储存上下文,使得每次执行生成器函数的时候,都可以从上一个执行结果开始执行,看起来就像函数被挂起了一样」

async/await

  • 从上面我们可以得知,Promise 的方式虽然解决了 callback hell,但是这种方式充满了 Promise的 then() 方法,如果处理流程复杂的话,整段代码将充满 then。语义化不明显,代码流程不能很好的表示执行流程。
  • Generator 的方式解决了 Promise 的一些问题,流程更加直观、语义化。但是 Generator 的问题在于,函数的执行需要依靠执行器,每次都需要通过 g.next() 的方式去执行。
    这两种方法都有一些小弊端,那么接下来的终极async函数完美的解决了上面两种方式的问题,同时它自带执行器,执行的时候无需手动加载

像写hooks高级函数一样写async函数

先来看看官方给的async/await

function foo(num) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(num * 2)
        }, 1000)
    })
}
async function asyncFn2() {
    const num1 = await foo(1)
    const num2 = await foo(num1)
    const num3 = await foo(num2)
    return num3
}
asyncFn2().then(res => console.log(res))
复制代码

屏幕截图 2022-12-01 110116.png

将前一个await返回的结果作为下一个的初始值,反复循环,直到return函数结束,这里我们可以看到打印出来的 asyncFn2()Promise { <pending> },async函数是返回了一个promise对象,这样的promise.then实现的链式调用,当我们的代码很长或者说需要嵌套的层数很多时,代码就会显得十分臃肿,那么怎么办呢,加上我们的Generator函数。

function foo(num) {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(num * 2)
      }, 1000)
    })
  }  
  // async  具有async功能的函数
  function generatorToAsync(generatorFn) {
    return function() {
      const gen = generatorFn.apply(this, arguments)  
      return new Promise((resolve, reject) => {  
        // const g = generatorFn()
        // const next1 = g.next()
        // next1.value.then(res1 =>  
        //   const next2 = g.next(res1)
        //   next2.value.then(res2 => {
        //     const next3 = g.next(res2)
        //     next3.value.then(res3 => {  
        //       resolve(g.next(res3).value) // { value: 8, done: true }
        //     })
        //   })
        // })
        function loop(key, arg) {
          let res = null
          res = gen[key](arg) // gen.next(8)
          const { value, done } = res
          if (done) {
            return resolve(value)
          } else {
            // Promise.resolve(value) 为了保证 value 中的promise状态已经变更成 成功状态
            Promise.resolve(value).then(val => loop('next', val))
          }
        }
        loop('next')
      })
    }
  }
  
  function* gen() {
    const num1 = yield foo(1)
    const num2 = yield foo(num1)
    const num3 = yield foo(num2)
    return num3
  }
  const asyncFn = generatorToAsync(gen)
  // console.log(asyncFn()); // Promise{}
  asyncFn().then(res => {
    console.log(res);
  })
复制代码

屏幕截图 2022-12-01 112329.png 定义一个高阶函数generatorToAsync,并将generatorFn函数当作参数传进去,然后返回一个具有async功能的函数,然后再下面定义一个loop函数,在其内部递归调用来知道我们要调用.then的次数,在其内部给res一个初始null值,然后loop函数使用'next'作为参数,通过循环判断执行出来的结果状态是false还是true,是false就继续递归,true就直接resolve出结果。从而实现async功能。

结语

定义高阶函数来实现具有async函数一样的功能,它不仅解决了代码冗余,同时,流程清晰,直观、语义明显。

分类:
前端