Generator函数异步应用
转载请注明出处: 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对象两种形式,使得自动流程控制更加的方便。
参考资料
Generator函数异步应用的更多相关文章
- Generator 和 函数异步应用 笔记
Generator > ES6 提供的一种异步编程解决方案 > Generator 函数是一个状态机,封装了多个内部状态.还是一个遍历器对象生成函数.返回<label>遍历器对 ...
- 转: ES6异步编程:Generator 函数的含义与用法
转: ES6异步编程:Generator 函数的含义与用法 异步编程对 JavaScript 语言太重要.JavaScript 只有一根线程,如果没有异步编程,根本没法用,非卡死不可. 以前,异步编程 ...
- js-ES6学习笔记-Generator函数的异步应用
1.ES6 诞生以前,异步编程的方法,大概有下面四种. 回调函数 事件监听 发布/订阅 Promise 对象 Generator 函数将 JavaScript 异步编程带入了一个全新的阶段. 2.所谓 ...
- 16.Generator 函数的异步应用
Generator 函数的异步应用 Generator 函数的异步应用 异步编程对 JavaScript 语言太重要.Javascript 语言的执行环境是"单线程"的,如果没有异 ...
- 17.Generator函数的异步应用
异步编程对 JavaScript 语言太重要.Javascript 语言的执行环境是“单线程”的,如果没有异步编程,根本没法用,非卡死不可. 1.传统方法 ES6 诞生以前,异步编程的方法,大概有下面 ...
- ES6的新特性(17)——Generator 函数的异步应用
Generator 函数的异步应用 异步编程对 JavaScript 语言太重要.Javascript 语言的执行环境是“单线程”的,如果没有异步编程,根本没法用,非卡死不可.本章主要介绍 Gener ...
- Generator 函数的异步应用
异步编程对 JavaScript 语言太重要.Javascript 语言的执行环境是“单线程”的,如果没有异步编程,根本没法用,非卡死不可.本章主要介绍 Generator 函数如何完成异步操作. 传 ...
- ES6学习笔记(十五)Generator函数的异步应用
1.传统方法 ES6 诞生以前,异步编程的方法,大概有下面四种. 回调函数 事件监听 发布/订阅 Promise 对象 Generator 函数将 JavaScript 异步编程带入了一个全新的阶段. ...
- es6 generator函数的异步编程
es6 generator函数,我们都知道asycn和await是generator函数的语法糖,那么genertaor怎么样才能实现asycn和await的功能呢? 1.thunk函数 将函数 ...
随机推荐
- 微信公众号开发——通过ffmpeg解决amr文件无法播放问题
今天刚好碰到个需求,要在微信浏览器中实现录音,并在其他页面上播放.录音功能本身是JS SDK的功能,倒没啥问题,然而录音的文件保存下来是amr格式,而IOS的浏览器没法播放amr(据说微信浏览器的vi ...
- 冒烟测试与BVT测试
冒烟测试,它和回归测试的性质一样--只是一个测试活动,并不是一个测试阶段.冒烟测试贯穿于测试的任何一个阶段,单元测试.集成测试.系统测试里都有冒烟测试. 冒烟测试和其他所有的测试活动的目的不一样,它不 ...
- 微信小程序语音识别开发过程记录 微信小程序silk转mp3 silk转wav 以及ffmpeg使用
说说最近在开发微信小程序语音识别遇到的问题吧 最先使用微信小程序录音控件可以拿到silk格式,后来微信官方又支持mp3格式了 但是我们拿到这些格式以后,都还不能直接使用,做语音识别,因为目前百度的语音 ...
- 使用XML序列化实现系统配置 - 开源研究系列文章
在实际的C#软件系统开发过程中,会遇到系统配置的保存问题,以及系统存储问题.在以前的系统开发过程中,笔者使用的是INI文件配置管理的方式.到了现在,INI文件配置保存仍然是一个平常使用的方式.在博客园 ...
- Head First设计模式之解释器模式
一.定义 给定一个语言,定义它的文法表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子. 主要解决:对于一些固定文法构建一个解释句子的解释器. 何时使用:如果一种特定类型的问题发生的频率足 ...
- 使用linux perf工具生成java程序火焰图
pre.cjk { font-family: "Nimbus Mono L", monospace } p { margin-bottom: 0.1in; line-height: ...
- wc--Linux
这个命令的功能也很好记,因为它功能很有限: wc -c filename:显示一个文件的字节数 wc -m filename:显示一个文件的字符数 wc -l filename:显示一个文件的行数 w ...
- Java学习笔记12(面向对象五:构造方法、this再探)
在开发中,经常需要在创建对象的同时明确对象对的属性值, 比如一个Person对象创建时候就应该有age和name等属性 那么如何做到在创建对象的同时给对象的属性初始化值呢? 这里介绍构造方法: 1.构 ...
- 【读书笔记】【深入理解ES6】#3-函数
函数形参的默认值 ES6中的默认参数值 function makeRequest(url, timeout = 2000, callback = function() {}) { } 可以为任意参数指 ...
- nodejs 做后台的一个完整业务整理
大家知道js现在不仅仅可以写前端界面而且可以写后端的业务了,这样js就可以写一个全栈的项目.这里介绍一个nodejs + express + mongodb + bootstap 的全栈项目. 1.安 ...