ES6 Generator与异步的同步书写

开始前

我们从来没有停止过对javascript语言异步调用方式的改造,我们一直都想用像java那样同步的方式去写异步,尽管Promise可以让我们将异步回调添加到then方法中,但是这种调用方式仍然不那么优雅,es6 中新增加了generator,我们可以通过他的特性来实现异步任务更加优雅的书写方式。

协程介绍

协程其实和线程,进程是没有关系的,它不是操作系统为我们提供的api接口,而是通过编程语言或者汇编语言对程序上下文、程序栈来操作实现的。一个线程里面可以包含多个协程,线程的调度是由操作体统来决定的,协程的调度是由用户来决定的。操作系统对其一无所知,因为可以由用户来调度,所以用来执行协作式的任务特别方便。(注意这里是方便,因为能通过协程解决的问题,通过线程和进程也可以解决,但是复杂)

Generator介绍

Generator 是协程在es6中的实现。它在es6中是一个函数,这个函数可以分阶段执行,也就是说我们可以在这个函数中的某个位置选择交出当前线程的执行权限,也可以在当前函数外面的某个位置选择将权限再交回这个函数,让它继续执行,这种调度完全由用户决定。在es6中协程函数是这样的

function* gen(p) {
    var a = yield p + 1;  //1
    var b = yield p + 2;  //2
    return b;  //3
}

var g = gen(1);
g.next();  //{value: 2, done: false}
g.next();  //{value: 3, done: false}
g.next();  //{value: undefined, done: true}

通过 var g = gen(1); 仅仅是创建了一个迭代器,函数 gen 里面的内容并没有执行函数体的执行时由第一个 g.next(); 开始的 并且将 yield 所在那那条语句执行完后就会返回结果。而后面的语句并没有执行。返回值是一个对象,它的第一个属性是 yield 后面表达式的值 (p+1或者p+2的值);第二个属性表示Generator函数是否执行完成。这里我们通过 yield 执行权限交出去,通过 next 将权限返回。

function* gen(p) {
    var a = yield p + 1;  //1
    var b = yield a + 1;  //2 注意这里是用到了 a
    return b;
}
var g = gen(1);
g.next();  //{value: 2, done: false}
g.next();  //{value: NaN, done: false} 这里的值是 NaN
g.next();  //{value: undefined, done: true}

g.next();  //{value: 2, done: false}
g.next(2);  //{value: 3, done: false}
g.next(6);  //{value: 6, done: true}

注意这里 //1 处 //2 处 var a = yield p + 1;这条赋值语句中 a 的值并不是 p + 1的值。这条语句只是一种写法,这里 a 的值是我们在第二个 next 中传入的 2 这个很重要 b 的值也是我们在第三个 next 中传入的 6

Generator 的重要特性

由上面的内容我们总结 3 个关于 Generator 的重要特性

1 通过 yield 交出执行权限,通过 next 返回执行权限
2 调用 next 会得到一个返回值,这个值里面包含了 yield 后面的表达式的执行结果
3 我们可以通过给 next 传递参数,并且可以在 Generator 函数中通过上面所写的特殊方式来引用

利用 Generator 的特性来实现异步代码的同步书写

我们来模拟一个异步函数

function post(url, callback) {
    setTimeout(function() {
        var data = { //模拟异步处理结果
            url:url,
            value:10
        };
        callback(data);
    }, 1000);
}

post('http://_ivenj',function(data){
    console.log(data.url);  // http://_ivenj
    console.log(data.value);  //10
});

对应上面的这个异步函数我想通过 Generator 来这样用

function* gen(url) {
    var data = yield post(url);  //1
    console.log(data.url);
    console.log(data.value);
}
var g = gen('http://_ivenj');
var resultG = g.next();
g.next(resultG.value);

是的,这样写漂亮多了,很像 java 的同步写法。不同之处就是多了个 yield* ,这个无伤大雅。当然以上这样用肯定是不行的。因为 post 毕竟是个异步方法。没有返回值.如果不能实现这样的写法我这半天就是在扯淡,所以通过包装是可以实现的。

通过以下两点可以实现以上的书写方式

(1)我有一篇文章 react 实践之 redux applyMiddleware方法详解 中介绍了柯里化(Currying)这篇文章虽然是写react的但是柯里化是独立的,这里就要利用柯里化的思想

(2)我们要在回调中调用 next 来继续执行,(这里有人会想不是不用回调了么,怎么还用,请继续看。。。)

我们要对 post 的调用形式进行包装

function kPost(url) {
    return function(callback) {
        post(url, callback);
    }
}

