用Generators解決callback金字塔

1000copy發表於2016-11-22

what is generator

Generators算得上js的一個新概念函式。它看起來像是一個函式,但是可以暫停執行。從語法上看,有3點關注:

  1. 函式名前有一個*
  2. 函式內有yield
  3. 呼叫時返回一個物件,對這個物件呼叫next才會繼續執行

你的node支援generator了嗎?

在node 0.11以上,對node必須加入–harmony 引數,即可支援:

看到undefined就說明支援了。如果不加引數,預設不支援。你會看到

Generators in ES6

宣告一個generator 是這樣的:

如果想要 generator 提供一個值並暫停,那麼需要使用yeild 關鍵字。yield 就像 return 一樣返回一個值。和它不同的是,yield會暫停函式。

我們做了一個迭代器,叫做 ticketGenerator. 我們可以和它要一個值,然後它返回1,然後暫停。依次返回2,3:

現在我們返回最大為3 ,不太有用.我們可以遞增到無窮。無窮數列來了。

再來一遍:

每次疊加,無窮列舉。這就有點意思了。

干預yield返回值

除了可以列舉累加外, next() 還有第二種用法:如果你傳遞一個值給next,它會作為yield 語句的返回值。我們可以利用這個特性,把剛剛的無限數列做一次重置:

這樣當我們呼叫next(true)的時候,i會等於-1,從而重設了i值到初始值。

無需困惑, yield i 會把i發到generator的呼叫next處,但是generator內部我們使用next提供的值(如果提供了的話)。

看看效果:

這就是generator。古怪,有趣。接下來會繼續分析它的應用,在解決callback hell方面。

問題

拿一個案例開刀吧。我們來看一個delay函式,延遲一些時間,然後列印點文字:

呼叫它也是常見程式碼:

為了保證兩次列印的次序,我們唯一的辦法,就是通過callback來完成。

要是延遲多幾次,比如12次,我們需要12次巢狀的callback,程式碼不段向右延伸。哇,回撥金字塔。

呼叫Generators

非同步是node的靈魂。可是非同步的麻煩在於callback,因為我餓每年需要等待完成通知,所以需要callback。

有了 generators,我們可以讓程式碼等。無需callback。可以用generators 在每個非同步呼叫進行中,在呼叫next()之前暫停執行。還記得yield/next 嗎?這是generators的絕活。

怎麼弄?

我們首先得知道,我們要把非同步呼叫暫停需要用到generator ,而不是典型的函式,所以加個星在函式前:

加入delay呼叫。delay需要callback。這個callback需要呼叫generator.next(),以便繼續程式碼。我們先放一個空的callback:

現在程式碼依然是非同步的。加入yield:

又近了一步。但是需要有人告訴generator向前走,走起來。

關鍵概念在這裡:當delay完成時,需要在它的callback內執行點東西,這些東西讓generator向前走(呼叫next)

這個函式,且不管如何實現,我們知道它得叫做resume:

我們得把定義好的resume傳遞給 myDelayedMessages

變魔術了…

如何實現resume,它又如何知道我們的generator?

給generator 函式加個外套,外套函式的功能就是啟動generator,傳遞寫好的resume,第一次撥動generator(呼叫next),等待resume被呼叫,在resume內繼續撥動generator。這樣generator就可以滾動起來了:

有點燒腦。當年寫作名噪一時的“goto statement considered harmful”的作者,看到此程式碼非得氣死。這裡沒有一行goto,卻跳來跳去的比有goto的還難。

注意哦,我們利用了next的第二個特性。resume 就是傳遞給callback 的函式,它因此接受了delay提供的值,resume傳遞這個值給 next, 故而 yield 語句返回了我們的非同步函式的結果。非同步函式的結果於是被console列印出來。就像“倒脫靴”。

程式碼整合起來:

就是這樣。我們呼叫兩次delay,按照次序執行,卻沒有callback的巢狀。如果要呼叫12次,也還是沒有callback巢狀。如果你依然迷惑不解,我們再次分步來一遍》

  • run 以generator 為引數,並且內部建立了一個resume 函式
  • run 建立一個generator函式,傳遞resume給它
  • 然後run第一次呼叫next,啟動了generator函式到yield
  • generator 遇到了第一個yield,並呼叫yield後的delay,然後暫停。
  • delay 在1000ms後完成,呼叫resume
  • resume 告訴generator 在走一步。並且傳遞delay的結果給run函式,由console 列印
  • generator 遇到第二個yield, 呼叫delay ,再次暫停
  • delay 在1200ms後完成,呼叫resume
  • resume 告訴generator 在走一步。並且傳遞delay的結果給run函式,由console 列印

co 更好

上面談到的做法,確實可以把非同步改成同步了。可是並不太完美:比如resume顯得比較突兀,在比如只能在callback返回一個值。不夠通用。

這樣的話,可以考慮TJ開發的co。連resume的宣告和引用也省掉了。還是以delay為例:

為了和co適配,delay需要做些修改,去掉callback,返回一個帶callback的函式,把計算結果通過callback傳遞出去。第一個引數依照node的規矩,留給err。

更絕。怪的不TJ被社群成為大神。

再來一個。readFile(file,callback),作為常見的非同步函式如何修改?

可是,readFile改造這樣過工作,純粹就是boilerplate !所以,有人做了這樣的工作。安裝co-fs,就可以:

node.js真是玩梯雲縱。以為已經很好了,還是有人在加入一把火。

所以,值得去npm看看,查詢下co-打頭的庫,有1000+個,要不要獨立出去:):

打個總結

成功。我們用generator替換了callback。我們這樣做到的:

  • 建立一個run函式,以 generator 為引數, 並傳遞一個 resume 函式給它
  • 傳遞resume 函式,單步推動generator ,返回任何非同步callback獲得的值給run
  • 傳遞resume 作為非同步呼叫callback . 這些非同步函式一旦完成,就呼叫callback,因此就是呼叫resume。允許resume推動generator

generators替代“callback hell” 是否最佳是可爭論的,但是這個練習可以幫助你理解到ES6的generators 和iterators

原文:http://modernweb.com/2014/02/10/replacing-callbacks-with-es6-generator…
參考:

Harmony Generator, yield, ES6, co框架學習 – http://bg.biedalian.com/2013/12/21/harmony-generator.html
Koa, co and coruntine – Harmony – 前端亂燉 – http://www.html-js.com/article/1752

fork me

fork me from :https://github.com/1000copy/learningnode/blob/master/nodebook/generato…

相關文章