转载请注明出处: Generator函数异步应用
上一篇文章详细的介绍了Generator函数的语法,这篇文章来说一下如何使用Generator函数来实现异步编程。
或许用Generator函数来实现异步会很少见,因为ECMAScript 2016的async函数对Generator函数的流程控制做了一层封装,使得异步方案使用更加方便。
但是呢,我个人认为学习async函数之前,有必要了解一下Generator如何实现异步,这样对于async函数的学习或许能给予一些帮助。
文章目录
- 知识点简单回顾
- 异步任务的封装
- thunk函数实现流程控制
- Generator函数的自动流程控制
- co模块的自动流程控制
知识点简单回顾
在Generator函数语法解析篇的文章中有说到,Generator函数可以定义多个内部状态,同时也是遍历器对象生成函数。yield表达式可以定义多个内部状态,同时还具有暂停函数执行的功能。调用Generator函数的时候,不会立即执行,而是返回遍历器对象。
遍历器对象的原型对象上具有next方法,可以通过next方法恢复函数的执行。每次调用next方法,都会在遇到yield表达式时停下来,再次调用的时候,会在停下的位置继续执行。调用next方法会返回具有value和done属性的对象,value属性表示当前的内部状态,可能的值有yield表达式后面的值、return语句后面的值和undefined;done属性表示遍历是否结束。
yield表达式默认是没有返回值的,或者说,返回值为undefined。因此,想要获得yield表达式的返回值,就需要给next方法传递参数。next方法的参数表示上一个yield表达式的返回值。因此在调用第一个next方法时可以不传递参数(即使传递参数也不会起作用),此时表示启动遍历器对象。所以next方法会比yield表达式的使用要多一次。
更加详细的语法可以参考这篇文章。传送门:Generator函数语法解析
异步任务的封装
yield表达式可以暂停函数执行,next方法可以恢复函数执行。这使得Generator函数非常适合将异步任务同步化。接下来会使用setTimeout来模拟异步任务。
const person = sex => {
return new Promise((resolve, reject) => {
window.setTimeout(() => {
const data = {
sex,
name: ‘keith‘,
height: 180
}
resolve(data)
}, 1000)
})
}
function *gen () {
const data = yield person(‘boy‘)
console.log(data)
}
const g = gen()
const next1 = g.next() // {value: Promise, done: false}
next1.value.then(data => {
g.next(data)
})
从上面代码可以看出,第一次调用next方法时,启动了遍历器对象,此时返回了包含value和done属性的对象,由于value属性值是promise对象,因此可以使用then方法获取到resolve传递过来的值,再使用带有data参数的next方法给上一个yield表达式传递返回值。
此时在const data = yield person()
这句语句中,就可以得到异步任务传递的参数值了,实现了异步任务的同步化。
但是上面的代码会有问题。每次获取异步的值时,都要手动执行以下步骤
const g = gen()
const next1 = g.next() {value: Promise, done: false}
next1.value.then(data => {
g.next(data)
})
上面的代码实质上就是每次都会重复使用value属性值和next方法,所以每次使用Generator实现异步都会涉及到流程控制的问题。每次都手动实现流程控制会显得麻烦,有没有什么办法可以实现自动流程控制呢?实际上是有的: )
thunk函数实现流程控制
thunk函数实际上有些类似于JavaScript函数柯里化,会将某个函数作为参数传递到另一个函数中,然后通过闭包的方式为参数(函数)传递参数进而实现求值。
函数柯里化实现的过程如下
function curry (fn) {
const args1 = Array.prototype.slice.call(arguments, 1)
return function () {
const args2 = Array.from(arguments)
const arr = args1.concat(args2)
return fn.apply(this, arr)
}
}
使用curry函数来举一个例子: )
// 需要柯里化的sum函数
const sum = (a, b) => {
return a + b
}
curry(sum, 1)(2) // 3
而thunk函数简单的实现思路如下:
// ES5实现
const thunk = fn => {
return function () {
const args = Array.from(arguments)
return function (callback) {
args.push(callback)
return fn.apply(this, args)
}
}
}
// ES6实现
const thunk = fn => {
return function (...args) {
return function (callback) {
return fn.call(this, ...args, callback)
}
}
}
从上面thunk函数中,会发现,thunk函数比函数curry化多用了一层闭包来封装函数作用域。
使用上面的thunk函数,可以生成fs.readFile
的thunk函数。
const fs = require(‘fs‘)
const readFileThunk = thunk(fs.readFile)
readFileThunk(fileA)(callback)
使用thunk函数将fs.readFile
包装成readFileThunk
函数,然后在通过fileA
传入文件路径,callback
参数则为fs.readFile
的回调函数。
当然,还有一个thunk函数的升级版本thunkify函数,可以使得回调函数只执行一次。原理和上面的thunk函数非常像,只不过多了一个flag参数用于限制回调函数的执行次数。下面我对thunkify函数做了一些修改。源码地址: node-thunkify
const thunkify = fn => {
return function () {
const args = Array.from(arguments)
return function (callback) {
let called = false
// called变量限制callback的执行次数
args.push(function () {
if (called) return
called = true
callback.apply(this, arguments)
})
try {
fn.apply(this, args)
} catch (err) {
callback(err)
}
}
}
}
举个例子看看: )
function sum (a, b, callback) {
const total = a + b
console.log(total)
console.log(total)
}
// 如果使用thunkify函数
const sumThunkify = thunkify(sum)
sumThunkify(1, 2)(console.log)
// 打印出3
// 如果使用thunk函数
const sumThunk = thunk(sum)
sumThunk(1, 2)(console.log)
// 打印出 3, 3
再来看一个使用setTimeout模拟异步并且使用thunkify模块来完成异步任务同步化的例子。
const person = (sex, fn) => {
window.setTimeout(() => {
const data = {
sex,
name: ‘keith‘,
height: 180
}
fn(data)
}, 1000)
}
const personThunk = thunkify(person)
function *gen () {
const data = yield personThunk(‘boy‘)
console.log(data)
}
const g = gen()
const next = g.next()
next.value(data => {
g.next(data)
})
从上面代码可以看出,value属性实际上就是thunkify函数的回调函数(也是person的第二个参数),而‘boy‘则是person的第一个参数。
Generator函数的自动流程控制
在上面的代码中,我们可以将调用遍历器对象生成函数,返回遍历器和手动执行next方法以恢复函数执行的过程封装起来。
const run = gen => {
const g = gen()
const next = data => {
let result = g.next(data)
if (result.done) return result.value
result.value(next)
}
next()
}
使用run函数封装起来之后,run内部的next函数实际上就是thunk(thunkify)函数的回调函数了。因此,调用run即可实现Generator的自动流程控制。
const person = (sex, fn) => {
window.setTimeout(() => {
const data = {
sex,
name: ‘keith‘,
height: 180
}
fn(data)
}, 1000)
}
const personThunk = thunkify(person)
function *gen () {
const data = yield personThunk(‘boy‘)
console.log(data)
}
run(gen)
// {sex: ‘boy‘, name: ‘keith‘, height: 180}
有了这个执行器,执行Generator函数就方便多了。不管内部有多少个异步操作,直接把Generator函数传入run函数即可。当然,前提是每一个异步操作,都要是thunk(thunkify)函数。也就是说,跟在yield表达式后面的必须是thunk(thunkify)函数。
const gen = function *gen () {
const f1 = yield personThunk(‘boy‘) // 跟在yield表达式后面的异步行为必须使用thunk(thunkify)函数封装
const f2 = yield personThunk(‘boy‘)
// ...
const fn = yield personThunk(‘boy‘)
}
run(gen) // run函数的自动流程控制
上面代码中,函数gen封装了n个异步行为,只要执行run函数,这些操作就会自动完成。这样一来,异步操作不仅可以写得像同步操作,而且一行代码就可以执行。
co模块的自动流程控制
在上面的例子说过,表达式后面的值必须是thunk(thunkify)函数,这样才能实现Generator函数的自动流程控制。thunk函数的实现是基于回调函数的,而co模块则更进一步,可以兼容thunk函数和Promise对象。先来看看co模块的基本用法
const co = require(‘co‘)
const gen = function *gen () {
const f1 = yield person(‘boy‘) // 调用person,返回一个promise对象
const f2 = yield person(‘boy‘)
}
co(gen) // 将thunk(thunkify)函数和run函数封装成了co模块,yield表达式后面可以是thunk(thunkify)函数或者Promise对象
co模块可以不用编写Generator函数的执行器,因为它已经封装好了。将Generator函数co模块中,函数就会自动执行。
co函数返回一个Promise对象,因此可以用then方法添加回调函数。
co(gen).then(function (){
console.log(‘Generator 函数执行完成‘)
})
co模块原理;co模块其实就是将两种自动执行器(thunk(thunkify)函数和Promise对象),包装成一个模块。使用co模块的前提条件是,Generator函数的yield表达式后面,只能是thunk(thunkify)或者Promise对象,如果是数组或对象的成员全部都是promise对象,也可以使用co模块。
基于Promise对象的自动执行
还是使用上面例子,不过这次是将回调函数改成Promise对象来实现自动流程控制。
const person = (sex, fn) => {
return new Promise((resolve, reject) => {
window.setTimeout(() => {
const data = {
name: ‘keith‘,
height: 180
}
resolve(data)
}, 1000)
})
}
function *gen () {
const data = yield person(‘boy‘)
console.log(data) // {name: ‘keith‘, height: 180}
}
const g = gen()
g.next().value.then(data => {
g.next(data)
})
手动执行实际上就是层层使用then方法和next方法。根据这个可以写出自动执行器。
const run = gen => {
const g = gen()
const next = data => {
let result = g.next(data)
if (result.done) return result.value
result.value.then(data => {
next(data)
})
}
next()
}
run(gen) // {name: ‘keith‘, height: 180}
如果对co模块感兴趣的朋友,可以阅读一下它的源码。传送门:co
关于Generator异步应用的相关知识也就差不多了,现在稍微总结一下。
- 由于yield表达式可以暂停执行,next方法可以恢复执行,这使得Generator函数很适合用来将异步任务同步化。
- 但是Generator函数的流程控制会稍显麻烦,因为每次都需要手动执行next方法来恢复函数执行,并且向next方法传递参数以输出上一个yiled表达式的返回值。
- 于是就有了thunk(thunkify)函数和co模块来实现Generator函数的自动流程控制。
- 通过thunk(thunkify)函数分离参数,以闭包的形式将参数逐一传入,再通过apply或者call方法调用,然后配合使用run函数可以做到自动流程控制。
- 通过co模块,实际上就是将run函数和thunk(thunkify)函数进行了封装,并且yield表达式同时支持thunk(thunkify)函数和Promise对象两种形式,使得自动流程控制更加的方便。
参考资料
原文地址:https://www.cnblogs.com/unclekeith/p/8335279.html