ES6 中的 Generator * 最终如何处理异步操作/async

最基础的Generator

function* Hello() {
    yield 100
    yield (function () {return 200})()
    return 300
}

var h = Hello()
console.log(typeof h)  // object

console.log(h.next())  // { value: 100, done: false }
console.log(h.next())  // { value: 200, done: false }
console.log(h.next())  // { value: 300, done: true }
console.log(h.next())  // { value: undefined, done: true }
  • 定义Generator时,需要使用function*,其他的和定义函数一样。内部使用yield,至于yield的用处以后再说
  • 执行var h = Hello()生成一个Generator对象,经验验证typeof h发现不是普通的函数
  • 执行Hello()之后,Hello内部的代码不会立即执行,而是出于一个暂停状态
  • 执行第一个h.next()时,会激活刚才的暂停状态,开始执行Hello内部的语句,但是,直到遇到yield语句。一旦遇到yield语句时,它就会将yield后面的表达式执行,并返回执行的结果,然后又立即进入暂停状态。
  • 因此第一个console.log(h.next())打印出来的是{ value: 100, done: false }value是第一个yield返回的值,done: false表示目前处于暂停状态,尚未执行结束,还可以再继续往下执行。
  • 执行第二个h.next()和第一个一样,不在赘述。此时会执行完第二个yield后面的表达式并返回结果,然后再次进入暂停状态
  • 执行第三个h.next()时,程序会打破暂停状态,继续往下执行,但是遇到的不是yield而是return。这就预示着,即将执行结束了。因此最后返回的是{ value: 300, done: true }done: true表示执行结束,无法再继续往下执行了。
  • 再去执行第四次h.next()时,就只能得到{ value: undefined, done: true },因为已经结束,没有返回值了。

要点

  • Generator不是函数,不是函数,不是函数
  • Hello()不会立即出发执行,而是一上来就暂停
  • 每次h.next()都会打破暂停状态去执行,直到遇到下一个yield或者return
  • 遇到yield时,会执行yeild后面的表达式,并返回执行之后的值,然后再次进入暂停状态,此时done: false
  • 遇到return时,会返回值,执行结束,即done: true
  • 每次h.next()的返回值永远都是{value: ... , done: ...}的形式

Generator最终如何处理异步操作

readFilePromise('some1.json').then(data => {
    console.log(data)  // 打印第 1 个文件内容
    return readFilePromise('some2.json')
}).then(data => {
    console.log(data)  // 打印第 2 个文件内容
    return readFilePromise('some3.json')
}).then(data => {
    console.log(data)  // 打印第 3 个文件内容
    return readFilePromise('some4.json')
}).then(data=> {
    console.log(data)  // 打印第 4 个文件内容
})

而如果学会Generator那么读取多个文件就是如下这样写

co(function* () {
    const r1 = yield readFilePromise('some1.json')
    console.log(r1)  // 打印第 1 个文件内容
    const r2 = yield readFilePromise('some2.json')
    console.log(r2)  // 打印第 2 个文件内容
    const r3 = yield readFilePromise('some3.json')
    console.log(r3)  // 打印第 3 个文件内容
    const r4 = yield readFilePromise('some4.json')
    console.log(r4)  // 打印第 4 个文件内容
})

Iterator 遍历器

ES6 中引入了很多此前没有但是却非常重要的概念,Iterator就是其中一个。Iterator对象是一个指针对象,实现类似于单项链表的数据结构,通过next()将指针指向下一个节点

Symbol数据类型

Symbol是一个特殊的数据类型,和number string等并列,详细的教程可参考阮一峰老师 ES6 入门的 Symbol 篇

console.log(Array.prototype.slice)  // [Function: slice]
console.log(Array.prototype[Symbol.iterator])  // [Function: values]

数组的slice属性大家都比较熟悉了,就是一个函数,可以通过Array.prototype.slice得到。这里的slice是一个字符串,但是我们获取Array.prototype[Symbol.iterator]可以得到一个函数,只不过这里的[Symbol.iterator]Symbol数据类型,不是字符串。但是没关系,Symbol数据类型也可以作为对象属性的key

var obj = {}
obj.a = 100
obj[Symbol.iterator] = 200
console.log(obj)  // {a: 100, Symbol(Symbol.iterator): 200}

原生具有[Symbol.iterator]属性的数据类型

在 ES6 中,原生具有[Symbol.iterator]属性数据类型有:数组、某些类似数组的对象(如argumentsNodeList)、SetMap。其中,SetMap也是 ES6 中新增的数据类型。

