ES6中的异步编程:Generators函数+Promise:最壮大的异步处理方式

接见原文地点

generators主要作用就是供应了一种,单线程的,很像同步要领的编程作风,轻易你把异步完成的那些细节藏在别处。这让我们能够用一种很天然的体式格局誊写我们代码中的流程和状况逻辑,不再须要去遵照那些新鲜的异步编程作风。

换句话说,经由过程将我们generator逻辑中的一些值的运算操纵和和异步处置惩罚(运用generators的迭代器iterator)这些值完成细节离开来写,我们能够非常好的把机能处置惩罚和营业关注点给离开。

效果呢?一切那些壮大的异步代码,将具有跟同步编程一样的可读性和可维护性。

那末我们将怎样完成这些豪举?

一个简朴的异步功用

先从这个非常的简朴的sample代码最先,如今generators并不须要去分外的处置惩罚一些这段代码还没有了的异步功用。

举例下,到场如今的异步代码已是如许的了:

function makeAjaxCall(url, cb) {
    //do some ajax fun
    //call cb(result) when complete
}

makeAjaxCall('http://some.url.1', function(result1) {
    var data = JSON.parse(result1);
    
    makeAjaxCall('http://some.url.2/?id='+data.id, function(result2) {
        var resp = JSON.parse(result2);
        console.log('The value you asked for: '+ resp.value);
    });
});

用一个generator(不增添任何decoration)去从新完成一遍,代码看这里:

function request(url) {
    // this is where we're hiding the asynchronicity,
    // away from the main code of our generator
    // it.next() 是generators的迭代器
    makeAjaxCall(url, function(response) {
        it.next(response);
    });
}

function *main() {
    var result1 = yield request('http://some.url.1');
    var data = JSON.parse(result1);
    
    var result2 = yield request('http://some.url.2/?id='+data.id);
    var resp = JSON.parse(result2);
    console.log('The value you asked for: '+ resp.value);
}

var it = main();
it.next(); //启动一切要求

让我们捋一下这是怎样事变的

request(..)函数对makeAjaxCall(..)做了基础封装,让数据要求的回调函数中挪用generator的iterator的next(...)要领。

先来看挪用request(".."),你会发明这里基础没有return任何值(换句话来讲,这是undefined)。这没紧要,然则它跟我们在这篇文章后面会讨论到的要领做对照是很主要的:我须要有用的在这里运用yield undefined.

这时刻我们来挪用yield(这时刻照样一个undefined值),实在除了停息一下我们的generators以外没有做别的了。它将守候晓得下次再挪用it.next(..) 才恢复,我们行列已把它在安排在(作为一个回调)Ajax要求终了的时刻。

然则当yield表达式实行返回效果后我们做了什么?我们把返回值赋值给了result1.。那为何yield会有从第一个Ajax要求返回的值?

这是因为当it.next(..)在Ajax的callback中挪用的是偶,它通报Ajax的返回效果。这说明这个返回值发送到我们的generators时,已中心那句result1 = yield .. 给停息下来了。

这个真的很酷很壮大。实质上看,result1 = yield request(..)这句是要求数据,然则它完整把异步逻辑在我们眼前藏起来了,最少不须要我们在这里斟酌这部份异步逻辑,它经由过程yield的停息才能隐蔽了异步逻辑,同时把generator恢复逻辑的功用星散到下一个yield函数中。这就让我们的主要逻辑看上去很像一个同步要求要领。

第二句表达式result2 = yield result(..)也基础一样的作用,它将pauses与resumes传进去,输出一个我们要求的值,也基础不须要对异步操纵忧郁。

固然,因为yield的存在,这里会有一个玄妙的提醒,在这个点上会发作一些奇异的事变(也称异步)。然则跟噩梦般的嵌套回调地狱(或许是promise链的API开支)比拟,yield语句只是须要一个很小的语法开支。

上面的代码老是启动一个异步Ajax要求,然则如果没有做会发作什么?如果我们厥后更改了我们递次中先前(预先要求)的Ajax返回的数据,该怎么办?或许我们的递次的URL路由体系经由过程其他一些庞杂的逻辑,能够马上满足Ajax要求,这时刻就能够不须要fetch数据从服务器了。

如许,我们能够把request(..)代码轻微修正一下

var cache = {};

function request(url) {
    if(cache[url]) {
        // defer cache内里的数据对如今来讲是已足够了
        // 实行下面
        setTimeout(function() {
            it.next(cache[url])
        }, 0);
    }
    else {
        makeAjaxCall(url, function(resp) {
            cache[url] = resp;
            it.next(resp);
        })
    }
}

