這一篇是在實際工程中遇到的一個可貴的例子;反映在Node裏兩種編程範式的設計衝突。這種衝突具備普適性,但本文僅分析問題本質,不探討更高層次的抽象。node
我在寫一個相似HTTP的資源協議,叫RP,Resource Protocol,和HTTP不一樣的地方,RP是構建在一箇中立的傳輸層上的;這個傳輸層裏最小的數據單元,message,是一個JSON對象。npm
協議內置支持multiplexing,即一個傳輸層鏈接能夠同時維護多個RP請求應答過程。編程
考慮客戶端request
類設計,相似Node內置的HTTP Client,或流行的npm包,如request
或 superagent
;promise
能夠採用EventEmitter
方式emit error
和response
s事件,也能夠採用Node Callback的形式,須要使用者提供接口形式爲(err, res) => {}
的callback函數。服務器
隨着async/await的流行,request
類也能夠提供一個.then
接口,用以下方式實現(實際上superagent
就是這麼實現的):併發
class Request extends Duplex { constructor () { super() ... this.promise = new Promise((resolve, reject) => { this.resolve = resolve this.reject = reject }) } then (...args) { return this.promise.then(...args) } }
RP的實際設計,形式和你們熟悉的HTTP Client有一點小區別,response對象自己不是stream,而是把stream作爲一個property提供。換句話說,callback函數形式爲:異步
(err, { data, chunk, stream }) => {}
若是請求返回的不是stream,則data
或者chunk
有值;若是返回的是stream,則僅stream
有值,且爲stream.Readable
類型。async
這個形式上的區別和本文要討論的問題無關。函數
RP底層從傳輸層取二進制數據,解析出message,而後emit給上層;它採用了一個簡單方式,循環解析收到的data chunk,直到沒有完整的message爲止。性能
這意味着能夠在一個tick裏分發多個消息。request
對象也必須可以在一個tick裏處理多個來自服務端的消息。
咱們具體要討論的狀況是服務器連續發了這樣兩條消息:
第一條意思是後面還有message stream,第二條abort指server發生意外沒法繼續發送了。
在request
對象收到第一條消息時,它建立response
對象,包含stream
對象:
this.res = { stream: new stream.Readabe({...}) } // this.emit('response', this.res) // this.callback(null, this.res) this.resolve(this.res)
象註釋中emit
或trigger使用者提供的callback
,都沒有問題;但若是調用resolve
,注意,Promise是保證異步的,這意味着使用者經過then
提供的onFulfilled
,不會在當前tick被調用。
接下來第二條消息,abort,在同一個tick被處理;但這個時候,由於使用者還沒來得及掛載上任何listener,包括error handler,若是設計上要求這個stream emit error——很合理的設計要求——此時,按照Node的約定,error沒有handler,整個程序crash了。
這個問題的dirty fix有不少種辦法。
首先request.handleMessage
方法,若是沒法同步完成對message的處理,而message的處理順序又須要保證,它應該buffer message,這是node裏最多見的一種synchronize方式,表明性的實現就是stream.Writable
。
但這裏有一個困難,this.resolve
這個函數沒有callback提供,必須預先知道運行環境的Promise實現方式;在node裏是nextTick
,因此在this.resolve
以後nextTick
一下,同時buffer其它後續消息的處理,可讓使用者在onFulfilled
函數中給stream掛載上handler。
這裏能夠看出,callback和emitter其實是同步的。
當調用callback或者listener時,request和使用者作了一個約定,你必須在這個函數內作什麼(在對象上掛載全部的listener),而後我繼續作什麼(處理下一個消息,emit data或者error);這至關因而interface protocol對順序的約定。
咱們能夠稱之爲synchronous sequential composition,是程序語義意義上的。
對應的asynchronous版本呢?
若是咱們不去假設運行環境的Promise的實現呢?它應該和同步版本的語義同樣對吧。
再回頭看看問題,假如stream emit error不會致使系統crash,使用者在onFulfilled
拿到{ stream }
這個對象時,它看到了什麼?一個已經發生錯誤後結束了的stream。
這個可能使用上會難過一點,須要判斷一下,但還感受不出是多大的問題。
再進一步,若是是另外一種狀況呢?Server在一個chunk裏發來了3個消息;
這個時候使用者看到的仍是一個errored stream,data去哪裏了呢?你還能說asynchronous sequential composition的語義和synchronous的一致麼?不能了對吧,同步的版本處理了data,極可能對結果產生影響。
在理想的狀況下,sequential composition,不管是synchronous的,仍是asynchronous的,語義(執行結果)應該一致。
那麼來看看如何作到一個與Promise A+的實現無關的作法,保證異步和同步行爲一致。
若是你願意用『通信』理解計算,這個問題的答案很容易思考出來:假想這個異步的handler位於半人馬座阿爾法星上,那咱們惟一能作的事情是老老實實按照事件發生的順序,發送給它,不能打亂順序,就像咱們收到他們時同樣。
可是當咱們把進來的message,翻譯實現成stream時,沒能保證這個order,包括:
這是問題的root cause,當咱們異步處理一個消息序列時,前面寫的實現break了順序和內容的完整性。
在數學思惟上,咱們說Promise增長了一個callback/EventEmitter不具有的屬性,deferred evaluation,是一個編程中罕見的temporal屬性;固然這不奇怪,由於這就是Promise的目的。
同時Promise -> Value還有一個屬性是它能夠被不一樣的使用者訪問屢次,保持了Value的屬性。
這也不奇怪。
只是Stream做爲一種體積上能夠爲無窮大的值,在實踐中不可能去cache全部的值,把它總體當成一個值處理,因此這個能夠被無限提取的『值』屬性就消失了。
可是這不意味着stream做爲一個對象,它的行爲,不能延遲等到它被構造且使用後纔開始處理消息。
一種方式是寫一個stream有這種能力的;stream.Readable
有一個flow屬性,必須經過readable.resume
開始,這是一個觸發方式;另外一個方式是有點tricky,能夠截獲response.stream
的getter,在它第一次被訪問時觸發異步處理buffered message。
這樣的作法是不須要依賴Promise A+的實現的;但不是百分百asynchronous sequential composition,由於stream的handler確定是synchronous的。
徹底的asynchronous能夠參照Dart的使用await消費stream的方式。
它的邏輯能夠這樣理解:把全部Event,不管哪裏來的,包括error,都寫到一個流裏去,用await消費這個流;但實際上在await返回的時候仍然面對一個狀態機,好處是
總結:
Node的Callback和EventEmitter在組合時handler/listener是同步的;Promise則反過來保證每一個handler/listener都是異步組合,這是二者的根本區別。
在順序組合函數(或者進程代數意義上的進程)上,同步組合是緊耦合的;它體如今一旦功能上出現什麼緣由,須要把一個同步邏輯修改爲異步時,都要大動干戈,好比原本是讀取內存,後來變成了讀取文件。
若是程序天生寫成異步組合,相似變化就不會對實現邏輯產生很大影響;可是細粒度的異步組合有巨大的性能損失,這和現代處理器和編譯器的設計與實現有關。
真正理想的狀況應該是開發者只表達「順序」,並不表達它是同步仍是異步實現;就像前面看到的,實際上同步的實現都有能夠對應的異步實現,差異只是執行效率和內存使用(buffer有更多的內存開銷,同步處理實際上更可能是『閱後即焚』);
但咱們使用的imperative langugage不是如此,它在強制你表達順序;而另一類號稱將來其實狗屎的語言,在反過來強制你不得表達順序。
都是神經病。學術界就不會真正理解產業界的實際問題。