Async、Await和Generator的关系

1,206 阅读4分钟

本篇文章会按迭代器 => 生成器 => 生成器+promise解决异步 => async、await这个顺序介绍以下几点

  • 迭代器和生成器是什么
  • 生成器解决异步的方法以及弊端
  • 如何处理这个弊端
  • async、await和generator的关系

ES6的迭代器和可迭代对象

什么是迭代器?

迭代器是一个对象,迭代器对象要求实现then方法,并且then方法的返回值是{ done:boolean , value:any } 的对象。让我们看一下迭代器的使用吧

let index = 0
const it = {
  next(){
    //当超出数组下标的时候,done是true,value是undefined
    return index < names.length?
        {value:names[index++],done:false}:{value:undefined,done:true}
  }
}
console.log(it.next());  //{ done:false , value: 1}
console.log(it.next());  //{ done:false , value: 2}
console.log(it.next());  //{ done:false , value: 3}
console.log(it.next());  //{ done:true , value: undefined}

什么是可迭代对象?

可迭代对象是实现了Symbol.iterator方法的对象,调用这个方法会返回迭代器对象。可迭代对象又叫iterable对象,鼠标移入的时候可以看见,常见的可迭代对象有:Array,Map,Set图片.png可以看到,set构造方法中可以传入一个可迭代对象来创建set实例,所以我经常会用它来方便的去重数组

let arr = [1,2,3,1]
console.log([...new Set(arr)])  //[1,2,3]
  • 可迭代对象有什么用?

再次说明,可迭代对象是实现了Symbol.iterator方法的对象,并且这个方法会返回迭代器。可迭代对象的应用场景有很多,例如

for...of

const arr = [1,2,3]
//普通对象是不能用for..of的
const obj = {name:'jzsp'}
for(const item of arr){
    console.log(item)
}

展开语法

const arr = [1,2,3]
const obj = {name:'jzsp'}

console.log(...arr)

//普通对象是不能直接展开的,会报错
// console.log(...obj)

//注意,这个是对象中用展开语法,是es9新增的,本质上用的不是迭代器
console.log({...obj})

解构赋值

const arr = [1,2,3]
const obj = {name:'jzsp'}

const [v1,v2,v3] = arr
//注意,这个本质上用的不是迭代器,是es9新增语法
const { name } = obj

创建可迭代对象的时候,传入可迭代对象

let arr = [1,2,3,1]
console.log([...new Set(arr)])  //[1,2,3]

其他一些可以传入可迭代对象的API

图片.png

图片.png

自定义类实现可迭代的案例

//案例是这样的:这是一个教室类,里面有很多的学生,要求可以用for...of遍历所有的学生
class classromm{
  constructor(address,name,students) {
    this.address = address
    this.name = name
    this.students = students
  }
  entry(newStudent){
    this.students.push(newStudent)
  }
  //在对象的原型链上添加[Symbol.iterator]方法
  [Symbol.iterator](){
    let index = 0
    //返回的是一个迭代器对象,迭代器对象要求实现next方法,next方法返回的是{done,value}对象
    return {
      next:()=>{
        //遍历所有的学生
        return index < this.students.length ?
            {done:false,value:this.students[index++]}:{done:true,value:undefined}
      }
    }
  }
}
const room = new classromm('台州','九折水瓶教室',['九折水瓶1号','九折水瓶2号'])
//
for (const stu of room) {
  console.log(stu)
}

Generator生成器

可能很多人会问了,明明标题是Generator,为什么前面要花那么多时间介绍迭代器呢?这是因为Generator生成器跟Iterator迭代器有异曲同工之妙。还记得迭代器对象是什么吗?

没错,Iterator是实现了next方法,并且next方法返回值是{done,value}的对象!Generator也是如此,它内部有next方法,返回值也是{done,value}对象。

生成器是ES6新增的一种函数控制、使用的方案,它可以让我们更加灵活的控制函数什么时候继续执行、暂停执行。生成器是特殊的迭代器

生成器函数

我们需要通过生成器函数来获取Generator对象,生成器函数是一个函数,但是与普通的函数比有区别

  • 生成器函数需要在function后面加*
  • 生成器函数内部可以通过yield关键字来控制函数的执行流程
  • 生成器函数的返回值是generator生成器对象
  • 通过调用生成器对象的next方法来执行函数。yield相当于将函数内部分段了,第i个next就执行第i-1段

让我们通过一个例子来看看生成器函数吧!

function* foo(){
  console.log(1)
  yield
  console.log(2)
  yield
  console.log(3)
  yield   'jzsp'
  console.log(4)
  return '我是返回值'
}

let generator = foo()    
generator.next()   // {value:undefined,done:false}
generator.next()

//因为生成器generator是特殊的迭代器,有next方法,返回值的格式跟迭代器一样
//value是yield关键字后面跟着的值(或者是return的值)
//done的话,如果当前next没有被yield关键字暂停执行,也就是说后面没有yield,那么done就为true
console.log(generator.next())   //{ value: 'jzsp' done: false }

//done为true的时候,也就是函数执行结束的时候,value的值是函数return的值
console.log(generator.next())   //{ value: '我是返回值', done: true }


next传参

i次调用next时传的参数,会作为第i-1yield表达式的返回值

i次调用next的返回值的value,是第iyield表达式后面的值

第一段(第一个yield前)代码需要传值的话,可以在调用生成器函数的时候传入

