【ES6标准入门】JavaScript的Generator函数的异步应用,异步操作思路之一(中级)

在这里插入图片描述

😁 作者简介:一名大三的学生,致力学习前端开发技术
⭐️个人主页:夜宵饽饽的主页
❔ 系列专栏:前端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属性),否则就直接退出

👨 我的思考:

  1. 关于接收和交换执行权,其实依靠yield和next就可以,yield移出,next移入,最大的一个问题是“如何判断或者说保证上一步的操作是成功完成的,我再进行下一步”
  2. 这样我们这里考虑的回调函数来,执行成功后执行这个函数,再到函数里面继续执行
  3. 所以,我们的代码依据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,从而终止执行
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值