摘要:更好的異步編程上面的方法可以適用于那些比較簡單的異步工作流程。小結的組合目前是最強大,也是最優雅的異步流程管理編程方式。
訪問原文地址
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中,我們甚至連這一層封裝都不需要寫了,變得更方便
參考Asynchronous calls with ES6 generators
[Javascript] Promise, generator, async與ES6
The Hidden Power of ES6 Generators: Observable Async Flow Control
Going Async With ES6 Generators
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/80970.html
摘要:由于可以使用語句來暫停異步操作,這讓異步編程的代碼,很像同步數據流方法一樣。該臨時函數就叫做函數。下面就是簡單的函數轉換器。 訪問原文地址 對ES6的generators的介紹分為3個部分 第一部分base介紹及使用 第二部分基于generators和Promise實現最強大的異步處理邏輯 概述 Generator函數是協程在ES6的實現,用來做異步流程的封裝,最大特點就是可以交出...
摘要:異步編程是每個使用編程的人都會遇到的問題,無論是前端的請求,或是的各種異步。本文就來總結一下常見的四種處理異步編程的方法。利用一種鏈式調用的方法來組織異步代碼,可以將原來以回調函數形式調用的代碼改為鏈式調用。 異步編程是每個使用 JavaScript 編程的人都會遇到的問題,無論是前端的 ajax 請求,或是 node 的各種異步 API。本文就來總結一下常見的四種處理異步編程的方法。...
摘要:盲目使用替換后可能會導致預期意外的結果。在中,許多種方法來處理函數的參數默認值,參數數量,參數命名。此外,處理后的值,無論是解決還是拒絕的結果值,都是不可改變的。 這是一個 ES2015(ES6) 的Cheatsheet,其中包括提示、小技巧、最佳實踐和一些代碼片段,幫助你完成日復一日的開發工作。 Table of Contents var 與 let / const 聲明 代碼執行...
摘要:注是先前版本處理異步函數的方式,通過可以將異步函數封裝成,傳入普通參數后形成僅需要參數的偏函數,以此簡化調用代碼目前中的偏函數已經被無情地化了。 前幾天研究了TJ的koa/co4.x和一系列koa依賴的源碼,在知乎上做出了人生首次回答(而且我真得再也不想去知乎回答技術問題了_(:з」∠)_),因此把文字搬到這里。 ES2015 Generator/Yield 關于Generator...
摘要:換句話說,我們很好的對代碼的功能關注點進行了分離通過將使用消費值得地方函數中的邏輯和通過異步流程來獲取值迭代器的方法進行了有效的分離。但是現在我們通過來管理代碼的異步流程部分,我們解決了回調函數所帶來的反轉控制等問題。 本文翻譯自 Going Async With ES6 Generators 由于個人能力知識有限,翻譯過程中難免有紕漏和錯誤,還望指正Issue ES6 Gener...
閱讀 3929·2021-09-09 09:33
閱讀 1792·2021-09-06 15:14
閱讀 1932·2019-08-30 15:44
閱讀 3088·2019-08-29 18:36
閱讀 3773·2019-08-29 16:22
閱讀 2101·2019-08-29 16:21
閱讀 2541·2019-08-29 15:42
閱讀 1656·2019-08-29 11:00