Generator函数与异步编程

因为js是单线程语言,所以需要异步编程的存在,要不效率太低会卡死。

传统的异步方法

  • 回调函数
  • 事件监听
  • 发布/订阅
  • Promise

之前写过一篇关于Promise的文章,里边写过关于异步的一些概念。这篇文章将会说一下Generator函数的异步应用。

Generator函数

协程

多个线程互相合作完成任务,在传统的编程语言中(比如java),当A线程在执行,执行一段时间之后暂停,由B线程继续执行,B线程执行结束之后A线程再执行,这个时候,A线程就被称为协程,而这个协程A就是异步任务。

function* foo(){
    ... //其他代码
    var f = readFile();
    ... //其他代码
}

上边这个函数,foo函数就是一个协程,通过yield命令实现协程的暂停,等到读取文件的函数执行完毕之后,再继续执行foo其他的操作。

协程的Generator函数实现

Generator函数是协程在ES6的实现,最大的特点是交出函数的执行权(暂停函数执行)

整个Generator函数就是一个封装好了的异步任务,而yield是函数暂停执行的标志。

function* foo(){
    let x = 1;
    let y = yield x + 2;
    return y
}

var f = foo()
f.next();   // {value:3,done:false}
f.next();   // {value:undefined,done:true}

next方法的作用是分批端执行Generator函数。

异步函数的封装

let fetch = require('node-fetch')

function* asynsFun(){
    let url = '....';
    var f = yield fetch(url);
    console.log(f)
}

当执行完fetch之后把取回的数据赋值给f,然后再把f打印出来,这个看起来很像同步的写法,但是实现起来却是异步的。

是不是很简单,如果用回掉函数或者Promise的写法会很复杂的。

let a = asyncFun()
a.next()

a.value.then(function(data){
  return data.json();
}).then(function(data){
  g.next(data);
});

这样的写法表示起来很简洁,但是流程管理比较复杂。

Thunk函数

thunk函数是自动执行Generator函数的一种方法。

Thunk函数的核心理解就是传名调用。

function f(x){
    return x * 2
}

f(x + 6)

//等同于

var thunk = function(x) {
    return x + 5
}

function f() {
    return thunk() * 2 
}

理论上,x+6被thunk函数替代了,所有用到原来参数的地方,直接用thunk求值就行。

这就是Thunk函数的策略,是传名求值的一种实现策略,用来替换某个表达式。

js中的Thunk函数

在js中,函数的参数并不是传名的而是传值的,所以,thunk函数不是用来替换表达式的,而是用来替换多参数函数的。将其中一个参数替换成只接收一个回掉函数的单参数参数。

听起来很拗口,看代码。

// 正常的写法
fs.readFile(filename,callback);

// thunk函数的单参数版本
var thunk = function(filename) {
    return function(callback) {
        return fs.readFile(filename,callback);
    }
}

var readThunk = thunk(filename)
readThunk(callback)

理论上,只要函数的一个参数是回调函数,就可以改写成Thunk函数。

Thunkify模块

一个转换器,把函数转成Thunk函数

安装
npm install thunkify

使用方法:
var thunkify = require('thunkify');
var fs = require('fs');

var read = thunkify(rs.readFile);
read('package-json')(function(err,str)
    // ...
)

thunkify接受一个回调方法、

Generator函数的流程管理

之前说过,Generator函数的流程管理比较复杂,那么Thunk函数有什么用呢,正确答案是,他可以帮助Generator函数实现自动的流程管理。

function* gen(){
    // ...
}
var g = gen();
var res = g.next();

while(!res.done){
    console.log(res.value)
    res.next();
}

理论上,上面的代码可以实现自动执行,但是,不能适合异步。用Thunk可以解决这个问题。

var thunkify = require('thunkify');
var fs = require('fs');
var readFileThunk = thunkify(fs.readFile)

var gen = function* (){
    var r1 = readFileThunk('filename1')
    console.log(r1);
    var r2 = readFileThunk('filename2')
    console.log(r2);
    
}

Thunk函数的自动流程管理

Thunk函数的真正意义在于可以自动执行Generator函数,看下边的例子。

function* g(){
    // ...
}

function run(fn){    //Thunk函数接收一个Generator函数
    var gen = fn();
    
    function next(err,data){
        var result = gen.next(data);
        if(result.done) return;
        return result.value(next)
    }
    
    next();
}

run(g)

解析一下这个代码:
run方法其实就是一个Generator函数自动执行器。内部函数next就是Thunk的回调函数,next函数首先把Generator函数的指针指向Generator函数的下一步方法(gen.next()),如果没有,就把next函数传给Thunk函数(result.value属性),否则直接退出。

有了这个执行器,执行Generator函数就方便多了,不管内部多少操作,直接把Generator函数传给run函数即可,当然前提是每一个异步操作都是一个Thunk函数,也就是yield后面的必须是Thunk函数。

function* g(){
    var f1 = yield fs.readFileThunk('filename1')
    var f2 = yield fs.readFileThunk('filename2')
    ...

}

run(g)

Thunk 函数并不是 Generator 函数自动执行的唯一方案。因为自动执行的关键是,必须有一种机制,自动控制 Generator 函数的流程,接收和交还程序的执行权。回调函数可以做到这一点,Promise 对象也可以做到这一点。

这篇文章写得比较难懂,其实主要是为了下一篇文章做铺垫。


张小草1018
285 声望8 粉丝