注重:一句很巧妙、奇异的setTimeout(..0)放在了当缓存中已要求过数据的处置惩罚逻辑中。如果我们马上挪用it.next(...),如许会发作一个error,这是因为generator还没有完成paused操纵。我们的函数首先要完整挪用request(..),这时刻才会启动yield的停息。因而,我们不能马上在request(..)中马上挪用it.next(...),这是因为这时刻generator仍然在运转(yield并没有实行)。然则我们能够稍后一点挪用it.next(...),守候如今的线程实行终了,这就是setTimeout(..0)这句有魔性的代码放在这里的意义。我们稍后还会有一个更好的处理办法。

如今,我们的generator代码并不须要发作任何变化:

var restult1 = yield request('http://some.url.1');
var data = JSON.parse(result1);
...

看到没?我们的generator逻辑(也就是我们的流程逻辑)纵然增添了缓存处置惩罚功用后,仍不须要发作任何转变。

*main()中的代码照样只须要要求数据后停息,以后比及数据返回后递次实行下去。在我们当前的情况下,‘停息’能够相对要长一些(做一个服务器的要求,约莫要300~800ms),或许他能够险些马上返回(走setTimeout的逻辑),然则我们的流程逻辑完整不须要体贴这些。

这就是将异步编程笼统成更小细节的真正气力。

更好的异步编程

上面的要领能够适用于那些比较简朴的异步generator事变流程。然则它将很快收到限定,因而我们须要一些更壮大的异步机制与我们的generator来协作,如许才能够发挥出更壮大的功用。那是什么机制:Promise。

新近的Ajax实例老是会收到嵌套回调的搅扰,题目以下:

  • 1.没有明白的要领来处置惩罚要求error。我们都晓得,Ajax要求偶然是会失利的,这时刻我们须要运用generator中的it.throw(...),同时还须要运用try...catch来处置惩罚要求错误时的逻辑。然则这更多是一些在背景(我们那些在iterator中的代码)手动的事变。我须要一些能够服用的要领,放在我们本身代码的generator中。

  • 2.如果makeAjaxCall(..)这段代码不在我们的掌握下了,或许他须要屡次挪用回调,又或许它同时返回success与error,等等。这时刻我们的generator的会变得杂乱无章(返回error完成,涌现非常值,等等)。掌握以及防备发作这类题目是须要消费大批的手工时候的,而且一点也不能即插即用。

  • 3.一般我须要实行并行实行使命(比方同时做2个Ajax要求)。因为generator yield机制都是逐渐停息,没法在同时运转另一个或多个使命,他的使命必需一个一个的按递次实行。因而,这不是太轻易在一个generator中去操纵多使命,我们只能默默的在背地手撸大批的代码。

就像你看到的,一切的题目都被处理了。然则没人情愿每次都去重复的去完成一遍这些要领。我们须要一种更壮大的形式,特地设想出一个可信赖的,可重用的基于generator异步编程的处理要领。

什么形式?把promise与yield连系,使得能够在实行完成后恢复generator的流程。

让我们轻微用promise修正下request(..),让yield返回一个promise。

function request(url) {
    //如今返回一个promise了
    return new Promise( function(resolve, reject) {
        makeAjaxCall(url, resolve);
    });
}

request(..)如今由一个promise组成,当Ajax要求完成后会返回这个promise,然则然后呢?

我们须要掌握generator的iterator,它将接受到yield返回的谁人promise,同时经由过程next(…)恢复generator运转,并把他们通报下去,我增添了一个runGenerator(...)要领来做这件事。

//比较简朴,没有error事宜处置惩罚
funtion runGenerator(g) {
    var it = g(), retl
    
    //异步iterator遍历generator
    (function iterate(val) {
        //返回一个promise
        ret = it.next(val);
        
        if(!ret.done) {
            if('then' in ret.value) {
                //守候吸收promise
                ret.value.then(iterate);
            }
            //猎取马上就有的数据,不是promise了
            else {
                //防止同步操纵
                setTimeout(function() {
                    iterate(ret.value);
                }, 0);
            }
        }
    })();
}

症结点 :

  • 自动初始化generator(直接建立它的iterator),而且异步递将他一向运转到终了(当done:true就不在实行)

  • 如果Promise被返回出来,这时刻我们就守候到实行then(...)要领的时刻再处置惩罚。

  • 如果是能够马上返回的数据,我们直接把数据返回给generator让他直接去实行下一步。

