彻底玩转Generator、迭代器、执行器到async_await

1,210 阅读4分钟
人贵有志,学贵有恒!人贵有志,学贵有恒!

1619510953(1).jpg

一、 Generator函数

1. 基本概念及用法

Generator函数是ES6提出的一种新的异步解决方案,它不同于一般的函数。Generator函数前面必须有*,函数体内有yiled标识暂停标记。我们可以将Generator理解为一个状态机,里面可以存在很多异步的操作。函数执行的结果不是return的值,而是会返回一个遍历器对象,通过执行遍历器对象上的next方法,可以改变函数内部的状态指针,进而遍历Generator函数内部的每一个状态。首先,我们了解一下这个函数的基本写法:

function* gen() {
    yield 1; // yield 暂停执行的标记
    yield 2;
    yield 3;
    yield 4;
}
const iterater = gen(); // iterater是一个遍历器,
                        // 可以通过它的next方法来遍历generator函数的内部状态
iterater.next(); // {value : 1, done : false}
iterater.next(); // {value : 2, done : false}
iterater.next(); // {value : 3, done : false}
iterater.next(); // {value : 4, done : false}
iterater.next(); // {value : undefined, done : true}
遍历器的执行机制:通过调用iterater的next方法,使得generator函数的内部指针移向下一个状态,
指针从函数头部或者上一次暂停的位置开始执行,直到下一个yieldreturn语句。
大概可以理解为generator函数是分段执行的,遇到yield可以暂停函数的执行,next可以恢复执行。  

虽然yield和return后面表达式的值都作为next()函数执行完返回的value属性的值,但它们是有所区别的,函数遇到return会认为遍历结束,遇到yiled只会暂停,还会有下一次的遍历

2. next方法的传参的注意点

next方法的执行是可以传参,而且next方法传入的参数作为上一个yield表达式的返回值
如果next方法执行的时候没有传入参数,那么上一个yiled表达式结果为undefined
第一个next的执行传入的参数是无效的  
function* gen() {
   var val1 = yield 1;
   console.log(val1)   // 2
   var val2 = yield 2;
   console.log(val2)   // 3
   var val3 = yield val2 + 3;
   console.log(val3)   // 4
   var val4 = yield 4;
   console.log(val4)   // 5
}
const iterater = gen(); 
iterater.next(1); //第一个next执行的时候传入的参数是无效的 {value: 1, done: false}
iterater.next(2); //{value: 2, done: false}
iterater.next(3); //{value: 6, done: false}
iterater.next(4); //{value: 4, done: false}
iterater.next(5); //{value: undefined, done: true}

3. Generator函数的使用(生成迭代器)

for...of循环内部其实是调用需要遍历的数据类型上部署的Iterator接口,这个接口其实就是自动调用迭代器的next方法,我们可以通过generator函数尝试给对象添加这个属性,使对象也可以通过for...of遍历

与此同理,扩展运算符...、解构赋值、Array.from内部调用的都是遍历器接口

//通过generator实现对象的迭代器接口
const obj = {
    'key1': 1,
    'key2': 2,
    'key3': 3,
    [Symbol.iterator] : function *(){ //这样我们通过generator函数实现了一个迭代器
        // 由于对象本身没有下标,也没有顺序,我们可以将对象转换为map
        const map = new Map();
                map.set('key1',1)
                map.set('key2',2)
                map.set('key3',3)
                console.log(map)
                // 0: {"key1" => 1}
                // 1: {"key2" => 2}
                // 2: {"key3" => 3}
                // size: 3
         console.log([...map.entries()]) 
       //map.entries()方法返回的对象可迭代    
       //...运算符其实底层也是通过执行该对象的迭代器方法
        const mapResult = [...map.entries()];
        var index = 0;
        var size = map.size;
        while(index < size){
            yield mapResult[index++];
            }
        }
    }
    // for...of会自动调用迭代器上的next方法
    for(let item of obj){
        console.log(item) // ["key1", 1]  ["key2", 2]  ["key3", 3]
    }
      //! 需要特别注意的是:for...of在next返回对象的done属性为true时就会停止遍历,
      //且不会包含done为true时对应的value值

二、执行器