通过这个包装,我们就能保证调用 kPost 就会同步的得到一个返回值

function* gen(url) {
    var data = yield kPost(url);  //1
    console.log(data.url);
    console.log(data.value);
}
//这里执行方式会不同
var g = gen('http://_ivenj');
//启动任务
var resultG1 = g.next();
var value_resultG1 = resultG1.value; //resultG1.value 一定是一个函数,因为我们包装了
value_resultG1(function(data){
    g.next(data);  //通过在异步的回调中调用 next 并传递值来确保依赖异步结果的代码能正确执行
});

下面就是整体代码,是上面的片段组合,请你粘贴到浏览器控制台,或者用node运行,就会看到想要的结果

function post(url, callback) {
    setTimeout(function() {
        var data = { //模拟异步处理结果
            url:url,
            value:10
        };
        callback(data);
    }, 1000);
}
function kPost(url) {
    return function(callback) {
        post(url, callback);
    }
}
function* gen(url) {
    var data = yield kPost(url);  //1
    console.log(data.url);
    console.log(data.value);
}
//这里执行方式会不同
var g = gen('http://_ivenj');
//启动任务
var resultG1 = g.next();
var value_resultG1 = resultG1.value; //resultG1.value 一定是一个函数,因为我们包装了
value_resultG1(function(data){
    g.next(data);
});

有人会说,怎么不就是将异步回调转移出来了么,还要写回调。这说明你还没有真正体会个中之奥妙。我们会发现 我们写的异步

value_resultG1(function(data){
    g.next(data);
});

仅仅是调用了 next 进行了结果的传递,这里面有共同之处,不管是哪一种异步,我们都只传递值。大家的处理都是一样的。真正的业务逻辑确实是用同步的方式写的。那么,我们可以将共同的地方提取出来,写一个通用的函数去执行这个传值操作,这样,我们完全就告别了异步,再也看不到了,好开心。co.js就是一个这种generator的执行库。使用它是我们只需要将我们的 gen 传递给它像这样 co(gen) 是的就这样。下面我们自己写一个 co

Generator执行器

function co(taskDef) {
    //获取迭代器  类似 java 中的外柄迭代子
    var task = taskDef();
    //开始任务
    var result = task.next();
    //调用next的递归函数
    function step() {
        if (!result.done) {  //如果generator没有执行完
            if (typeof result.value === "function") {
                result.value(function(err, data) {
                    if (err) {
                        result = task.throw(err);
                        return;
                    }
                    result = task.next(data);  //向后传递当前异步处理结果
                    step();  //递归执行
                });
            } else {
                result = task.next(result.value);  //如果执行完了就传递值
                step();  //递归执行
            }

        }
    }
    // 启动递归函数
    step();
}

通过 co 执行的完整代码

function post(url, callback) {
    setTimeout(function() {
        var data = { //模拟异步处理结果
            url:url,
            value:10
        };
        callback(data);
    }, 1000);
}
function kPost(url) {
    return function(callback) {
        post(url, callback);
    }
}
function gen(url) {
    return function* () {
        var data = yield kPost(url);  //1
        console.log(data.url);
        console.log(data.value);
    }
}
function co(taskDef) {
    var task = taskDef();
    //开始任务
    var result = task.next();
    // 调用next的递归函数
    function step() {
        if (!result.done) {  //如果generator没有执行完
            if (typeof result.value === "function") {
                result.value(function(err, data) {
                    if (err) {
                        result = task.throw(err);
                        return;
                    }
                    result = task.next(data);  //向后传递当前异步处理结果
                    step();  //递归执行
                });
            } else {
                result = task.next(result.value);  //如果执行完了就传递值
                step();  //递归执行
            }

        }
    }
    // 启动递归函数
    step();
}
    
co(gen('http://_ivenj')); //调用方式就是这么简单

以上代码执行 1s 后会抛出一个异常,并且正确打印{url: “http://_ivenj”, value: 10},聪明的你一定知道为什么会抛出异常!!!

到这里已经说明白了,并且也说完了,你会想是不是把异步包装成Promise也可以呢,答案是肯定的,柯里化的思想只是一种实现方式,Promise 也是一种,你可以自己去琢磨,co.js 就是将两种方式都实现了的一个执行器。es7 中从语言层面对 Generator 进行了包装,在es7 中我们可以使用 asyncawait更优雅的实现类似java的顺序书写方式,asyncawaitGenerator的语法糖,在es7中内置了执行器。别人都说是终极方案。

    原文作者:_ivenj
    原文地址: https://segmentfault.com/a/1190000010106930
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