co与generator

呼延河
2023-12-01

co与generator

众所周知,js的的异步操作,基本的是引入回调机制。但是,回调机制容易产生的一个问题是回调金字塔
回调金字塔是js以前编程语法的上的一个诟病,也是异常处理中很麻烦的一件事情。
因此,人们希望能引入一些模式,来用同步编程代替异步编程,ES6之前的promise模式就是如此,而ES6后
,co与与generator也实现了更简洁,更简单的异步代替同步的逻辑。

generator基础知识

ES6语法中,提出了generator(生成器)的概念,那么,我们先了解下什么是生成器?
我们先了解下:generator 的语法:

函数声明/函数表达式 的关键字 function 后多了个 * 。

函数体中多了 yield 运算符。yield 有点像是 return 关键字,因为它们都返回一个值,但是这里的函数会在 yield 之后。

例如:

function *oneGenerator(){
   yield console.log("one Generator");
}

好像这么看来gen和普通的函数没有什么区别啊,别急,让我们看看接下来的这个例子:

function * GenA(){
     console.log('from GenA, first.');
     yield 1
     console.log('from GenA, second.');
     var value3 = yield console.log("test");
     console.log('from GenA, third.',value3);
     return 3;
 }
 var takeNumber=GenA();
console.log(takeNumber.next());//{ value: 1, done: false }
console.log(takeNumber.next()); 
//{ value: undefined, done: false }
console.log(takeNumber.next(23333)); //{ value: 3, done: true }

大家仔细看上述代码以及gen的执行过程,大家会发现:
1. gen和普通函数的区别在于,不是直接执行的
生成器返回的是一个gen对象,我们要通过这个对象来执行里面的逻辑
2. gen的中常常用yield的关键字隔开,gen中如有有yield关键字,那么,gen中的代码不会一次被执行完成
而是会根据yield关键字,一段一段的执行完成:如果gen中有n个yield关键字,那么gen就要调用n+1次next逻辑
来执行。
3.gen的每次执行返回的都会返回一个next对象,value的的数值就是yield代码执行后返回的数值,done代表之后
是否还有要执行的逻辑,false代表还有,反之则无。调用next的方法的过程中,如果传递了参数到next方法的参数中,
那么,这个参数将作为gen的yield关键字后返回的对象:比如示范中的23333这个参数,就被赋值给了value3
,然后再value3的数值确实是23333.

大家看完了上述关于gen的示范后,大概明白了gen的和普通函数的区别所在了吧。掌握gen的主要几点在于:
1. gen类似于一个糖果机,需要不断的从外部提供next来进行gen的内部代码的支持,当然,gen如果没有yield,
自然就和普通的function没有区别了。
2 . gen是调用next方法执行的内部的代码逻辑的,next方法返回一个对象,这个对象有value和done两个属性,
并且调用next方法的参数,可以作为yield内部返回的数值.

关于gen的使用,在项目过程中有以下几点需要注意的:
1. yield 关键字只有在gen中才能使用,在普通的function中,编译时会报错的。
2. gen的调用方式,不能作为普通函数调用,而要通过yield函数来调用,不然的话,gen的执行过程直接跳过。

异步逻辑如何转化为同步逻辑

js的中的常用的异步逻辑都是如下所示的:

 require('fs').readFile(filename, function (err, data) {
        if (callback) {
            callback(err, data);
        }
    });

那么,我们要怎么做,可以让js中的同步逻辑变成异步逻辑呢?我们是否可以考虑使用generator将异步逻辑变成同步逻辑呢?

这里我们一步一步的分析,首先,gen的执行逻辑是和糖果机一样,用yield的执行隔开,如果我们可以
做到以下几点,就有可能将异步逻辑封装为同步逻辑:
1. 将原来的异步函数的返回值,用gen的next对象体现出来
2. 需要一个函数,来代替异步回调的callback函数的地位,这个函数的参数可以就是callback函数的参数,
还有一点就是,这个函数可以起到驱动generator不断前行的逻辑。

看来,要实现这一步,我们需要编写这么一个函数,通常称为resume函数.
我们给出示例代码:

function readFile(filename, callback) {
    require('fs').readFile(filename, function (err, data) {
        if (callback) {
            callback(err, data);
        }
    });
}  

这段代码将readfile过程作了封装,将callback函数作为参数传递到回调函数里面去.

function run(generatorFunc) {
    var generatorltr = generatorFunc(resume);
    function resume(err, callbackValue) {
        if (err) {
            console.log("error");
        }
        generatorltr.next(callbackValue);
    }
    generatorltr.next();
}  

