😁 作者简介:一名大三的学生,致力学习前端开发技术
⭐️个人主页:夜宵饽饽的主页
❔ 系列专栏:前端js专栏
👐学习格言:成功不是终点,失败也并非末日,最重要的是继续前进的勇气
🔥前言:
这里是关于Generator函数的异步应用,也是异步操作重点之一,里面有关thunk和co的包,这两个包虽然在使用上面有些过时,但是其源码思想是非常值得学习和借鉴的,希望大家可以好好理解。 这是我自己的学习JavaScript的笔记,希望可以帮助到大家,欢迎大家的补充和纠正
🌻 理解异步操作的初级篇在上一节:Generator函数的基本语法,异步操作之一(初级)
第17章 Generator函数的异步应用
17.1 传统方法
ES6诞生以前,异步编程的方法大概有下面4种
- 回调函数
- 事件监听
- 发布/订阅
- Promise对象
Generator函数将JavaScript异步编程带入了全新的阶段
17.2 基本概念
17.2.1 异步
所谓异步,简单来说就是一个任务不是连续完成的,可以理解成该任务被人为分成两段,先执行第一段,然后转而执行其他任务,等做好准备后再回过头执行第二段
比如,有一个任务是读取文件进行处理,任务的第一段是向操作系统发出请求,要求读取文件,然后,程序执行其他任务,等到操作系统返回文件后再接着执行任务的第二段(处理文件)这种不连续的执行就叫做异步
相应的,连续执行叫做同步,由于是连续执行,不能插入其他任务,所以操作系统从硬盘读取文件的这段时间,程序只能等待
17.2.2 回调函数
fs.readFile('/etc/passwd','utf-8',function(err,data){
if(err) throw err
console.log(data)
})
上面的代码中,readFile函数的第三个参数就是回调函数,也就是任务的第二段,等到操作系统返回/etc/passd文件以后,回调函数才会执行
一个有趣的问题是,为什么Node约定回调函数的第一个参数必须是错误对象err(如果没有错误,该参数就是null)呢?
原因在于,执行阶段分为两段,第一段执行完以后,任务所在的上下文环境就已经结束了,在这以后抛出的错误,其原来的上下文环境已经无法捕捉,因此只能当作参数被传入第二段
17.2.3 Promise
上面使用回调函数,如果出现多个操作,就会出现多重嵌套,代码不是众向发展的,而是横向发展。
var readFile=require('fs-readfile-promise')
readFile(fileA)
.then(function(date){
consoel.log(data.toString())
})
.then(function(){
return readFile(fileB)
})
.then(function(date){
consoel.log(data.toString())
})
.catch(function(err){
consoel.log(err)
})
Promise的最大问题就是代码冗余,原来的任务被Promise包装之后,无论上面操作,一眼看去都是许多的then的堆积,原来的语义变得很不清楚。
17.3 Generator函数
17.3.1 协程
传统的编程语言中早有异步编程解决方案(其实是多任务的解决方案),其中一种叫做协程,意思是多个线程互相协作,完成异步任务
它的运行流程如下:
- 第一步,协程A开始执行
- 第二步,协程A执行到一半,进入暂停状态,执行权转移到协程B中
- 第三步,(一段时间后)协程B交还执行权
- 第四步,协程A恢复执行
上面流程的协程A就是异步任务,因为它分为两段(或多段)执行
17.3.2 协程的Generator函数实现
Generator函数是协程在ES6中的实现,最大特点就是可以交出函数的执行权(即暂停执行)
function* gen(x){
var y=yield x+2;
return y
}
var g=gen()
g.next() //{value:3,done:false}
g.next() //{value:undefined,done:true}
17.3.3 Generator函数的数据交换和错误处理
数据交换:next返回值的value属性是Generator函数向外输出数据,next方法还可以接受参数,向Generator函数体内输入数据
错误处理:捕获函数体外抛出的错误
17.3.4 异步任务的封装
var fetch=require('node-fetch')
function* gen(){
var url='https://api.github.com/users/github'
var result=yield fetch(url)
console.log(result.bio)
}
var g=gen()
var result=g.next()
result.value.then(function(data){
return data.json()
}).then(function(data){
g.next(data)
})
上面的代码中首先执行Generator函数获取遍历器对象,然后使用next方法(第二行)执行异步任务的第一阶段,由于Fetch模块返回的是一个Promise对象,因此要用then方法调用下一个next方法
可以看到,虽然Generator函数将异步操作表示的很简洁,但是流程管理却不方便(即何时执行第一阶段,何时执行第二阶段)
17.4 Thunk函数
Thunk函数是自动执行Generator函数的一种方法
17.4.1 参数的求值策略
“求值策略”是指函数的参数到底应该在何时求值,目前有两种意见来解决
var x=1;
function f(m){
return m*2
}
f(x+5)
-
第一种意见是“传值调用”,即在进入函数体之前就是计算x+5的值,再将这个值传入函数f。C语言就采用了这种策略
f(x+5) //传值调用等同于 f(6)
-
第二种意见是”传名调用“,即直接将表达式x+5传入函数体,只在用到它的时候求值,Haskell语言就采用这种策略
f(x+5) //传名调用时,等同于 (x+5)*2
那么这两种意见哪一种比较好呢?
答案是各有利弊,传值调用简单,但是对参数求值的时候,实际上还没有用到这个参数,有可能造成性能损失
17.4.2 Thunk函数的含义
编译器的传名调用的实现往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体,这个临时函数就是交Thunkhanshu
function f(m){
return m*2
}
f(x+5)
//等同于
var thunk=function(){
return x+5
}
function f(thunk){
return thunk()*2
}
这就是Thunk函数的定义,它是传名函数的一种实现策略,可以用来替换某个表达式
17.4.3 JavaScript语言的Thunk函数
JavaScript语言是传值调用,它的Thunk含义有所不同,在JavaScript语言中,Thunk函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数
//正常版本的readFile(多参数版本)
fs.readFile(fileName,callback);
//Thunk版本的readFile(单参数版本)
var Thunk=function(fileName){
return function(callback){
return fs.readFile(fileName,callback)
}
}
var readFileThunk=Thunk(fileName)
readFileThunk(callback)
上面的代码中,fs模块的readFile方法是一个多参数函数,两个参数分别为文件名和回调函数,经过转换器处理,它变成了一个单参数函数,只接受回调函数作为参数,这个单参数版本就叫做Thunk函数
任何函数,只要参数有回调函数,就能写成Thunk函数的形式
👨 **我的思考:**Thunk函数其实就是保证最后的一层嵌套是回调函数作为参数的函数就行,其余的外面可以根据原本的函数的参数个数来决定套几层函数
下面是一个更加实际的例子
const Thunk=function(fn){
return function(...args){
return function(callback){
return fn.call(this,...args,callback)
}
}
}
var readFileThunk=Thunk(fs.readFile)
readFileThunk(fileA)(callback)
17.4.4 Thunkify模块
首先是安装
npm install thunkify
使用方式如下
var thunkify=require('thunkify')
var fs=require('fs')
var read=thunkify(fs.readFile)
read('package.json')(functoin(err,str){
})
其源码与上一节的简单转换器非常像(这个源码的实现方式还是非常值得参考的)
function thunkify(fn){
return function(){
var args=new Array(arguments.length)
var ctx=this;
for (let i = 0; i < args.length; i++) {
args[i] = arguments[i];
}
return function(done){
var called
args.push(function(){
if(called) return
called=true
done.apply(null,arguments)
});
try {
fn.apply(ctx,args)
} catch (error) {
done(error)
}
}
}
}
17.4.5 Generator函数的流程管理
在Generator函数的出现的背景下,Thunk函数可以用于Generator函数的自动管理
var g=gen()
var res=g.next()
while(!res.done){
console.log(res.value)
res=g.next()
}
上面的代码中,Generator函数gen会自动执行完所有步骤
但是,这不适合异步操作,如果必须保证前一步执行完才能执行后一步,上面的自动执行就不可行,这时,Thunk函数就能派上用处,以读取文件为例,下面的Generator函数封装了两个异步操作
var fs=require('fs')
var thunkify=require('thunkify')
var readFileThunk=thunkify(fs.readFile)
var gen=function*(){
var r1=yield readFileThunk('/etc/fstab')
console.log(r1.toString())
var r2=yield readFileThunk('/etc/shells')
console.log(rs.toString())
}
上面的代码中,yield命令用于将程序的执行权移出Generator函数,那么就需要一种方法将执行权再交还给Generator函数
这种方法就是使用Thunk函数,因为它可以在回调函数里将执行权交还给Generator函数,为了便于理解,外面先来看一下如何手动执行上面的Generator函数
var g=gen()
var r1=g.next()
r1.value(function(err,data){
if(err) throw err
var r2=g.next(data)
r1.value(function(err,data){
if(err) throw err
g.next(data)
})
})
上面的代码中,变量g是Generator函数的内部指针,标明目前执行到哪一步,next方法负责将指针移动到下一步,并返回该步的信息(value属性和 done属性)
仔细查看上面的代码,可以发现Generator函数的执行过程其实是将同一个回调函数反复传入next方法的value属性,这使得我们可以用递归来自动完成这个过程
17.4.6 Thunk函数的自动流程管理
Thunk函数真正的威力在于可以自动执行Generaror函数,下面就是基于Thunk函数的Generator执行器的例子
function run(fn){
var gen=fn()
function next(err,data){
var result=gen.next(data)
if(result.done) return
result.value(next)
}
next()
}
function* gen(){
}
run(g)
以上的代码中run函数就是一个Generator函数的自动执行器。内部next函数就是Thunk的回调函数。next函数先将指针移到Generator函数的下一步(gen.next方法),然后判断Generator函数是否结束(result.done属性),如果没有结束就将next函数再传入Thunk函数(result.value属性),否则就直接退出
👨 我的思考:
- 关于接收和交换执行权,其实依靠yield和next就可以,yield移出,next移入,最大的一个问题是“如何判断或者说保证上一步的操作是成功完成的,我再进行下一步”
- 这样我们这里考虑的回调函数来,执行成功后执行这个函数,再到函数里面继续执行
- 所以,我们的代码依据i上面的17.4.5的最后代码例子,可以得出17.4.6的例子代码,只是做了一个简化,达到自动执行管成功的回调函数
总结:Thunk函数并不是Generator函数的自动执行的唯一方案,因为自动执行的关键是,必须有一种机制自动控制Generator函数的流程,接收和交还程序的执行权,回调函数可以做到这一点,Promise对象也可以做到这一点
17.5 co模块
17.5.1 基本用法
co模块是著名程序员TJ holowaychuk于2013年6月发布的一个小根据,用于Generator函数的自动执行
var gen=function* (){
var f1=yield readFile('/etx/fstab')
var f1=yield readFile('/etx/shells')
console.log(f1.toString())
console.log(f2.toString())
}
var co=require('co')
co(gen).then(function(){
console.log('Generator 函数执行完成')
})
17.5.2 co模块的原理
为什么co可以自动执行Generatoo函数呢
前面说过,Generator就是一个异步操作的容器,它的自动执行需要一种机制,当异步操作有了结果,这种机制就要自动交回执行权
有两种方法可以做到这一点
- 回调函数。将异步操作包装成Thunk函数,在回调函数里面交回执行权
- Promise对象。将异步操作包装成Promise对象,用then方法交回执行权
co模块其实就是将两种自动执行器(Thunk函数和Promise对象)包装成一个模块,使用co的前提条件是,Generator函数的yield命令后面只能是Thunk函数或者Promise对象,如果数组或对象的成员全部都是Promise对象,也可以使用co(co v4.0版本以后,yield命令后面只能是Promise对象,不再支持Thunk函数
上一节已经介绍基于Thunk函数的自动执行器,下面来看基于Promise对象的自动执行器
17.5.3 基于Promise对象的自动执行
var fs=require('fs')
var readFile=function(fileName){
return new Promise(function(resolve,reject){
fs.readFile(fileName,function(errpr,data){
if(errpr) return reject(errpr)
resolve(data)
})
})
}
var gen=function* (){
var f1=yield readFile('/etc/fstab')
var f2=yield readFile('/etx/shells')
console.log(f1.toString())
console.log(f2.toString())
}
var g=gen()
g.next().value.then(function(data){
g.next(data).value.then(function(data){
g.next(data)
})
})
手动执行其实就是用then方法层层添加回调函数。理解了这一点,就可以写出一个自动执行器
function run(gen){
var g=gen()
function next(data){
var result=g.next(data)
if(result.done) return result.value
result.value.then(function(data){
next(data)
})
}
next()
}
run(gen)
17.5.4 co模块的源码
function co(gen){
var ctx=this
return new Promise(function(resolve,reject){
if(typeof gen === 'function') gen=gen.call(ctx)
if(!gen || typeof gen.next ! == 'function') return resolve(gen)
onFulfilled()
function onFulfilled(res){
var ret;
try {
ret=gen.next(res)
} catch (error) {
return reject(e)
}
}
next(ret)
function next(ret){
if(ret.done) return resolve(ret.value)
var value=toPromise.call(ctx,ret.value)
if(value && isPromise(value)) return value.then(onFulfilled,onRejected)
return onRejected(
new TypeError(
'You may only yield a function,promise,generator,array or object,'
+'but the following object was passed:"'
+String(ret.value)
+'"'
))
}
}
上面的代码中 next函数的内部代码一共只有4行命令
- 第1行:检查当前是否为Generator函数的最后一步,如果是就返回
- 第2行:确保每一步的返回值是Promise对象
- 第3行:使用then方法为返回值加上回调函数,如何通过onFulfilled函数再次调用next函数
- 第4行:在参数不符合要求的情况下(参数非Thunk函数和Promise对象)将Promise对象的状态改为rejected,从而终止执行