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函数 将函数 ...
随机推荐
- 利用generator自动生成model(实体)、dao(接口)、mapper(映射)
1 在MySQL数据库中创建相应的表 /* Navicat MySQL Data Transfer Source Server : 虚拟机_zeus01 Source Server Version : ...
- Linux(CentOS6.5)下编译安装MySQL Community Server 5.7.12
组件 官方网站 直接下载地址 备注 mysql http://dev.mysql.com/downloads/mysql/ http://mirrors.sohu.com/mysql/MySQL- ...
- Q:算法(第四版)—第一章
1.1.14:编写一个静态方法lg(),接受一个整型参数N,返回不大于log2N的最大整数(ps:不使用Math库) 分析: 利用将公式k=log2N转化为N=2k的原理,不断的逼近其输入的值N,当N ...
- 封装简单的equery
/** * Created by wang on 2016/3/23. */ //绑定操作 function bindEvent(obj,events,fn){ if (obj.addEventLis ...
- Git详解之七:自定义Git
自定义 Git 到目前为止,我阐述了 Git 基本的运作机制和使用方式,介绍了 Git 提供的许多工具来帮助你简单且有效地使用它. 在本章,我将会介绍 Git 的一些重要的配置方法和钩子机制以满足自定 ...
- CentOS下LAMP环境安装配置
本来几下yum都能装好的,yum却出问题了,报错:AttributeError: 'YumBaseCli' object has no attribute '_not_found_i',可能是某个文件 ...
- R语言命令行参数
批量画图任务中,需要在R中传入若干参数,之前对做法是在perl中每一个任务建立一个Rscript,这种方式超级不cool,在群里学习到R的@ARGV调用方式,差不多能够达到批量任务的要求: a ...
- Linux文件的复制、删除和移动命令
cp命令 功能:将给出的文件或目录拷贝到另一文件或目录中,就如同DOS下的copy命令一样,功能非常强大. 语法:cp [选项] 源文件或目录 目标文件或目录 说明:该命令把指定的源文件复制到目 ...
- java 集合类基础问题汇总
1.Java集合类框架的基本接口有哪些? 参考答案 集合类接口指定了一组叫做元素的对象.集合类接口的每一种具体的实现类都可以选择以它自己的方式对元素进行保存和排序.有的集合类允许重复的键,有些不允许 ...
- springboot 注册服务注册中心(zk)的两种方式
在使用springboot进行开发的过程中,我们经常需要处理这样的场景:在服务启动的时候,需要向服务注册中心(例如zk)注册服务状态,以便当服务状态改变的时候,可以故障摘除和负载均衡. 我遇到过两种注 ...