runGenerator( function *main(){
    var result1 = yield request( "http://some.url.1" );
    var data = JSON.parse( result1 );

    var result2 = yield request( "http://some.url.2?id=" + data.id );
    var resp = JSON.parse( result2 );
    console.log( "The value you asked for: " + resp.value );
} );

等一下,如今的generator跟本来的完整一样嘛。只管我们改用了promise,然则yield要领不须要有什么变化,因为我们把那些逻辑都从我们的流程治理中星散出去了。

只管如今的yield是返回一个promise了,并把这个promise通报给下一个it.next(..),然则result1 = yield request(..)这句获得的值跟之前照样一样的。

我们如今最先用promise来治理generator中的异步代码,如许我们就处理掉一切运用回调函数要领中会涌现的反转/信托题目。因为我们用了generator+promise的要领,我们不须要增添任何逻辑就处理掉了以上一切题目

  • 我们能够很轻易的增添一个error非常处置惩罚。虽然不在runGenerator(...)中,然则很轻易从一个promise中监听error,并它他们的逻辑写在it.throw(..)内里,这时刻我们就能够用上try..catch`要领在我们的generator代码中去猎取和治理erros了。

  • 我们获得到一切的 control/trustability,完整不须要增添代码。

  • promise有着很强的笼统性,让我们能够完成一些多使命的并行操纵。

    比方:`Promise.all([ .. ])`就能够并行实行一个promise数组,yield虽然只能拿到一个promise,然则这个是一切子promise实行终了以后的鸠合数组。
    

先让我们看下error处置惩罚的代码:

function request(url) {
    return new Promise( function(resolve, reject) {
        //第一个参数是error
        makeAjaxCall(url, function(err, text) {
            if(err) reject(err);
            else resolve(text);
        });
    });
}

runGenerator(function *main() {
    try {
        var result1 = yield request('http://some.url.1');
    }
    catch(err) {
        console.log('Error:' + err);
        retrun;
    }
    var data = JSON.parse(result1);
    
    try{
        var result2 = yield request('http://some.url.2?id='+data.id);
    }
    catch(err) {
        console.log('Error:' + err);
        retrun;
    }
    var resp = JSON.parse(result2);
    console.log("The value you asked for: " + resp.value);
});

如果实行url的fetch时promise被reject(要求失利,或许非常)了,promise会给generator抛出一个非常,经由过程try..catch语句能够猎取到了。

如今,我们让promise来处置惩罚更庞杂的异步操纵:

function request(url) {
    return new Promise( function(resolve, reject) {
        makeAjax(url, resolve);
    })
    //猎取到返回的text值后,做一些处置惩罚。
    .then( function(text) {
        
            //如果我们拿到的是一个url就把text提早出来在返回
            if(/^http?:\/\/.+/.text(text)) {
                return request(text);            }
            //如果我们就是要一个text,直接返回
            else {
                return text;
            }
    });
}

runGenerator (function *main() {
    var search_terms = yield Promise.all([
        request( "http://some.url.1" ),
     request( "http://some.url.2" ),
     request( "http://some.url.3" )
    ]);

    var search_results = yield request(
        'http://some.url.4?search='+search_terms.join('+')
    );
    var resp = JSON.parse(search_results);
    
    console.log('Search results:'+resp.value);
});

Promise.all([ .. ]) 内里放了3个子promise,主promise完成后就会在runGenerator中恢复generator。子promise拿到的是一天重定向的url,我们会把它丢给下一个request要求,然后猎取到终究数据。

任何庞杂的异步功用都能够被promise搞定,而且你还能够用generator把这些流程写的像同步代码一样。只需你让yield返回一个promise。

ES7 async

如今能够轻微提下ES7了,它更像把runGenerator(..)这个异步实行逻辑做了一层封装。

async funtion main() {
    var result1 = await request('http://some.url.1');
    var data = JSON.parse(result1);
    
    var result2 = await request('http://some.url.2?id='+data.id);
    var resp = JSON.parse(result2);
    console.log( "The value you asked for: " + resp.value );
}

main();

我们直接挪用main()就能够实行完一切的流程,不须要挪用next,也不须要去完成runGenerator(..)之类的来治理promise逻辑。只须要把yield症结词换成await就能够通知异步要领,我们在这里须要比及一个promise后才会接着实行。

有了这些原生的语法支撑,是否是很酷。

小结

generator + yielded promise(s)的组合如今是最壮大,也是最文雅的异步流程治理编程体式格局。经由过程封装一层流实行逻辑,我们能够自动的让我们的generator实行终了,而且还能够像处置惩罚同步逻辑一样治理error事宜。

在ES7中,我们甚至连这一层封装都不须要写了,变得更轻易

参考

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