// 数组
console.log([1, 2, 3][Symbol.iterator])  // function values() { [native code] }
// 某些类似数组的对象,NoeList
console.log(document.getElementsByTagName('div')[Symbol.iterator])  // function values() { [native code] }

//原生具有[Symbol.iterator]属性数据类型有一个特点,就是可以使用for...of来取值
var item
for (item of [100, 200, 300]) {
    console.log(item)
}
// 打印出:100 200 300 
// 注意,这里每次获取的 item 是数组的 value,而不是 index ,这一点和 传统 for 循环以及 for...in 完全不一样

生成Iterator对象

定义一个数组,然后生成数组的Iterator对象

const arr = [100, 200, 300]
const iterator = arr[Symbol.iterator]()  // 通过执行 [Symbol.iterator] 的属性值(函数)来返回一个 iterator 对象

现在生成了iterator,那么该如何使用它呢 ———— 有两种方式:nextfor...of

先说第一种,next

console.log(iterator.next())  // { value: 100, done: false }
console.log(iterator.next())  // { value: 200, done: false }
console.log(iterator.next())  // { value: 300, done: false }
console.log(iterator.next())  // { value: undefined, done: true }

// iterator对象可以通过next()方法逐步获取每个元素的值,以{ value: ..., done: ... }形式返回,value就是值,done表示是否到已经获取完成

再说第二种,for...of

let i
for (i of iterator) {
    console.log(i)
}
// 打印:100 200 300 
//上面使用for...of遍历iterator对象,可以直接将其值获取出来。这里的“值”就对应着上面next()返回的结果的value属性

Generator返回的也是Iterator对象

看到这里,你大体也应该明白了,上一节演示的Generator,就是生成一个Iterator对象。因此才会有next(),也可以通过for...of来遍历

function* Hello() {
    yield 100
    yield (function () {return 200})()
    return 300 
}
const h = Hello()
console.log(h[Symbol.iterator])  // [Function: [Symbol.iterator]]

//执行const h = Hello()得到的就是一个iterator对象,因为h[Symbol.iterator]是有值的。既然是iterator对象,那么就可以使用next()和for...of进行操作
console.log(h.next())  // { value: 100, done: false }
console.log(h.next())  // { value: 200, done: false }
console.log(h.next())  // { value: 300, done: false }
console.log(h.next())  // { value: undefined, done: true }

let i
for (i of h) {
    console.log(i)
}

Generator 的具体应用

介绍了Generator可以让执行处于暂停状态,并且知道了Generator返回的是一个Iterator对象

nextyield参数传递

我们之前已经知道,yield具有返回数据的功能,如下代码。yield后面的数据被返回,存放到返回结果中的value属性中。这算是一个方向的参数传递。

function* G() {
    yield 100
}
const g = G()
console.log( g.next() ) // {value: 100, done: false}

还有另外一个方向的参数传递,就是nextyield传递,如下代码

function* G() {
    const a = yield 100
    console.log('a', a)  // a aaa
    const b = yield 200
    console.log('b', b)  // b bbb
    const c = yield 300
    console.log('c', c)  // c ccc
}
const g = G()
g.next()    // value: 100, done: false
g.next('aaa') // value: 200, done: false
g.next('bbb') // value: 300, done: false
g.next('ccc') // value: undefined, done: true

执行过程

  • 执行第一个g.next()时,为传递任何参数,返回的{value: 100, done: false},这个应该没有疑问
  • 执行第二个g.next('aaa')时,传递的参数是'aaa',这个'aaa'就会被赋值到G内部的a标量中,然后执行console.log('a', a)打印出来,最后返回{value: 200, done: false}
  • 执行第三个、第四个时,道理都是完全一样的,大家自己捋一捋

有一个要点需要注意,就g.next('aaa')是将'aaa'传递给上一个已经执行完了的yield语句前面的变量,而不是即将执行的yield前面的变量

for...of的应用示例

针对for...ofIterator对象的操作之前已经介绍过了,不过这里用一个非常好的例子来展示一下。用简单几行代码实现斐波那契数列。通过之前学过的Generator知识,应该不能解读这份代码

function* fibonacci() {
    let [prev, curr] = [0, 1]
    for (;;) {
        [prev, curr] = [curr, prev + curr]
        // 将中间值通过 yield 返回,并且保留函数执行的状态,因此可以非常简单的实现 fibonacci
        yield curr
    }
}
for (let n of fibonacci()) {
    if (n > 1000) {
        break
    }
    console.log(n)
}

