zl程序教程

您现在的位置是:首页 >  其它

当前栏目

Promise - (八)Iterator、Generator、co

Promise Generator Iterator co
2023-09-14 09:13:41 时间

ES6不仅推出了Promise,还推出了Iterator、Generator。

简单理解:Iterator是迭代器,Generator是生成器。

Iterator迭代器

从名字来看,我们就应该知道Iterator的功能应该类似于for循环。但是它和for循环有着本质区别。

Iterator迭代器主要解决了两个问题:

1、for循环是不可控迭代,而Iterator可以实现可控迭代

什么叫不可控迭代,什么叫可控迭代?

我们平时使用for循环,只要定义好for,for循环就会从数列头部一直迭代到数列尾部

那么有没有可能,我们来控制迭代,即我们发出一条迭代指令,然后才向后迭代一个位置,我们不发出指令,就不会发生自动迭代。Iterator帮助我们实现了可控迭代。

2、不同类型的对象使用的遍历方式不同,比如数组对于使用for,for...in,forEach,类数组对象使用for,普通对象使用for...in,而不同的遍历方式又取决于对象的结构模型,比如类数组对象可以使用的原因是类数组对象都有length属性以及整数属性,比如普通对象使用for...in是因为for...in是基于对象的可遍历属性来遍历的,而数组对象不仅有length属性和整数索引,且其整数索引就是数组对象的可遍历属性,所以数组既可以用for,也适合用for...in。

从上面遍历方式的选择可以看出,我们必须要知道对象的结构模型,才能选择适合的遍历方式。所以随着新类型的推出,比如Set,Map等,将意味着可能要推出新的遍历方式,那么程序员需要记住太多的遍历方式,而Iterator的推出,阻止了这一不好的发展趋势,Iterator致力于推出统一的迭代方式,即只要是实现了Iterator的类,它产生的对象就可以使用统一的迭代方式。

Iterator解决了传统遍历的两大问题,而这也是Iterator的两大闪光点:

1、可控迭代

2、统一迭代方式

Iterator定义

ES6规定,如果某对象想要实现Iterator,则该对象需要内置一个[Symbol.iterator]属性(一般在对象的原型上)

[Symbol.iterator]是一个函数,该函数用于生成一个迭代器对象,

迭代器对象有一个方法next,next方法用来实现可控迭代,

next方法返回一个迭代结果对象 { value: '',  done: true/false }

其中value是被迭代的数列的数据,done表示是否迭代结束,true表示迭代完成,false表示迭代未完成。

比如Array就实现了Iterator

重写Array的Iterator

Array.prototype[Symbol.iterator] = function() {
            let cursor = 0
            const length = this.length
            return {
                next: () => {
                    return {
                        value: this[cursor++],
                        done: cursor > length
                    }
                }
            }
        }

 需要注意的是:第五次next还不算迭代结束

通过以上测试我们发现,的确实现了可控迭代,但是每次都是打印迭代结果{value, done},我们迭代数列时一般只关注value,不关注done,且大多数情况下,我们希望自动迭代,而不希望手动迭代

for...of 遍历可迭代对象

上面方式实现了自动迭代,但是这种方式还是比较麻烦,且对于所有的实现Iterator的对象来说,都是模板式操作,所以ES6推出了for...of遍历,封装了以上模板式操作

也就是说 for a of A做了如下几件事

0、for...of的前提是A是一个有[Symbol.iterator]属性的对象,否则报错

1、通过A[Symbol.iterator]() 获取到迭代器对象

2、每一次迭代都调用迭代器的next方法,并将返回结果对象的value属性赋值给a

迭代器应用

Array、String、Arguments、Set、Map都实现了迭代器,它们都可以使用for...of遍历

生成器Generator

生成器Generator是一个函数,但是它有不同于普通函数,在语法上,生成器函数会在函数声明的function关键字和函数名之间加一个*,一般我们将*紧挨在function后面,以体现生成器函数和普通函数的不同

生成器函数和普通函数的区别

生成器函数和普通函数的不同,除了语法定义上,

function* fn1(){
    // ...
}

var fn2 = function* (){
    // ...
}

还有功能上:

1、调用生成器函数不会立即执行函数体代码(普通函数会立即执行函数体代码)

2、调用生成器会返回一个迭代器对象(普通函数会返回return的值,没有return就返回undefined)

生成器函数返回的迭代器对象 迭代啥?

生成器函数内部一般使用yield关键字产出一个值,给迭代器迭代

yield从翻译来看,不具有产生,还有让步,暂停的含义,而生成器函数中的yield除了产出值能力外,也有暂停,让步执行的能力

总结 function*, iterator , yield, next 之间关系

function* 会返回一个迭代器对象iterator,function* 无权控制自身函数体执行,需要通过返回的iterator对象的next方法来触发function* 函数体执行,当执行到yield时,next会得到一个yield产出值,并暂停后续执行,如果想继续执行,则需要继续调用迭代器对象的next

使用生成器函数来重写Array的迭代器

Array.prototype[Symbol.iterator] = function*() {
    let cursor = 0
    const length = this.length

    while (cursor < length) {
        yield this[cursor++]
    }
}

next传参给yield

