原文地址:http://zodiacg.net/2015/08/javascript-async-control-flow/javascript
隨着ES6標準逐漸成熟,利用Promise和Generator解決回調地獄問題的話題一直很熱門。可是對解決流程控制/回調地獄問題的各類工具認識仍然比較麻煩。最近兩天看了不少文章,想出幾個場景把各類異步流程方式類比一下,但願能有助於理解他們的實現。java
須要說明的是類比只能反映被類比的事物的一個方面,必然有其反映不到的部分,不能徹底以類比來理解各類異步控制的本質。因此僅用於簡化理解,快速入門,依然須要閱讀有深刻研究的文章來加深對各類異步流程控制的方法的掌握。node
文章中沒有嚴格使用Node.js核心模塊的函數名,而是僞造的函數名來方便在各類方式下保持一致性和簡化書寫。git
從最簡單的同步開始。以一段經典的代碼爲例吧,傳入文件名,從文件中讀取內容並按JSON格式解析,把其中的一部份內容發還給用戶。es6
function foo(filename){ var file = readFile(filename); var json = parseJSON(file); return json.someContent; } var resultA=foo('first.json'); var resultB=foo('second.json');
其中readFile
和parseJSON
顯而易見是同步、阻塞的函數。github
咱們構造這樣一個場景,M(ain)表明着解釋引擎的主線程,坐在櫃檯前等待用戶提交請求。web
來了兩個用戶A和用戶B,用戶A排在前面。json
用戶A: 你好,我要讀文件
first.json
。
M:好的你等一下我去拿。
【M離開了櫃檯去拿文件】
M:好的我回來了,這是你要的first.json
的內容。而後呢?
用戶A:而後把內容按JSON解析一下。
M:你等等我去解析一下。
【M離開了櫃檯去解析文件內容】
M:好了解析完了,而後呢?
用戶A:我要裏面的someContent
部分。
M:給你。再見。segmentfault用戶B:你好,我要讀文件
second.json
。
…………promise
能夠看到同步阻塞顧名思義,操做一步一步進行,遇到IO操做每一步都要等待,用戶A不處理完,用戶B也進不來。
仍然是這個需求,此次的代碼就比較像Node.js裏的常見形式了,此處爲了簡單忽略了err。因爲回調套回調,縮進如同金字塔,被稱做回調地獄。固然回調地獄遠不是一個縮進金字塔那麼簡單。
function foo(filename, cb){ readFile(filename,function(file){ parseJSON(file,function(json){ cb(json.someContent); }); }); } foo('first.json',cbA); foo('second.json',cbB);
依然是來了兩個用戶A和用戶B
用戶A:你好,我要讀文件
first.json
,按JSON解析後把裏面的someContent
寄往cbA
發回給我。
M:好的。
【M開始寫信,信封上寫上文件讀取處readFile
,拿出一張信紙寫到:「讀取first.json
,而後內容放進後附信封中寄出」。】
【M又拿出一個信封,寫上JSON解析處parseJSON
,信紙上寫到「把給你的file
按JSON解析,而後把裏面的someContent
放到所附信封裏寄出」。】
【M又又拿出一個信封,寫上cbA
,而後把這個信封放進了剛纔的信封裏,又把剛纔的信封塞進了第一個信封裏。】
【M把鼓鼓囊囊的信封扔到郵箱裏就無論了】
M:下一位!
用戶B:你好……
寫幾封信的時間比本身跑出去取文件要快的多。異步操做帶來的處理速度提高是顯而易見的。
可是爲了保證業務流程的銜接,信裏面就包含了後續一切須要進行的操做,層層包裹。第一封信寄出,M就既無從得知信走到了何處,也沒法控制readFile
和parseJSON
是否是如本身所想寄出了給他的信封,有沒有私自複印了多寄了一兩封。
這纔是回調地獄真正危險的地方,缺少控制。
引入Promise以後,不少人就覺得能解決回調地獄了,其實否則。在某些場景下只是讓縮進好看了一點而已。有些場景下縮進也無法好看,須要書寫的回調不只不會減小還會增多。
function foo(filename,cb){ readFile(filename) .then(parseJSON(file)) .then(function(json){ cb(json.someContent)}); } foo('first.json',cbA); foo('second.json',cbB);
固然這裏面的readFile
和parseJSON
已是Promise化了的。
依然是來了兩個用戶A和用戶B
用戶A:你好,我要讀文件
first.json
,按JSON解析後把裏面的someContent
寄往cbA
發回給我。
M:好的。
【M叫來了一個辦事員小P】
M:小P你聽好,先去找readFile
讀取first.json
,而後把內容給parseJSON
讓他解析一下,最後把解析的內容裏的someContent
寄給cb
,懂了嗎?
P:我辦事你放心!
【小P離開了櫃檯】
M:下一位!
用戶B:你好……
小P是一位M信得過的辦事員,M相信他可以挨個去找該找的部門,不偷工減料也不毛手毛腳。讓小P去辦事比寄一封信靠譜的多,M依然能很快回過頭來繼續應付下一個用戶請求。
固然實際場景中雖然寫的時候.then
一會兒連起來寫完,並非真的一會兒把內容都交給同一位小P/同一個Promise。更像是一個Promise公司,每一個操做進行完後都由一位Promise公司的辦事員進行下一步操做。
Promise物如其名,使用Promise重要的就是Promise的可信性,好比Promise的狀態不可逆,好比fulfill
回調只會被調用一次。Promise並非迴避書寫回調,而是用一種更可靠的方式來書寫回調。
這裏只談co不談Generator,是由於Generator並非爲解決異步流程控制而生的,而TJ大神用co把Generator和Thunk/Promise結合在一塊兒提供了新的異步流程控制的方法。
var foo=co(function*(filename){ var file = yield readFile(filename); var json = yield parseJSON(file); return json.someContent; }); foo('first.json').then(cbA); foo('second.json').then(cbB);
咦?這代碼看起來跟同步的怎麼差很少。
此次咱們換個方法描述,同樣是來了用戶A和用戶B,可是先從用戶A的視角來看這件事情。
用戶A: 你好,我要讀文件
first.json
。
M:好的你等一下我去拿。
【M離開了櫃檯】
M:我回來了,這是你要的first.json
的內容。而後呢?
用戶A:而後把內容按JSON解析一下。
M:你等等我去解析一下。
【M離開了櫃檯】
M:好了解析完了,而後呢?
用戶A:我要裏面的someContent
部分。
M:給你。再見。
是否是看起來跟同步如出一轍?實際上從M的角度看這件事情呢?
用戶A: 你好,我要讀文件
first.json
。
M:好的你等一下我去拿。
【M離開了櫃檯】
M:小P來一下!去readFile
讀first.json
,回來叫我。
P:好的我這就去。
【M轉向了另外一個櫃檯窗口】
M:你好。
用戶B:你好,我要讀文件second.json
。
…………
…………
P:M,first.json
拿回來了。
【M轉向第一個櫃檯】
M:我回來了,這是你要的first.json
的內容。而後呢?
用戶A:而後把內容按JSON解析一下。
M:你等等我去解析一下。
【M離開了櫃檯】
M:小P來一下!去parseJSON
把這堆東西解析一下,回來叫我。
P:好的我這就去。
【M轉向了另外一個櫃檯窗口】
…………
…………
真相大白了,M並無親自去拿文件解析JSON,而是叫來了不辭辛苦的小P幹活,本身在用戶A面前假裝成被佔用了全部的時間的樣子,其實偷偷去接待別的用戶了。
利用Generator能夠用yield
中斷執行,再在外部經過next
喚醒繼續執行的特性,co把Generator的next
寫到Promise的then
裏面從而實現循環調用。使用了co以後,代碼看起來跟同步很是相像,寫起來符合人正常的同步思惟,甚至可使用同步的流程控制語句好比for。可是執行起來卻能充分利用異步帶來的性能優點。
順便提一句,co看起來已經很是像async/await方式了。Node.js中一樣近似於async/await方式的還有asyncawait庫,它不依賴generator而是依賴於node-fiber,看名字大概就是Node裏的一個纖程的實現吧。因爲不須要generator,對於諸如Coffescript和Typescript類的語言支持很是好。
以上就是對Javascript中最近常討論的幾種異步流程控制的簡單類比說明。這種理解方式很是粗淺,並且有不少問題並不像上面寫的那樣那麼簡單。要想使用好異步,仍是要多讀一些更爲深刻的文章。