异步发展史(Promise、Generator、async await)

148 阅读5分钟

从开始的回调函数,到es6中的promise,再到async await最受喜爱,中间到底经历了什么?

回调函数

最最原始的异步操作是通过纯回调函数实现的。

getFile(syncFunc,successCallback,errorCallback);

多个串行且相互以来的异步操作如下:

// 回调地狱
getFile(data,successCallback(result1){
    getFileTwo(result1,successCallbackTwo(result2){
        getFileThree(result2,successCallbackThree(result3){
            // 处理数据
        },errorCallback3)
    },errorCallback2)
},errorCallback1)

可以看出,回调方式的异步串行会造成回调地狱,很难维护,并且错误处理与正常业务代码耦合在一起,牵一发而动全身。

为了解决回调地狱,出现了Promise。

Promise与回调函数比有什么优点?(为什么要用Promise?)

针对以上情况,Promise的代码实现如下(经过简化):

function getFile(){
    return new Promise((resolve,reject)=>{
        const content = fs.readFileSync('')
        return resolve(content)
    })
}

getFile()
.then(result1=> return getFileTwo(result1))
.then(result2=> return getFileTwo(result2))
.then(result3=> 处理数据)
// 统一错误处理(错误透传)
.catch(err=>console.log(err))

可以看出,纯回调函数和promise的区别如下:

  1. 回调函数在写异步操作时,就必须同时写回调。而Promise可以在异步进行中/结束后的任何时候为它添加回调函数,并且获得的结果是固定的。
  2. Promise可以多次获取异步操作的结果,回调函数想要获取多次结果只能执行多次异步操作,并为异步操作绑定回调获取结果。
  3. 用promise解决回调地狱,不用多次处理异常,提高代码可读性。

Promise实现本身其实还是依赖回调,它只是通过发布订阅的方式,在异步执行的时候把回调函数进行存储,异步执行结束后再调用回调,从而实现了链式调用。

通过resolve和reject,拿到了异步函数的控制权。

生成器(Generator)

promise虽然解决了回调地狱,但是它一连串的then,在视觉上可读性还是不够好。

我想难道不能像写串行代码一样写多个异步函数吗?

像下面这样,做不到吗?

const data = getDataSync();
const data1 = showDataSync(data);
const data2 = alertDataSync(data1);

显然通过以上技术都做不到,除非让异步操作变成同步操作。

我在执行getDataSync异步函数的时候,异步结果还没得到,此时返回的data为undefined。作为相互依赖的异步操作显然是不行的。

我又想:我需要让前一个异步执行完之后,再向下执行另一个,能不能做到?

生成器函数的出现为我们提供了异步串行的一种新思想。

/*
生成器
yield:函数代码分割符,当一个异步函数结束后,调用next继续调用后面的异步
*/

function getData(){
  return new Promise((resolve,reject)=>{
    setTimeout(() => {
      let data = '111';
      // 得到第一个结果了,调用next继续
      iterator.next(data)
    }, 1000);
  })
  
}

function showData(data){
  return new Promise((resolve,reject)=>{
    setTimeout(() => {
      let a = '111';
      // 得到第二个结果了,调用next继续
      iterator.next(data + a)
    }, 1000);
  })
}

function alertData(data){
  return new Promise((resolve,reject)=>{
    setTimeout(() => {
      let a = '111';
      // 得到第三个结果了,调用next继续
      iterator.next(data + a)
    }, 1000);
  })
}

/*
yield可以获取异步函数的执行权 通过next继续执行下一个异步操作
*/
function * gen(){
  const data = yield getData();
  const data1 = yield showData(data);
  const data2 = yield alertData(data1);
  console.log(data2)
}

const iterator = gen();
iterator.next()

上面代码块中gen函数就是生成器函数,前面的*是生成器的标识符,执行之后返回一个迭代器对象。它的特点是拥有异步函数的执行权,可以在执行一个异步函数的时候让程序无阻塞的暂停,得到结果后又可以通过迭代器对象中的next方法继续执行下一个异步,直到执行完迭代器中所有的函数。

注:为什么要让程序暂停呢?

这里的暂停主要是给异步函数执行的时间,不然我们通过const data = syncFunc()这种方式无法在异步执行时得到data

所以,有了生成器函数之后,我们的几个互相依赖的异步函数的写法,就像串行语句一样优雅,可读性非常棒。

但是!!我又想

我每次执行完一个异步操作并且获取到数据之后,都要手动调用next方法并传入数据,让程序继续执行下去。太麻烦了吧!

那我手写一个让迭代器自动执行并且获取到最后数据的方法吧:

function co(iterator){
  return new Promise((resolve,reject)=>{
    function _next(v){
      const { value, done } = iterator.next(v);
      if(done){
        return resolve(value)
      } 
      Promise.resolve(value).then((res)=>{
        _next(res)
      },(reason)=>{
        return reject(reason)
      })
    }
  _next()

  })
}

co(gen()).then((res)=>{
  console.log(999,res)
})

终极Boss:async await

于是,我们的终极Boss出现了,他就是async 和 await。

其实就是上面的co函数以及生成器函数的语法糖。

async function fun(){
    const result1 = await getFile();
    const result2 = await getFile(result1);
    const result3 = await getFile(result2);
}

与上面的生成器函数对比,可以发现,就是把*换成async,yield换成await

但是,async await还做了以下优化:

1.可自动执行next函数,不用手动执行。

2.await后面跟promise,如果不是promise,就调用Promise.resolve转换成Promise

3.await可以将promise的值返回

4.可以用同步的方式处理错误,可以在外层用try catch包裹所有的await。

以上,完全实现了互相依赖异步操作的串行代码化。真正的语法糖贼甜。