yield*语句

如果有两个Generator,想要在第一个中包含第二个,如下需求

function* G1() {
    yield 'a'
    yield 'b'
}
function* G2() {
    yield 'x'
    yield 'y'
}

针对以上两个Generator,我的需求是:一次输出a x y b,该如何做?有同学看到这里想起了刚刚学到的for..of可以实现

这要演示一个更加简洁的方式yield*表达式

function* G1() {
    yield 'a'
    yield* G2()  // 使用 yield* 执行 G2()
    yield 'b'
}
function* G2() {
    yield 'x'
    yield 'y'
}
for (let item of G1()) {
    console.log(item)
}

之前学过的yield后面会接一个普通的 JS 对象,而yield*后面会接一个Generator,而且会把它其中的yield按照规则来一步一步执行。如果有多个Generator串联使用的话(例如Koa源码中),用yield*来操作非常方便。

Generator中的this

对于以下这种写法,大家可能会和构造函数创建对象的写法产生混淆,这里一定要注意 —— Generator 不是函数,更不是构造函数

function* G() {}
const g = G()

而以下这种写法,更加不会成功。只有构造函数才会这么用,构造函数返回的是this,而Generator返回的是一个Iterator对象。完全是两码事,千万不要搞混了。

function* G() {
    this.a = 10
}
const g = G()
console.log(g.a) // 报错

Thunk 函数

要想让Generator和异步操作产生联系,就必须过thunk函数这一关。这一关过了之后,立即就可以着手异步操作的事情,因此大家再坚持坚持。至于thunk函数是什么,下文会详细演示。

一个普通的异步函数

就用 nodejs 中读取文件的函数为例

fs.readFile('data1.json', 'utf-8', (err, data) => {
    // 获取文件内容
})

其实这个写法就是将三个参数都传递给fs.readFile这个方法,其中最后一个参数是一个callback函数。这种函数叫做 多参数函数,我们接下来做一个改造

封装成一个thunk函数

改造的代码如下所示。不过是不是感觉越改造越复杂了?不过请相信:你看到的复杂仅仅是表面的,这一点东西变的复杂,是为了让以后更加复杂的东西变得简单。对于个体而言,随性比较简单,遵守规则比较复杂;但是对于整体(包含很多个体)而言,大家都随性就不好控制了,而大家都遵守规则就很容易管理

const thunk = function (fileName, codeType) {
    // 返回一个只接受 callback 参数的函数
    return function (callback) {
        fs.readFile(fileName, codeType, callback)
    }
}
const readFileThunk = thunk('data1.json', 'utf-8')
readFileThunk((err, data) => {
    // 获取文件内容
})
  • 执行const readFileThunk = thunk('data1.json', 'utf-8')返回的其实是一个函数
  • readFileThunk这个函数,只接受一个参数,而且这个参数是一个callback函数

thunk函数的特点

就上上面的代码,我们经过对传统的异步操作函数进行封装,得到一个只有一个参数的函数,而且这个参数是一个callback函数,那这就是一个thunk函数。就像上面代码中readFileThunk一样。

使用thunkify

上面代码的封装,是我们手动来做的,但是没遇到一个情况就需要手动做吗?在这个开源的时代当让不会这样,直接使用第三方的thunkify就好了。

首先要安装npm i thunkify --save,然后在代码的最上方引用const thunkify = require('thunkify')。最后,上面我们手动写的代码,完全可以简化成这几行,非常简单!

const thunk = thunkify(fs.readFile)
const readFileThunk = thunk('data1.json', 'utf-8')
readFileThunk((err, data) => {
    // 获取文件内容
})

Generator 与异步操作

Genertor中使用thunk函数

代码中表达的意思,是要依次读取两个文件的内容

const readFileThunk = thunkify(fs.readFile)
const gen = function* () {
    const r1 = yield readFileThunk('data1.json')
    console.log(r1)
    const r2 = yield readFileThunk('data2.json')
    console.log(r2)
}

挨个读取两个文件的内容

接着以上的代码继续写,注释写的非常详细,大家自己去看,看完自己写代码亲身体验。

const g = gen()

// 试着打印 g.next() 这里一定要明白 value 是一个 thunk函数 ,否则下面的代码你都看不懂
// console.log( g.next() )  // g.next() 返回 {{ value: thunk函数, done: false }} 