我们已经知道了生成器函数中的yield会产出值给next迭代,如果yield产出的值,通过next迭代出来后,被用来做某些事,最终产生一个结果,而后续yield需要这个上一个yield产出值的执行结果,那么如何将结果传递给yield呢?很显然,我们会想到通过next,因为yield和next是有关联的

function* fn(){
    let value1 = yield 1
    yield value1
}

const iterator = fn()

let {value, done} = iterator.next() // 我们通过next拿到第一个yield的产出值1,并解构给value

let result = ++value // 我们对产出值做出处理,并得到处理结果result

// 现在我们想将result反馈给第一个yield,发现与之关联的next已经执行完毕。。。

通过实操,我们发现next返回yield产出值后,它就已经不在了,也就无法将结果通过它传递给yield

就像人生一样,小时候我们可以父母亲密无间(yield),可以肆无忌惮地表达我们的爱意,去撒娇,去耍无赖,但是当我们进入社会后(next),发现了父母的艰难,自己也因为幼稚被社会不断地毒打,搞得我们遍体鳞伤,最终我们坚强了,学会保护自己(result),但是此时当我们再去面对父母时,再也无法像以前一样(next之前),因为彼此之间有了一层隔阂。

如果这个时候我们多了一个幼稚的弟弟妹妹,我们可以让他们帮我们转交我们迟到的爱意给父母,因为我们和父母面对幼稚的弟弟妹妹都可以卸下隔阂。

function* fn(){
    let value1 = yield 1
    yield value1
}

const iterator = fn()

let {value, done} = iterator.next() // 我们通过next拿到第一个yield的产出值1,并解构给value

let result = ++value // 我们对产出值做出处理,并得到处理结果result

// 现在我们想将result反馈给第一个yield,发现与之关联的next已经执行完毕。。。

iterator.next(result)

总结 next传参给yield

next可以迭代出yield产出的值,而next在迭代完后,就不在了,我们对于yield产出值的结果只能通过下一次next反馈

生成器函数与异步串行

生成器函数中的yield的暂停执行能力,以及生成器函数返回的迭代器的next让生成器恢复执行的能力,让我们可以将一个函数的执行变得可控。

我们再来回顾下异步串行的概念:异步任务A执行结束后,才能执行异步任务B,异步任务B执行结束后,才能执行异步任务C

在传统的异步串行实现中,我们使用回调地狱,但是体验不好,被后来的Promise的then链式取代了,但是Promise的then链式还是不够简单,或者说还有些缺点,这里不做讨论,后面有章节专门讨论。那么什么才是最棒最牛最简单的串行呢?

那肯定是像同步代码一样啊,从上到下依次执行,简单又好看,合情合理又好看

异步任务串行有可能像同步代码一样搞吗?没有生成器之前,你的梦想是什么,有生成器之后,梦想成真

生成器暂停恢复执行机制搭配Promise异步编程,完美实现异步任务串行的究极形态

const fs = require('fs')

function read(path, encode) {
    return new Promise((resolve, reject) => {
        fs.readFile(path, encode, function(err, data) {
            if (err) return reject(err)
            resolve(data)
        })
    })
}


function* fn() {
    let val1 = yield read('a.txt', 'utf-8')

    let val2 = yield read(val1, 'utf-8')

    let val3 = yield read(val2, 'utf-8')
    console.log(val3)
}

function co(fn) {
    const iterator = fn()
    let { value, done } = iterator.next()
    value.then(data => {
        let { value, done } = iterator.next(data)
        value.then(data => {
            let { value, done } = iterator.next(data)
            value.then(data => {
                iterator.next(data)
            })
        })
    })
}

co(fn)

在node环境上运行以上代码,可以fn*中 val2 依赖于val1,val3依赖于val2,所以他们必须是串行执行的

其中co函数就是执行器,它和迭代器,生成器相辅相成

其实co函数还可以再次优化,因为发现了可以递归的痕迹

const fs = require('fs')

function read(path, encode) {
    return new Promise((resolve, reject) => {
        fs.readFile(path, encode, function(err, data) {
            if (err) return reject(err)
            resolve(data)
        })
    })
}


function* fn() {
    let val1 = yield read('a.txt', 'utf-8')

    let val2 = yield read(val1, 'utf-8')

    let val3 = yield read(val2, 'utf-8')
    console.log(val3)
}

function co(fn) {
    const iterator = fn()

    function next(value) {
        if (!value) value = iterator.next().value
        value.then(data => {
            let { value, done } = iterator.next(data)
            if (!done) {
                next(value)
            }
        })
    }
    next()
}

co(fn)

此时,如果我们不去看co函数定义,以及co函数执行

const fs = require('fs')

function read(path, encode) {
    return new Promise((resolve, reject) => {
        fs.readFile(path, encode, function(err, data) {
            if (err) return reject(err)
            resolve(data)
        })
    })
}


function* fn() {
    let val1 = yield read('a.txt', 'utf-8')

    let val2 = yield read(val1, 'utf-8')

    let val3 = yield read(val2, 'utf-8')
    console.log(val3)
}

可以发现 生成器函数fn 和 async await用法几乎一致

其实async await就是 生成器+迭代器+执行器 实现异步串行的语法糖

下节我们来写async和await