虽然generator函数是一种很好的异步解决方案,可以让我们像写同步代码一样去处理异步逻辑,但是我们可以发现,每次都要手动执行生成迭代器,手动调用next方法来遍历,这种方式处理业务也不太友好,所以我们需要一个执行器自动来帮我们遍历所有的状态。

function readFile(url){
    return new Promise(function(resolve,reject){
        fs.readFile(url,function(error,data){
            if(error) reject(error);
            resolve(data)
        })
    })
}

function* gen(){
    var f1 = yield readFile('./a.txt');
    console.log(f1)
    var f2 = yield readFile('./b.txt');
    console.log(f2)
   
}
function co( generator ){
    const iterator = generator();
    return new Promise(function(resolve,reject){
      // 通过递归的方式遍历内部状态
        function diff(value){
            ret =  iterator.next(value);
            if(ret.done) return resolve(ret.value);
            Promise.resolve(ret.value).then(function(data){
                    diff(data.toString())
             });
        }
        try{
            diff()
        }catch(err){
            reject(err)
        }
    })

}
co(gen)
co模块是自己实现的一个执行器模块,co方法,接收一个generator函数作为参数,
co内部会自动根据构造器函数生成迭代器,并且自动迭代每个状态,拿到异步的结果,
并通过next传参的方式,将每个yiled结果的值赋值给对应yiled执行结果,
最终,我们就在generator函数里打印出来了对应文件的内容

通过执行器执行了generator函数,我们会发现,现在在generator的写法和async函数很类似了,没错,async其实就是generator函数的语法糖,比generator函数多了一个执行器模块

三、 async await

有了上述内容的铺垫,相信大家再来理解async函数就很简单了,它其实就是generator函数自带一个执行器,且比generator函数语义更清晰,可以直接拿到await后面的表达式的结果,不需要手动执行迭代器。

1.async函数的使用

async函数的返回值是一个promise,可以通过.then拿到函数的返回值,如果函数没有return值,那么值就是undefiend.

    // async用来标识是async函数
    async function getInfo(){
     // await 等待异步执行,返回结果后再继续执行
      var result1 = await  ajax('https://api.github.com/users/github');
     console.log(result)
     var result2 = await  ajax('https://api.github.com/users/github');
     console.log(result2)
}

// 函数的调用就和普通的函数类似,只需调用一次,函数体内的多个异步会一次执行
var pro = getInfo()
pro.then(function(data){
    console.log(data) //undefined
})

2.async执行后返回的promise状态变化

一个async函数被执行,会立即返回一个Promise对象,只有当await后面所有的promise对象执行完,才会发生变化,除了中途遇到return 语句或者代码执行错误。即函数体内的所有异步操作执行完毕后,才会执行then方法的回调。

3.await

await命令后面,可以是Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作),如果后面不是Promise对象,会自动转化为一个立即resolve的Promise对象

    async function asy(){
      var res1 = await 3;
    }
    ==> 等同于:
      async function asy(){
      var res1 = await Promise.resolve(3);
    }

await命令后面的Promise如果变为rejected状态,那么async返回的Promise也会变为rejected状态

async function asy(){
    await Promise.reject('error')
   const f2 =  await readFile('./a.txt') //后面的代码不会执行了
}
asy().then(function(data){
    console.log(data)
}).catch(function(err){
    console.log(err) //error
})
  • 因为Promise对象运行的结果很有可能是rejected状态,为了不让一个异步操作的失败,影响后面的异步操作,我们最好把await语句都放在try...catch中
async function asy(){
    let f2;
    try {
        await Promise.reject('error')
    } catch (error) {
        console.log(error)
    }
    f2 =  await readFile('./a.txt')
      return f2
}
asy().then(function(data){
    console.log(data) //最后会打印出来f2的结果
}).catch(function(err){
    console.log(err)
})
  • 多个await后面的异步操作如果没有继发关系,最好是让它们同时触发,这样能够提高执行速度
async function asy(){
    // let f1 = await readFile('./a.txt')
    // let f2 =  await readFile('./b.txt')
    let rf1 =  readFile('./a.txt')
    let rf2 =  readFile('./b.txt')
    let f1 = await rf1;
    let f2 = await rf2;
}

好了,关于async,Generator的这篇文章就分享到这儿了,喜欢的小伙伴们点个赞,不足之处也希望大家在评论中指出,一起学习,一起进步!