javascript-Generator的异步应用

loading 2022年11月17日 50次浏览

1. 引入

1.1 什么是异步?

异步,就是指一个任务不是连续地完成。可以理解为某个任务a先执行前一段,随后进行其他任务b,等a做好准备了。再回头执行a的后一段。

        //发出读取文件的请求
        readFileRequest();

        //期间程序执行其他任务
        otherTask();

        //接收到文件之后处理文件
        dealWithFile()

相应的,连续执行某个任务就叫同步,同步任务执行期间不可插入其他任务,导致有的情况下浪费资源效率较低。

1.2 回调函数和Promise

回调函数就是把任务的第二段单独写在一个函数里,等到重新执行该异步任务时就调用这个函数。

比如下面这个例子里的funtion,读取到文件后就抛出错误或者打印

        fs.readFile('/etc/passwd', 'utf-8', function (err, data) {
            if (err) throw err;
            console.log(data);
        });

但是如果太多回调函数嵌套就会形成所谓的回调地狱,会导致代码的可维护性和可读性非常差。

        fs.readFile(fileA, 'utf-8', function (err, data) {
            fs.readFile(fileB, 'utf-8', function (err, data) {
                // ...
            });
        });

Promise就是用来解决这个问题的,通过then/catch来形成清晰易懂的链式调用。

	//返回一个Promise版本的readFile函数
        var readFile = require('fs-readfile-promise');

        readFile(fileA)
            .then(function (data) {
                console.log(data.toString());
            })
            .then(function () {
                return readFile(fileB);
            })
            .then(function (data) {
                console.log(data.toString());
            })
            .catch(function (err) {
                console.log(err);
            });

然而Promise也有代码冗余的问题,太多then操作的存在也会让语义变得很不清楚,因此也就有了Generator函数的引入。

Generator之所以清晰是由于yield表达式的存在,它代表了异步两个阶段的分界线。使得Generator函数可以实现暂停执行和恢复执行。

        const fetch = require('node-fetch');

        const gen = function* () {
            const url = 'http://baidu.com';
            //fetch模块会返回一个Promise对象
            const result = yield fetch(url);
            console.log(result.bio)
        }

        const it = gen();

        //调用next方法 执行异步任务的第一阶段
        const obj = it.next();

        //obj.value是一个fetch返回的promise
        obj.value
            .then(data => {
                return data.json();
            })
            .then(data =>{
                it.next(data);
            })       

2.Thunk函数

2.1 传值调用和传名调用

很好理解,其实就是两种函数传参方式

先使得算出x+5=6,再将6传进去就是传值调用;直接传x+5进去,用到时再求职就是传名调用。

var x = 1;

function f(m) {
  return m * 2;
}

f(x + 5)

而thunk函数的普遍作用就是用来暂时存放参数,普遍用来实现传名调用

        function f(m) {
            return m * 2;
        }
        f(x + 5);

        // 等同于

        var thunk = function () {
            return x + 5;
        };
        function f(thunk) {
            return thunk() * 2;
        }

2.2 js中的Thunk函数

但是由于js是传值调用的,因此js中的thunk含义是不同的。thunk的作用是将多参数函数替换成只接受回调函数作为参数的单参数函数

        //正常的多参数readFile
        fs.readFile(fileName , callback);

        //thunk版本的单参数readFile
        const thunk = function(fileName){
            //返回一个回调函数作为单参数的函数
            return function(callback){
                return fs.readFile(fileName , callback);
            }
        }

        const readFileThunk = thunk(fileName);
        readFileThunk(callback);

任何函数只要有回调函数,就能写成Thunk函数的形式,可以写出一个统一的thunk转换器

        const fn = (val , callback) =>{
            callback(val);
        }

        //thunk转换器
        const thunk = function(fn){
            return function(...args){
                return function(callback){
                    return fn.call(this , ...args , callback);
                }
            }
        }

        const newFn = thunk(fn);
        console.log(newFn)
        newFn(1)(console.log)

2.3 Generator中的thunk函数

thunk函数在以前用处并不大,但在generator函数中thnuk可以用于自动流程管理。

如果是同步操作,只要这么写就行:

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

        var g = gen();
        var res = g.next();

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

通过thunk函数可以进行方便的异步操作管理(前提是每个异步操作都是thunk函数)

        function run(fn) {
            var gen = fn();

            function next(err, data) {
                var result = gen.next(data);
                if (result.done) return;
                result.value(next);
            }
            next();
        }

        var g = function* () {
            var f1 = yield readFileThunk('fileA');
            var f2 = yield readFileThunk('fileB');
            var f3 = yield readFileThunk('fileC');
        };

        run(g);

3. co模块

通过co模块就可以不用自己编写generator的执行器了,也就是不用写上面那堆难理解的东西,可以直接用于generator的自动执行。

比如这个generaotr函数,用于依次读取两个文件。可以用co函数执行gen。

使用co的前提条件是generator的yield后面是thunk函数或者promise对象(或者是一个数组/对象,其中的成员全是promise对象)

        var gen = function* () {
            var f1 = yield readFile('/etc/fstab');
            var f2 = yield readFile('/etc/shells');
            console.log(f1.toString());
            console.log(f2.toString());
        };

        var co = require('co');
        co(gen);

co函数返回一个Promise对象,因此也可以调用then方法。

        co(gen).then(() =>{
            console.log('generator successfully executed')
        })

co也支持处理并发的异步操作,将异步操作放在数组/对象中交给yield即可

        var gen = function* () {
            let res = yield[
                Promise.resolve(1),
                Promise.resolve(2)
            ];
            console.log(res);
        }
        co(gen);