functionfoo(first){
    console.log('123',first)    
    const n = yield first
    console.log(n)   //第二个参数
}


let gen = foo('第一个参数')
console.log(gen.next())   //{ value: '第一个参数', done: false }
gen.next("第二个参数")

生成器代替迭代器(yield*)

生成器代替迭代器的原理是,生成器用yield可以实现暂停效果,用next可以实现迭代效果,并且next返回对象中的done属性会根据yield的迭代情况自动判断value属性就是yield关键字后的值

//传入一个数组,返回该数组的迭代器(虽然数组有迭代器)
function createArrayIterator(arr){
  let index = 0
  return {
    next:function(){
      return index < arr.length?
          {value:arr[index++],done:false}:{value:undefined,done:true}
    }
  }
}

//代替写法
function* createArrayIterator(arr){
	//第一种写法
    for(const item of arr){
        yield item
    }
    
    //第二种写法:yield*后面跟着的是可迭代对象,每次调用next的时候会迭代一次可迭代对象
    yield* arr
}

生成器+Promise解决异步问题

花了那么大篇幅介绍了迭代器跟生成器(其实里面还有很多内容,但是主要是为了讲解处理异步),终于到正题了!让我们看一下Generator是怎么解决异步问题的吧!

假设我们现在有一个用Promise封装的模拟异步请求,如下

function request(url) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      //我们默认是成功的
      resolve(url);
    }, 3000);
  });
}

现在想模拟的是,发送一个字符串给服务器,服务器将这个返回,我们再将这个返回的字符串拼接上aaa再发送,服务器再返回,我们再拼接上bbb再发送,服务器再返回。

使用Generator+promise可以解决这个问题,代码如下

function* getData(){
  const res1 = yield request("111")   
  const res2 = yield request(res1+'aaa')
  const res3 = yield request(res2+'bbb')
  return res3
}

//手动执行(回调地狱)
//第一次next获取到第一个yield返回的promise,指定then回调,在回调中获取返回值
//第二次next传入第一个promise值,这个值会作为第一个yield的返回值res1
const gen = getData()
gen.next().value.then(res=>{
  gen.next(res).value.then(res=>{
    gen.next(res).value.then(res=>{
      console.log(res)
    })
  })
})

这显然不是我们想要的,因为实际生活中我们很难处理很多请求的情况(回调地狱),并且还要自己写,那么我们可以实现一个自动执行next的函数来帮我们完成这件事。

//自动执行,传入生成器函数对象,在内部通过递归自动执行
function execGenerator(fn){
  //获取生成器对象
  const gen = fn()
  function exec(v){
    //先调用next执行函数,直到yield停止并且获取yield的返回值
    const res = gen.next(v)
    
    //判断是否迭代完成,如果完成的话直接返回
    if(res.done){
      return res.value
    }
    
    //没迭代完成的话,就取出返回对象中的value(promise),在then中取出promise的值,递归调用
    res.value.then(res=>{
      exec(res)
    })
  }
  exec()
}
execGenerator(getData)

async、await

先来介绍一下async和await吧

async函数

  • 在声明函数前加上async关键字就可以变成async函数。
  • async函数的返回值一定是一个promise,这个返回的promise由async函数return的内容决定。

注意,加上async关键字不代表函数就变成异步的了,在函数内部正常写的话,跟普通函数没有太大区别

async function foo(){
    console.log(2)
}
console.log(1)
foo()
console.log(3)
//输出顺序是123,说明foo并没有变成异步函数

await关键字

await关键字后面经常跟上一个promise,当这个promise状态没有改变(一直是pending的时候),下面的代码都不会执行。等到promise状态改变的时候,会将promise的值取出作为await关键字的返回值,然后可以继续执行后面的代码。(如果是普通值,就直接作为await的关键字的返回值)

function request(){
  return new Promise((resolve,reject)=>{
    setTimeout(()=>{
      //我们默认是成功的
      resolve(111)
    },2000)
  })
}
async function foo(){
  const res =  await request()
  console.log('----------',res)
}
foo()

如果await后的promise结果是reject失败的,那么会将reject的值作为整个async函数foo返回的promise的reject的值,并且不会向下执行

function request(){
  return new Promise((resolve,reject)=>{
    setTimeout(()=>{
      reject(111)   //失败的promise
    },2000)
  })
}
async function foo(){
  const res =  await request()
  //不会向下执行
  console.log(res)
}
foo().catch(err=>{
	console.log(err)
})

await关键字是语法糖:在Generator中,yield后面是Promise的话,会在promise执行结束的时候将promise封装在next()返回的对象的value属性中,想执行下一步需要调用next方法;而await后面是promise的话,会在promise状态改变时取出promise的值作为await关键字的返回值

值得注意的是,await关键字后面的代码本质上是在当前await返回的Promise的then方法中执行的,也就是说,await后面的代码是放到微任务中执行的。需要注意的是,await后如果跟着的是常数,await关键字下面的代码本质上还是会被放到微任务中。

async、await解决异步

async、await这种解决异步的方案,其实就是Generator+Promise自动执行版本的语法糖哦~(看是不是跟上面Generator的例子很像呢)感谢ES7,感谢JavaScript!

async function getData(){
  const res1 = await request("111")
  const res2 = await request(res1+'aaa')
  const res3 = await request(res2+'bbb')
  return res3
}