这里我们定义了一个run函数,run函数的主要内容是:接受一个generator作为参数(generatorFunc),
然后通过generatorFunc得到一个生成器对象generatorltr,然后,我们再定义了一个resmuse函数,这里就是
关键了:
resmuse函数的参数是err和callbackValue(和callback函数的参数是一样的),同时,resmue函数
内部,调用到了generatorltr.next机制,利用next的机制,将callbackvalue的数值,作为yield语句后
的返回值。同时驱动gen向下运行.

我看下示范例子:

run(function*(resume) {
    var data =yield  readFile("./public.pem", resume);
    console.log(data.toString("base64"));
})  

现在我们调用了一个run函数,并且传递了generator作为run函数的参数,同时这个generator还有一个参数就是
resums函数。因此,run函数的执行逻辑成了:

  1. 将resmus函数传入generatorFun中。
  2. 得到generatorFunc生成的generator对象的数值.
  3. 开始 generatorltr.next()的执行逻辑代码逻辑,执行到 readFile(“./public.pem”, resume)代码 .
  4. 执行require(‘fs’).readFile的过程
  5. 在callback函数中,执行resmuse函数的逻辑,将读取的文件内容作为参数,赋值给var data,同时,继续
    后面的逻辑
  6. 在终端上打印文件的内容。

co库的原理和thunk模式

上面一部分,告诉了将异步函数变成同步的基本逻辑,其实,其中的核心就是:

  1. 封装异步逻辑,将异步代码,用函数封装,达到可以调用的目的。
  2. 需要一个resmuse函数:这个函数能够接受callback函数的参数,同时可以控制generator的进行。

上面的代码只是简单的逻辑而已,但是给我们提供了学习co库原理的基础,我们可以依照这个思想来学习co库。
co库是TJ大神的神作之一,co库主要目的就是接受一个generator,并且执行generator内部的逻辑。
可以说co就是上述的run函数。
那么,封装异步逻辑的呢?这里要提到js的thunk模式。js的thunk模式这里不做详细介绍,具体可以参照这个连接
http://www.ruanyifeng.com/blog/2015/05/thunk.html .
这里将readfile封装为一个thunk模式:

//thunk mode
function readFile(filename) {
    return function (callback) {
        require('fs').readFile(filename, 'utf8', callback);
    };
}   

然后呢?我们写一个简单co的逻辑库:

function co(generator) {
    var gen = generator();
    function next(err, result) {
        if (err) {
            console.log("error");
        }
        var step = gen.next(result);
        if (!step.done) {
            step.value(next);
        }else{
            console.log("end");
        }
    }
    next();
}  

接下来我们开始执行这个co函数:

co(function * () {
    var file1 = yield readFile('public.pem');
    console.log(file1.toString("base64"));
});  

好,接下来,我们开始分析下这个简单的co执行的逻辑,其实也很简单,co函数传入一个generator,然后
co函数变成了这样:

function co() {
    var file1 = yield readFile('public.pem');
    console.log(file1.toString("base64"));
     function next(err, result) {
        if (err) {
            console.log("error");
        }
        var step = gen.next(result);
        if (!step.done) {
            step.value(next);
        }else{
            console.log("end");
        }
    }
    next();
}  

接下来执行co内部定义的next函数,由于next函数需要两个参数,而这里一开始调用next();并没有给出,因此,err=null,result=null;然后,就会执行 var step = gen.next(result);这一句,这句话将执行
readFile(‘public.pem’) 函数,注意,readFile(‘public.pem’)这个函数是thunk模式的函数,也就是说,只是返回一个fuction, 所以,现在的step.value=function (callback) {require(‘fs’).readFile(filename, ‘utf8’, callback);};(step的value为这读文件的这个函数),;由于step.done=false,接下来执行的逻辑是step.value(next)也就是执行读取文件的这个逻辑过程,并且参数是next函数自己(也就是把next函数作为参数,传入到callback函数过程中去),接下来就是callback内部的逻辑,得到文件的内容,并且通过generator的next()传递参数,将读取到的文件内容赋值给file1;然后重新生成step,这时候的step.done=true,因此,所以,现在执行console.log(“end”)的过程。

OK,以上就是整个co的逻辑部分,其实和文章第二部分的逻辑还是有很多相似之处,最关键的就是用next函数
来代替callback函数,并且在callback函数内部,生成generator对象,并且判定generator是否执行完成.还有
就是使用了js的thunk模式来进行函数的封装。co的大致原理也是同上所示。

以上就是关于现在node中如何将异步编程方式转化为同步编程方式的,利用thunk模式封装原来的异步函数,再
利用generator的执行特点,我们就可以解决异步编程带来的回调过多,异常处理困难等问题了。这里可以看到,co的带来的同步逻辑,其实在底层实现上还是异步的,只是co改变了一种编程方式,使得回调过深还有异常捕捉的等问题得到改变.

 类似资料: