摘要:關鍵字表示代碼在該處將會被阻塞式暫停阻塞的僅僅是函數代碼本身,而不是整個程序,但是這并沒有引起函數內部自頂向下代碼的絲毫改變。通過實現模式在通過實現理論的過程中已經有一些有趣的探索了。
至此本系列的四篇文章翻譯完結,查看完整系列請移步blogs
由于個人能力知識有限,翻譯過程中難免有紕漏和錯誤,望不吝指正issue
ES6 Generators: 完整系列
The Basics Of ES6 Generators
Diving Deeper With ES6 Generators
Going Async With ES6 Generators
Getting Concurrent With ES6 Generators
如果你已經閱讀并消化了本系列的前三篇文章:第一篇、第二篇、第三篇,那么在此時你已經對如何使用ES6 generator函數胸有成竹,并且我也衷心希望你能夠受到前三篇文章的鼓舞,實際去使用一下generator函數(挑戰極限),探究其究竟能夠幫助我們完成什么樣的工作。
我們最后一個探討的主題可能和一些前沿知識有關,甚至需要動腦筋才能夠理解(誠實的說,一開始我也有些迷糊)。花一些時間來練習和思考這些概念和示例。并且去實實在在的閱讀一些別人寫的關于此主題的文章。
此刻你花時間(投資)來弄懂這些概念對你長遠來看是有益的。并且我完全深信在將來JS處理復雜異步的操作能力將從這些觀點中應運而生。
正式的CSP(Communicating Sequential Processes)起初,關于該主題的熱情我完全受啟發于 David Nolen @swannodette的杰出工作。嚴格說來,我閱讀了他寫的關于該主題的所有文章。下面這些鏈接可以幫助你對CSP有個初步了解:
"Communicating Sequential Processes"
"ES6 Generators Deliver Go Style Concurrency"
"Extracting Processes"
OK,就我在該主題上面的研究而言,在開始寫JS代碼之前我并沒有編寫Clojure語言的背景,也沒有使用Go和ClojureScript語言的經驗。在閱讀上面文章的過程中,我很快就發現我有一點弄不明白了,而不得不去做一些實驗性學習或者學究性的去思考,并從中獲取一些有用的知識。
在這個過程中,我感覺我達到了和作者相同的思維境界,并且追求相同的目標,但是卻采取了另一種不那么正規的思維方式。
我所努力并嘗試去構建一個更加簡單的Go語言風格的CSP(或者ClojureScript語言中的core.async)APIs,并且(我希望)竟可能的保留那些潛在的能力。在閱讀我文章的那些聰明的讀者一定能夠容易的發現我對該主題研究中的一些缺陷和不足,如果這樣的話,我希望我的研究能夠演進并持續發展下去,我也會堅持和我廣大的讀者分享我在CSP上的更多啟示。
分解 CSP 理論(一點點)CSP究竟是什么呢?在CSP概念下講述的“communicating”、“Sequential”又是什么意思呢?“processes”有代表什么?
首先,CSP的概念是從Tony Hoare的書 "Communicating Sequential Processes"中首次被提及。這本書主要是一些CS理論上的東西,但是如果你對一些學術上的東西很感興趣,相信這本書是一個很好的開端。在關于CSP這一主題上我絕不會從一些頭疼的、難懂的計算機科學知識開始,我決定從一些非正式入口開始關于CSP的討論。
因此,讓我們先從“sequential”這一概念入手,關于這部分你可能已經相當熟悉,這也是我們曾經討論過的單線程行為的另一種表述或者說我們在同步形式的ES6 generator函數中也曾遇到過。
回憶如下的generator函數語法:
function *main() { var x = yield 1; var y = yield x; var z = yield (y * 2); }
上面代碼片段中的語句都按順序一條接一條執行執行,同一時間不能夠執行多條語句。yield 關鍵字表示代碼在該處將會被阻塞式暫停(阻塞的僅僅是 generator 函數代碼本身,而不是整個程序),但是這并沒有引起 *main() 函數內部自頂向下代碼的絲毫改變。是不是很簡單,難道不是嗎?
接下來,讓我們討論下「processes」。「processes」究竟是什么呢?
本質上說,一個 generator 函數的作用相當于虛擬的「進程」。它是一段高度自控的程序,如果 JavaScript 允許的話,它能夠和程序中的其他代碼并行運行。
說實話,上面有一點捏造事實了,如果 generator 函數能夠獲取到共享內存中的值(也就是說,如果它能夠獲取到一些除它本身內部的局部變量外的「自由變量」),那么它也就不那么獨立了。但是現在讓我們先假設我們擁有一個 generator 函數,它不會去獲取函數外部的變量(在函數式編程中通常稱之為「組合子」)。因此理論上 generator 函數可以在其自己的進程中獨立運行。
但是我們這兒所討論的是「processes」--復數形式--,因為更重要的是我們擁有兩個或者多個的進程。換句話說,兩個或者多個 generator 函數通常會同時出現在我們的代碼中,然后協作完成一些更加復雜的任務。
為什么將 generator 函數拆分為多個而不是一個呢?最重要的原因:實現功能和關注點的解耦。如果你現在正在著手一項 XYZ 的任務,你將這個任務拆分成了一些子任務,如 X, Y和 Z,并且每一個任務都通過一個 generator 函數實現,現在這樣的拆分和解耦使得你的代碼更加易懂且可維護性更高。
這個你將一個function XYZ()分解為三個函數X(),Y(),Z(),然后在X()函數中調用Y(),在Y()函數中調用Z()的動機是一樣的,我們將一個函數分解成多個函數,分離的代碼更加容易推理,同時也是的代碼可維護性增強。
我們可以通過多個 generator 函數來完成相同的事情
最后,「communicating」。這有表達什么意思呢?他是從上面--協程—的概念中演進而來,協程的意思也就是說多個 generator 函數可能會相互協作,他們需要一個交流溝通的渠道(不僅僅是能夠從靜態作用域中獲取到共享的變量,同時是一個真實能夠分享溝通的渠道,所有的 generator 函數都能夠通過獨有的途徑與之交流)。
這個通信渠道有哪些作用呢?實際上不論你想發送什么數據(數字 number,字符串 strings 等),你實際上不需要通過渠道來實際發送消息來和渠道進行通信。「Communication」和協作一樣簡單,就和將控制權在不同 generator 函數之間傳遞一樣。
為什么需要傳遞控制權?最主要的原因是 JS是單線程的,在同一時間只允許一個 generator 函數的執行。其他 generator 函數處于運行期間的暫停狀態,也就是說這些暫停的 generator 函數都在其任務執行過程中停了下來,僅僅是停了下來,等待著在必要的時候重新啟動運行。
這并不是說我們實現了(譯者注:作者的意思應該是在沒有其他庫的幫助下)任意獨立的「進程」可以魔法般的進行協作和通信。
相反,顯而易見的是任意成功得 CSP 實現都是精心策劃的,將現有的問題領域進行邏輯上的分解,每一塊在設計上都與其他塊協調工作。// TODO 這一段好難翻譯啊。
我關于 CSP 的理解也許完全錯了,但是在實際過程中我并沒有看到兩個任意的 generator 函數能夠以某種方式膠合在一起成為一個 CSP 模式,這兩個 generator 函數必然需要某些特殊的設計才能夠相互的通信,比如雙方都遵守相同的通信協議等。
通過 JS 實現 CSP 模式在通過 JS 實現 CSP 理論的過程中已經有一些有趣的探索了。
上文我們提及的 David Nolen 有一些有趣的項目,包括 Om和 core.async ,Koa通過其use(..)方法對 CSP 也有些有趣的嘗試。另外一個庫 js-csp完全忠實于 core.async/Go CSP API。
你應該切實的去瀏覽下上述的幾個杰出的項目,去發現通過 JS實現 CSP 的的不同途徑和實例的探討。
asynquence 中的 runner(..) 方法:為 CSP 而設計由于我強烈地想要在我的 JS 代碼中運用 CSP 模式,很自然地想到了擴展我現有的異步控制流的庫asynquence ,為其添加 CSP 處理能力。
我已經有了 runner(..)插件工具能夠幫助我異步運行 generator 函數(參見第三篇文章Going Async With Generators),因此對于我來說,通過擴展該方法使得其具有像CSP 形式一樣處理多個 generator函數的能力變得相對容易很多。
首選我需要解決的設計問題:我怎樣知道下一個處理哪個 generator 函數呢?
如果我們在每個 generator 函數上面添加類似 ID一樣的標示,這樣別的 generator 函數就能夠很容易分清楚彼此,并且能夠準確的將消息或者控制權傳遞給其他進程,但是這種方法顯得累贅且冗余。經過眾多嘗試后,我找到了一種簡便的方法,稱之為「循環調度法」。如果你要處理一組三個的 generator 函數 A, B, C,A 首先獲得控制權,當 A 調用 yield 表達式將控制權移交給 B,再后來 B 通過 yield 表達式將控制權移交給 C,一個循環后,控制權又重新回到了 A generator 函數,如此往復。
但是我們究竟如何轉移控制權呢?是否需要一個明確的 API 來處理它呢?再次,經過眾多嘗試后,我找到了一個更加明確的途徑,該方法和Koa 處理有些類似(完全是巧合):每一個 generator 對同一個共享的「token」具有引用,yield表達式的作用僅僅是轉移控制權。
另外一個問題,消息渠道究竟應該采取什么樣的形式呢。一端的頻譜就是你將看到和 core.async 和 js-csp(put(..和take(..))相似的 API 設計。經過我的嘗試后,我傾向于頻譜的另一端,你將看到一個不那么正式的途徑(甚至不是一個 API,僅僅是共享一個像array一樣的數據結構),但是它又是那么的合適且有效。
我決定使用一個數組(稱作messages)來作為消息渠道,你可以采取任意必要的數組方法來填充/消耗數組。你可以使用push()方法來想數組中推入消息,你也可以使用pop()方法來將消息從數組中推出,你也可以按照一些約定慣例想數組中插入不同的消息,這些消息也許是更加復雜的數據接口,等等。
我的疑慮是一些任務需要相當簡單的消息來傳遞,而另外一些任務(消息)卻更加復雜,因此我沒有在這簡單的例子上面花費過多的精力,而是選擇了不去對 message 渠道進行格式化,它就是簡簡單單的一個數組。(因此也就沒有為array本身設計特殊的 API)。同時,在你覺得格式化消息渠道有用的時候,你也可以很容易的為該消息傳遞機制添加格外的格式化(參見下面的狀態機的事例)。
最后,我發現這些 generator 函數「進程」依然受益于多帶帶的 generator 函數的異步能力。換句話說,如果你通過 yield 表達式不是傳遞的一個「control-token」,你通過 yield 表達式傳遞的一個 Promise (或者異步序列),runner(..)的運行機制會暫停并等待返回值,并且不會轉移控制權。他會將該返回值傳遞會當前進程(generator 函數)并保持該控制權。
上面最后一點(如果我說明得正確的話)是和其他庫最具爭議的地方,從其他庫看來,真是的 CSP 模式在 yield 表達式執行后移交控制權,然而,我發現在我的庫中我這樣處理卻相當有用。(譯者注:作者就是這樣自信)
一個簡單的 FooBar 例子我們已經理論充足了,讓我們看一些代碼:
// Note: omitting fictional `multBy20(..)` and // `addTo2(..)` asynchronous-math functions, for brevity function *foo(token) { // grab message off the top of the channel var value = token.messages.pop(); // 2 // put another message onto the channel // `multBy20(..)` is a promise-generating function // that multiplies a value by `20` after some delay token.messages.push( yield multBy20( value ) ); // transfer control yield token; // a final message from the CSP run yield "meaning of life: " + token.messages[0]; } function *bar(token) { // grab message off the top of the channel var value = token.messages.pop(); // 40 // put another message onto the channel // `addTo2(..)` is a promise-generating function // that adds value to `2` after some delay token.messages.push( yield addTo2( value ) ); // transfer control yield token; }
OK,上面出現了兩個 generator「進程」,*foo() 和 *bar()。你會發現這兩個進程都將操作token對象(當然,你可以以你喜歡的方式稱呼它)。token對象上的messages屬性值就是我們的共享的消息渠道。我們可以在 CSP 初始化運行的時候給它添加一些初始值。
yield token明確的將控制權轉一個「下一個」generator 函數(循環調度法)。然后yield multBy20(value)和yield addTo2(value)兩個表達式都是傳遞的 promises(從上面虛構的延遲數學計算方法),這也意味著,generator 函數將在該處暫停知道 promise 完成。當 promise 被解決后(fulfill 或者 reject),當前掌管控制權的 generator 函數重新啟動繼續執行。
無論最終的 yield的值是什么,在我們的例子中yield "meaning of..."表達式的值,將是我們 CSP 執行的最終返回數據。
現在我們兩個 CSP 模式的 generator 進程,我們怎么運行他們呢?當然是使用 asynquence:
// start out a sequence with the initial message value of `2` ASQ( 2 ) // run the two CSP processes paired together .runner( foo, bar ) // whatever message we get out, pass it onto the next // step in our sequence .val( function(msg){ console.log( msg ); // "meaning of life: 42" } );
很明顯,上面僅是一個無關緊要的例子,但是其也能足以很好的表達 CSP 的概念了。
現在是時候去嘗試一下上面的例子(嘗試著修改下值)來搞明白這一概念的含義,進而能夠編寫自己的 CSP 模式代碼。
另外一個「玩具」演示用例如果那我們來看看最為經典的 CSP 例子,但是希望大家從文章上面的解釋及發現來入手,而不是像通常情況一樣,從一些學術純化論者的觀點中導出。
Ping-pong。多么好玩的游戲,啊!它也是我最喜歡的體育運動了。
讓我們想象一下,你已經完全實現了打乒乓球游戲的代碼,你通過一個循環來運行這個游戲,你有兩個片段的代碼(通常,通過if或者switch語句來進行分支)來分別代表兩個玩家。
你的代碼運行良好,并且你的游戲就像真是玩耍乒乓球一樣!
但是還記得為什么我說 CSP 模式是如此有用呢?它完成了關注點和功能模塊的分離。在上面的乒乓球游戲中我們怎么分離的功能點呢?就是這兩位玩家!
因此,我們可以在一個比較高的層次上,通過兩個「進程」(generator 函數)來對我們的游戲建模,每個進程代表一位玩家,我們還需要關注一些細節問題,我們很快就感覺到還需要一些「膠水代碼」來在兩位玩家之間進行控制權的分配(交換),這些代碼可以作為第三個 generator 函數進程,我們可以稱之為裁判員。
我們已經消除了所有可能會遇到的與專業領域相關的問題,比如得分,游戲機制,物理學常識,游戲策略,電腦玩家,控制等。在我們的用例中我們只關心模擬玩耍乒乓球的反復往復的過程,(這一過程也正隱喻了 CSP 模式中的轉移控制權)。
想要親自嘗試下演示用例?那就運行把(注意:使用最新每夜版 FF 或者 Chrome,并且帶有支持 ES6,來看看 generators 如何工作)
現在,讓我們來一段一段的閱讀代碼。
首先,asynquence 序列長什么樣呢?
ASQ( ["ping","pong"], // player names { hits: 0 } // the ball ) .runner( referee, player, player ) .val( function(msg){ message( "referee", msg ); } );
我們給我們的序列設置了兩個初始值["ping", "pong"]和{hits: 0}。我們將在后面討論它們。
接下來,我們設置 CSP 運行 3 個進程(協作程序):*referee() 和 兩個*player()實例。
游戲最后的消息傳遞給了我們序列的第二步,我們將在序列第二步中輸出裁判傳遞的消息。
裁判進程的代碼實現:
function *referee(table){ var alarm = false; // referee sets an alarm timer for the game on // his stopwatch (10 seconds) setTimeout( function(){ alarm = true; }, 10000 ); // keep the game going until the stopwatch // alarm sounds while (!alarm) { // let the players keep playing yield table; } // signal to players that the game is over table.messages[2] = "CLOSED"; // what does the referee say? yield "Time"s up!"; }
我們稱「控制中token」為table,這正好和(乒乓球游戲)專業領域中的稱呼想一致,這是一個很好的語義化,一個游戲玩家通過用拍子將球「yields 傳遞 table」給另外一個玩家,難道不夠形象嗎?
while循環的作用就是在*referee()進程中,只要警報器沒有吹響,他將不斷地通過 yield 表達式將 table 傳遞給玩家。當警報器吹響,他掌管了控制權,宣布游戲結束「時間到了」。
現在,讓我們來看看*player()generator 函數(在我們的代碼中我們兩次使用了該實例):
function *player(table) { var name = table.messages[0].shift(); var ball = table.messages[1]; while (table.messages[2] !== "CLOSED") { // hit the ball ball.hits++; message( name, ball.hits ); // artificial delay as ball goes back to other player yield ASQ.after( 500 ); // game still going? if (table.messages[2] !== "CLOSED") { // ball"s now back in other player"s court yield table; } } message( name, "Game over!" ); }
第一位玩家從消息數組中取得他的名字「ping」,然后,第二位玩家取得他的名字「pong」,這樣他們可以很好的分辨彼此的身份。兩位玩家同時共享ball這個對象的引用(通過他的hits計數)。
只要玩家沒有從裁判口中聽到結束的消息,他們就將通過將計數器加一來「hit」ball(并且會輸入一條計數器消息),然后,等待500ms(僅僅是模擬乒乓球的飛行耗時,不要還以為乒乓球以光速飛行呢)。
如果游戲依然進行,游戲玩家「yield 傳遞 table」給另外一位玩家。
就是這樣!
查看一下演示用例的代碼獲取一份完整用例的代碼,看看不同代碼片段之間是如何協同工作的。
狀態機:Generator 協同程序最后一個例子,通過一個 generator 函數集合組成的協同程序來定義一個狀態機,這一協同程序都是通過一個簡單的工具函數來運行的。
演示用例(注意:使用最新的每夜版 FF 或者 Chrome,并且支持 ES6的語法特性,看看 generator 函數如何工作)
首先讓我們來定義一個工具函數,來幫助我們控制我們有限的狀態:
function state(val, handler) { // make a coroutine handler (wrapper) for this state return function*(token) { // state transition handler function transition(to) { token.messages[0] = to; } // default initial state (if none set yet) if (token.messages.length < 1) { token.messages[0] = val; } // keep going until final state (false) is reached while (token.messages[0] !== false) { // current state matches this handler? if (token.messages[0] === val) { // delegate to state handler yield *handler( transition ); } // transfer control to another state handler? if (token.messages[0] !== false) { yield token; } } }; }
state(..) 工具函數為一個特殊的狀態值創建了一個generator 代理的上層封裝,它將自動的運行狀態機,并且在不同的狀態轉換下轉移控制權。
按照慣例來說,我已經決定使用的token.messages[0]中的共享數據插槽來儲存狀態機的當前狀態值,這也意味著你可以在序列的前一個步驟來對該狀態值進行初始化,但是,如果沒有傳遞該初始化狀態,我們簡單的在定義第一個狀態是將該狀態設置為初始狀態。同時,按照慣例,最后終止的狀態值設置為false。正如你認為合適,也很容易改變該狀態。
狀態值可以是多種數據格式之一,數字,字符串等等,只要改數據可以通過嚴格的===來檢測相等性,你就可以使用它來作為狀態值。
在接下來的例子中,我展示了一個擁有四個數組狀態的狀態機,并且其運行運行:1 -> 4 -> 3 -> 2。該順序僅僅為了演示所需,我們使用了一個計數器來幫助我們在不同狀態間能夠多次傳遞,當我們的 generator 狀態機最終遇到了終止狀態false時,異步序列運行至下一個步驟,正如你所期待那樣。
// counter (for demo purposes only) var counter = 0; ASQ( /* optional: initial state value */ ) // run our state machine, transitions: 1 -> 4 -> 3 -> 2 .runner( // state `1` handler state( 1, function*(transition){ console.log( "in state 1" ); yield ASQ.after( 1000 ); // pause state for 1s yield transition( 4 ); // goto state `4` } ), // state `2` handler state( 2, function*(transition){ console.log( "in state 2" ); yield ASQ.after( 1000 ); // pause state for 1s // for demo purposes only, keep going in a // state loop? if (++counter < 2) { yield transition( 1 ); // goto state `1` } // all done! else { yield "That"s all folks!"; yield transition( false ); // goto terminal state } } ), // state `3` handler state( 3, function*(transition){ console.log( "in state 3" ); yield ASQ.after( 1000 ); // pause state for 1s yield transition( 2 ); // goto state `2` } ), // state `4` handler state( 4, function*(transition){ console.log( "in state 4" ); yield ASQ.after( 1000 ); // pause state for 1s yield transition( 3 ); // goto state `3` } ) ) // state machine complete, so move on .val(function(msg){ console.log( msg ); });
上面代碼的運行機制是不是非常簡單。
yield ASQ.after(1000)表示這些 generator 函數可以進行 promise/sequence等異步工作,正如我們先前縮減,yield transition(..)告訴我們怎樣將控制權傳遞給下一個狀態。
我們的state(..)工具函數真實的完成了yield *代理這一艱難的工作,像變戲法一樣,使得我們能夠以一種簡單自然的形式來對狀態進行操控。
總結CSP 模式的關鍵點在于將兩個或者多個 generator「進程」組合在一起,并為他們提供一個共享的通信渠道,和一個在其彼此之間傳遞控制權的方法。
市面上已經有很多庫多多少少實現了GO 和 Clojure/ClojureScript APIs 相同或者相同語義的 CSP 模式。在這些庫的背后是一些聰明而富有創造力的開發者門,這些庫的出現,也意味著需要更大的資源投入以及研究。
asynquence 嘗試著通過著通過不那么正式的方法卻依然希望給大家呈現 CSP 的運行機制,只不過,asynquence 的runner(..)方法使得了我們通過 generator 模擬 CSP 模式變得如此簡單,正如你在本篇文章所學的那樣。
asynquence CSP 模式中最為出色的部分就是你將所有的異步處理手段(promise,generators,flow control 等)以及剩下的有機的組合在了一起,你不同異步處理結合在一起,因此你可以任何合適的手段來處理你的任務,而且,都在同一個小小的庫中。
現在,在結束該系列最后一篇文章后,我們已經完成了對 generator 函數詳盡的研究,我所希望的是你能夠在閱讀這些文章后有所啟發,并對你現有的代碼進行一次徹底革命!你將會用 generator 函數創造什么奇跡呢?
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/84814.html
摘要:每個任務必須顯式地掛起自己,在任務切換發生時給予它完全的控制。在這些嘗試中,數據經常在任務之間共享。但由于明確的暫停,幾乎沒有風險。 翻譯自 github 概述 什么是generators? 我們可以把generators理解成一段可以暫停并重新開始執行的函數 function* genFunc() { // (A) console.log(First); yi...
摘要:關于協程和中的什么是協程進程和線程眾所周知,進程和線程都是一個時間段的描述,是工作時間段的描述,不過是顆粒大小不同,進程是資源分配的最小單位,線程是調度的最小單位。子程序就是協程的一種特例。 關于協程和 ES6 中的 Generator 什么是協程? 進程和線程 眾所周知,進程和線程都是一個時間段的描述,是CPU工作時間段的描述,不過是顆粒大小不同,進程是 CPU 資源分配的最小單位,...
摘要:平日學習接觸過的網站積累,以每月的形式發布。年以前看這個網址概況在線地址前端開發群月報提交原則技術文章新的為主。 平日學習接觸過的網站積累,以每月的形式發布。2017年以前看這個網址:http://www.kancloud.cn/jsfron... 概況 在線地址:http://www.kancloud.cn/jsfront/month/82796 JS前端開發群月報 提交原則: 技...
摘要:平日學習接觸過的網站積累,以每月的形式發布。年以前看這個網址概況在線地址前端開發群月報提交原則技術文章新的為主。 平日學習接觸過的網站積累,以每月的形式發布。2017年以前看這個網址:http://www.kancloud.cn/jsfron... 概況 在線地址:http://www.kancloud.cn/jsfront/month/82796 JS前端開發群月報 提交原則: 技...
閱讀 2520·2023-04-25 14:54
閱讀 595·2021-11-24 09:39
閱讀 1803·2021-10-26 09:51
閱讀 3845·2021-08-21 14:10
閱讀 3477·2021-08-19 11:13
閱讀 2691·2019-08-30 14:23
閱讀 1804·2019-08-29 16:28
閱讀 3347·2019-08-23 13:45