// 下一行中,g.next().value 是一个 thunk 函数,它需要一个 callback 函数作为参数传递进去
g.next().value((err, data1) => {
    // 这里的 data1 获取的就是第一个文件的内容。下一行中,g.next(data1) 可以将数据传递给上面的 r1 变量,此前已经讲过这种参数传递的形式
    // 下一行中,g.next(data1).value 又是一个 thunk 函数,它又需要一个 callback 函数作为参数传递进去
    g.next(data1).value((err, data2) => {
        // 这里的 data2 获取的是第二个文件的内容,通过 g.next(data2) 将数据传递个上面的 r2 变量
        g.next(data2)
    })
})

也许上面的代码给你带来的感觉并不好,第一它逻辑复杂,第二它也不是那么易读、简洁呀,用Generator实现异步操作就是这个样子的?

自驱动流程

以上代码中,读取两个文件的内容都是手动一行一行写的,而我们接下来要做一个自驱动的流程,定义好Generator的代码之后,就让它自动执行。完整的代码如下所示:

// 自动流程管理的函数
function run(generator) {
    const g = generator()
    function next(err, data) {
        const result = g.next(data)  // 返回 { value: thunk函数, done: ... }
        if (result.done) {
            // result.done 表示是否结束,如果结束了那就 return 作罢
            return
        }
        result.value(next)  // result.value 是一个 thunk 函数,需要一个 callback 函数作为参数,而 next 就是一个 callback 形式的函数
    }
    next() // 手动执行以启动第一次 next
}

// 定义 Generator
const readFileThunk = thunkify(fs.readFile)
const gen = function* () {
    const r1 = yield readFileThunk('data1.json')
    console.log(r1.toString())
    const r2 = yield readFileThunk('data2.json')
    console.log(r2.toString())
}

// 启动执行
run(gen)

其实这段代码和上面的手动编写读取两个文件内容的代码,原理上是一模一样的,只不过这里把流程驱动给封装起来了。我们简单分析一下这段代码

  • 最后一行run(gen)之后,进入run函数内部执行
  • const g = generator()创建Generator实例,然后定义一个next方法,并且立即执行next()
  • 注意这个next函数的参数是err, data两个,和我们fs.readFile用到的callback函数形式完全一样
  • 第一次执行next时,会执行const result = g.next(data),而g.next(data)返回的是{ value: thunk函数, done: ... }value是一个thunk函数,done表示是否结束
  • 如果done: true,那就直接return了,否则继续进行
  • result.value是一个thunk函数,需要接受一个callback函数作为参数传递进去,因此正好把next给传递进去,让next一直被执行下去

使用co

使用之前请安装npm i co --save,然后在文件开头引用const co = require('co')co到底有多好用,我们将刚才的代码用co重写,就变成了如下代码。非常简洁

// 定义 Generator
const readFileThunk = thunkify(fs.readFile)
const gen = function* () {
    const r1 = yield readFileThunk('data1.json')
    console.log(r1.toString())
    const r2 = yield readFileThunk('data2.json')
    console.log(r2.toString())
}
const c = co(gen)


//而且const c = co(gen)返回的是一个Promise对象,可以接着这么写
c.then(data => {
    console.log('结束')
})

co库和Promise

刚才提到co()最终返回的是Promise对象,后知后觉,我们已经忘记Promise好久了,现在要重新把它拾起来。如果使用co来处理Generator的话,其实yield后面可以跟thunk函数,也可以跟Promise对象。

thunk函数上文一直在演示,下面演示一下Promise对象的,也权当再回顾一下久别的Promise。其实从形式上和结果上,都跟thunk函数一样。

const readFilePromise = Q.denodeify(fs.readFile)

const gen = function* () {
    const r1 = yield readFilePromise('data1.json')
    console.log(r1.toString())
    const r2 = yield readFilePromise('data2.json')
    console.log(r2.toString())
}

co(gen)

koa 中使用 Generator

koa 是一个 nodejs 开发的 web 框架,所谓 web 框架就是处理 http 请求的。开源的 nodejs 开发的 web 框架最初是 express

我们此前说过,既然是处理 http 请求,是一种网络操作,肯定就会用到异步操作。express 使用的异步操作是传统的callbck,而 koa 用的是我们刚刚讲的Generator(koa v1.x用的是Generator,已经被广泛使用,而 koa v2.x用到了 ES7 中的async-await,不过因为 ES7 没有正式发布,所以 koa v2.x也没有正式发布,不过可以试用)

koa 是由 express 的原班开发人员开发的,比 express 更加简洁易用,因此 koa 是目前最为推荐的 nodejs web 框架。阿里前不久就依赖于 koa 开发了自己的 nodejs web 框架 egg

