转载请注明出处: Generator函数异步应用

上一篇文章详细的介绍了Generator函数的语法,这篇文章来说一下如何使用Generator函数来实现异步编程。

或许用Generator函数来实现异步会很少见,因为ECMAScript 2016的async函数对Generator函数的流程控制做了一层封装,使得异步方案使用更加方便。

但是呢,我个人认为学习async函数之前,有必要了解一下Generator如何实现异步,这样对于async函数的学习或许能给予一些帮助。

文章目录

  1. 知识点简单回顾
  2. 异步任务的封装
  3. thunk函数实现流程控制
  4. Generator函数的自动流程控制
  5. 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异步应用的相关知识也就差不多了,现在稍微总结一下。

  1. 由于yield表达式可以暂停执行,next方法可以恢复执行,这使得Generator函数很适合用来将异步任务同步化。
  2. 但是Generator函数的流程控制会稍显麻烦,因为每次都需要手动执行next方法来恢复函数执行,并且向next方法传递参数以输出上一个yiled表达式的返回值。
  3. 于是就有了thunk(thunkify)函数和co模块来实现Generator函数的自动流程控制。
  4. 通过thunk(thunkify)函数分离参数,以闭包的形式将参数逐一传入,再通过apply或者call方法调用,然后配合使用run函数可以做到自动流程控制。
  5. 通过co模块,实际上就是将run函数和thunk(thunkify)函数进行了封装,并且yield表达式同时支持thunk(thunkify)函数和Promise对象两种形式,使得自动流程控制更加的方便。

参考资料

  1. Generator 函数的异步应用
  2. node-thunkify
  3. co

Generator函数异步应用的更多相关文章

  1. Generator 和 函数异步应用 笔记

    Generator > ES6 提供的一种异步编程解决方案 > Generator 函数是一个状态机,封装了多个内部状态.还是一个遍历器对象生成函数.返回<label>遍历器对 ...

  2. 转: ES6异步编程:Generator 函数的含义与用法

    转: ES6异步编程:Generator 函数的含义与用法 异步编程对 JavaScript 语言太重要.JavaScript 只有一根线程,如果没有异步编程,根本没法用,非卡死不可. 以前,异步编程 ...

  3. js-ES6学习笔记-Generator函数的异步应用

    1.ES6 诞生以前,异步编程的方法,大概有下面四种. 回调函数 事件监听 发布/订阅 Promise 对象 Generator 函数将 JavaScript 异步编程带入了一个全新的阶段. 2.所谓 ...

  4. 16.Generator 函数的异步应用

    Generator 函数的异步应用 Generator 函数的异步应用 异步编程对 JavaScript 语言太重要.Javascript 语言的执行环境是"单线程"的,如果没有异 ...

  5. 17.Generator函数的异步应用

    异步编程对 JavaScript 语言太重要.Javascript 语言的执行环境是“单线程”的,如果没有异步编程,根本没法用,非卡死不可. 1.传统方法 ES6 诞生以前,异步编程的方法,大概有下面 ...

  6. ES6的新特性(17)——Generator 函数的异步应用

    Generator 函数的异步应用 异步编程对 JavaScript 语言太重要.Javascript 语言的执行环境是“单线程”的,如果没有异步编程,根本没法用,非卡死不可.本章主要介绍 Gener ...

  7. Generator 函数的异步应用

    异步编程对 JavaScript 语言太重要.Javascript 语言的执行环境是“单线程”的,如果没有异步编程,根本没法用,非卡死不可.本章主要介绍 Generator 函数如何完成异步操作. 传 ...

  8. ES6学习笔记(十五)Generator函数的异步应用

    1.传统方法 ES6 诞生以前,异步编程的方法,大概有下面四种. 回调函数 事件监听 发布/订阅 Promise 对象 Generator 函数将 JavaScript 异步编程带入了一个全新的阶段. ...

  9. es6 generator函数的异步编程

    es6 generator函数,我们都知道asycn和await是generator函数的语法糖,那么genertaor怎么样才能实现asycn和await的功能呢? 1.thunk函数    将函数 ...

随机推荐

  1. 微信公众号开发——通过ffmpeg解决amr文件无法播放问题

    今天刚好碰到个需求,要在微信浏览器中实现录音,并在其他页面上播放.录音功能本身是JS SDK的功能,倒没啥问题,然而录音的文件保存下来是amr格式,而IOS的浏览器没法播放amr(据说微信浏览器的vi ...

  2. 冒烟测试与BVT测试

    冒烟测试,它和回归测试的性质一样--只是一个测试活动,并不是一个测试阶段.冒烟测试贯穿于测试的任何一个阶段,单元测试.集成测试.系统测试里都有冒烟测试. 冒烟测试和其他所有的测试活动的目的不一样,它不 ...

  3. 微信小程序语音识别开发过程记录 微信小程序silk转mp3 silk转wav 以及ffmpeg使用

    说说最近在开发微信小程序语音识别遇到的问题吧 最先使用微信小程序录音控件可以拿到silk格式,后来微信官方又支持mp3格式了 但是我们拿到这些格式以后,都还不能直接使用,做语音识别,因为目前百度的语音 ...

  4. 使用XML序列化实现系统配置 - 开源研究系列文章

    在实际的C#软件系统开发过程中,会遇到系统配置的保存问题,以及系统存储问题.在以前的系统开发过程中,笔者使用的是INI文件配置管理的方式.到了现在,INI文件配置保存仍然是一个平常使用的方式.在博客园 ...

  5. Head First设计模式之解释器模式

    一.定义 给定一个语言,定义它的文法表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子. 主要解决:对于一些固定文法构建一个解释句子的解释器. 何时使用:如果一种特定类型的问题发生的频率足 ...

  6. 使用linux perf工具生成java程序火焰图

    pre.cjk { font-family: "Nimbus Mono L", monospace } p { margin-bottom: 0.1in; line-height: ...

  7. wc--Linux

    这个命令的功能也很好记,因为它功能很有限: wc -c filename:显示一个文件的字节数 wc -m filename:显示一个文件的字符数 wc -l filename:显示一个文件的行数 w ...

  8. Java学习笔记12(面向对象五:构造方法、this再探)

    在开发中,经常需要在创建对象的同时明确对象对的属性值, 比如一个Person对象创建时候就应该有age和name等属性 那么如何做到在创建对象的同时给对象的属性初始化值呢? 这里介绍构造方法: 1.构 ...

  9. 【读书笔记】【深入理解ES6】#3-函数

    函数形参的默认值 ES6中的默认参数值 function makeRequest(url, timeout = 2000, callback = function() {}) { } 可以为任意参数指 ...

  10. nodejs 做后台的一个完整业务整理

    大家知道js现在不仅仅可以写前端界面而且可以写后端的业务了,这样js就可以写一个全栈的项目.这里介绍一个nodejs + express + mongodb + bootstap 的全栈项目. 1.安 ...