koa 是一个 web 框架,处理 http 请求,但是这里我们不去管它如何处理 http 请求,而是直接关注它使用Genertor的部分-中间件。

例如,我们现在要用 3 个Generator输出12345

let info = ''
function* g1() {
    info += '1'  // 拼接 1
    yield* g2()  // 拼接 234
    info += '5'  // 拼接 5
}
function* g2() {
    info += '2'  // 拼接 2
    yield* g3()  // 拼接 3
    info += '4'  // 拼接 4
}
function* g3() {
    info += '3'  // 拼接 3
}

var g = g1()
g.next()
console.log(info)  // 12345

但是如果用 koa 的 中间件 的思路来做,就需要如下这么写。

app.use(function *(next){
    this.body = '1';
    yield next;
    this.body += '5';
    console.log(this.body);
});
app.use(function *(next){
    this.body += '2';
    yield next;
    this.body += '4';
});
app.use(function *(next){
    this.body += '3';
});
  • app.use()中传入的每一个Generator就是一个 中间件,中间件按照传入的顺序排列,顺序不能乱

每个中间件内部,next表示下一个中间件。yield next就是先将程序暂停,先去执行下一个中间件,等next被执行完之后,再回过头来执行当前代码的下一行。因此,koa 的中间件执行顺序是一种洋葱圈模型,不过这里看不懂也没问题。

  • 每个中间件内部,this可以共享变量。即第一个中间件改变了this的属性,在第二个中间件中可以看到效果。

koa 的这种应用机制是如何实现的

class MyKoa extends Object {
    constructor(props) {
        super(props);

        // 存储所有的中间件
        this.middlewares = []
    }

    // 注入中间件
    use (generator) {
        this.middlewares.push(generator)
    }

    // 执行中间件
    listen () {
        this._run()
    }

    _run () {
        const ctx = this
        const middlewares = ctx.middlewares
        co(function* () {
            let prev = null
            let i = middlewares.length
            //从最后一个中间件到第一个中间件的顺序开始遍历
            while (i--) {
                // ctx 作为函数执行时的 this 才能保证多个中间件中数据的共享
                //prev 将前面一个中间件传递给当前中间件,才使得中间件里面的 next 指向下一个中间件
                prev = middlewares[i].call(ctx, prev);
            }
            //执行第一个中间件
            yield prev;
        })
    }
}


//最后我们执行代码实验一下效果

var app = new MyKoa();
app.use(function *(next){
    this.body = '1';
    yield next;
    this.body += '5';
    console.log(this.body);  // 12345
});
app.use(function *(next){
    this.body += '2';
    yield next;
    this.body += '4';
});
app.use(function *(next){
    this.body += '3';
});
app.listen();

Generator 的本质是什么?是否取代了 callback

其实标题中的问题,是一个伪命题,因为Generatorcallback根本没有任何关系,只是我们通过一些方式(而且是很复杂的方式)强行将他俩产生了关系,才会有现在的Generator处理异步。

Generator的本质

暂停 这个词 —“暂停”才是Generator的本质 —只有Generator能让一段程序执行到指定的位置先暂停,然后再启动,再暂停,再启动

而这个 暂停 就很容易让它和异步操作产生联系,因为我们在处理异步操作时,即需要一种“开始读取文件,然后暂停一下,等着文件读取完了,再干嘛干嘛...”这样的需求。因此将Generator和异步操作联系在一起,并且产生一些比较简明的解决方案,这是顺其自然的事儿,大家要想明白这个道理。

不过,JS 还是 JS,单线程还是单线程,异步还是异步,callback还是callback。这一切都不会因为有一个Generator而有任何变化。

callback的结合

之前在介绍Promise的最后,拿Promisecallback做过一些比较,最后发现Promise其实是利用了callback才能实现的。而这里,Generator也必须利用callback才能实现。

拿介绍co时的代码举例(代码如下),如果yield后面用的是thunk函数,那么thunk函数需要的就是一个callback参数。如果yield后面用的是Promise对象,Promisecallback的联系之前已经介绍过了。

co(function* () {
    const r1 = yield readFilePromise('some1.json')
    console.log(r1)  // 打印第 1 个文件内容
    const r2 = yield readFileThunk('some2.json')
    console.log(r2)  // 打印第 2 个文件内容
})

因此,Generator离不开callbackPromise离不开callback,异步也离不开callback

 

posted @ 2020-08-16 16:59  JackieDYH  阅读(5)  评论(0编辑  收藏  举报  来源