點選檢視精彩視訊
來自FIBJS核心貢獻者陳壘在 D2 的演講 “FIBJS子產品重構—從回調到協程”。
JS的應用非常廣泛,例如做一些浏覽器的發展、機器學習、控制機器人以及編寫嵌入式的應用。

如上圖所示為使用浏覽器原生的對象發送一個請求的典型例子。首先建立了一個xhr對象,接下來的一步并不是馬上發送它,而是要設一個回調函數onreadystatechange,這個函數表示請求完成以後怎樣處理傳回結果。xhr也定義了本次請求要發送的HTTP動詞和發送的目标網址,直到最後一步才真正将請求發送出去。但是,這個代碼是異步的,當send運作之後,代碼繼續執行,很可能代碼沒有完成,等到請求完成後才可以處理。像xhr的請求,它的異步過程的執行順序和聲明順序不一緻,先定義回調再發送請求,這種情況稱之為異步。是以,要求執行順序和聲明順序嚴格一緻。現代的應用不是單純的同步或者異步,往往是兩者的混合,時而同步時而異步,是以如何按需處理同步和異步的流程一直是一個熱門問題。
也可以使用Promise把異步流程轉換到同步,如上圖所示。浏覽器首先提供了一個fetch方法,發起一個fetch請求,通過then方法将回調處理,推到一個隊列裡。如果在回調裡請求是正确的,傳回了HTTP,那麼會運作then,如果HTTP請求出錯,則會運作catch。但是發完請求之後還是會繼續向下執行,對所有的傳回結果的處理還是要到then裡執行,而上圖中的代碼是沒辦法依賴then裡的傳回結果的,因為不知道什麼時候才會執行完成,是以會出現異步的傳染性問題,一旦使用回調或promise的方式,能處理異步的方法就變得局限了,隻能在then或catch裡處理回調的過程。
那麼使用async/await + Promise來控制異步會産生什麼效果,如上圖所示。首先是一個request請求,然後在函數上标記一個async方法,它依然會發起一個fetch請求,與上述唯一不同的是在fetch前加了一個await,這樣,在fetch裡無論是成功還是失敗,總可以得到一個值,這個值會存到result裡,當result有值時才會執行。這種方式即處理了異步又可以阻塞上面的流程。但還是會出現異步的傳染性問題,await不能用在JS腳本的全局中。
對此,我們可以假想一種新的鎖原語,鎖對象會阻塞JS的執行,如上圖所示。首先設計一個鎖,在回調之前先聲明一個鎖對象,當請求發送出去之後,将鎖調用wait方法,作用是等信号量釋放,當鎖收到不阻塞JS的消息後,繼續執行。
這樣的方法在 NodeJS 和浏覽器中都無法執行,但這種風格,确是 FIBJS 推薦的異步控制風格。
FIBJS ORM 重構
ORM表示對象關系模組化,指資料庫之間對象關系的操作。FIBJS自己的包叫FIBJSORM,是Node ORM2包遷移過來的。Node.js引入子產品的方式是require,而FIBJS有一個機制,當require一個腳本時,可以指定這個腳本上下文裡要做子產品解析的别名,是以應用這個機制,讓Node ORM2做一個JS上的子產品,不需要改代碼,隻需要替換内置子產品,就可以使ORM在FIBJS上運作。但是,存在一個問題,Node ORM2是使用回調程式設計風格來書寫的,是以會消耗性能。
FIBJS 的同步程式設計風格
如上圖所示為FIBJS中的一段代碼,其中coroutine表示協程。首先建立了一把鎖lock1,然後執行doSthAsync1表示異步的過程,在第9行到第12行,建立了第二把鎖,作為第二個異步的事情,在第18行和第19行時,lock1釋放的時候再繼續向下執行。其中在第5行定義了doSthAsync1回調,在這個回調裡鎖得到了釋放,在第18行收到信号後再繼續往下執行。同樣,鎖2也要等到lock2釋放後再繼續往下執行。通過這種鎖機制的方式,同時盡快的發起了兩個異步任務doSthAsync1和doSthAsync2,可以非常确定的知道在18行和19行結束的時候的兩個異步任務是完成的,不需要去回調裡單獨處理結果,也不需要寫promise、then、async、await。
當任務過多的時候,可以建立上千個鎖,并且在FIBJS中建立鎖的代價是很低的,但是代碼很繁瑣,是以,可以使用上圖中的co.parallel函數,它可以建立一個序列化的隊列,同時又是并發執行的,并且每個函數的傳回結果會傳回到一個數組裡,再從數組裡把函數取出來。上述兩種方式就是FIBJS的同步程式設計風格。
NodeJS 與 FIBJS 程式設計風格的比較
NodeJS: 回調即異步?
上圖所示為NodeJS OMR2處理資料庫請求的一個代碼。其中connection是一個資料庫連接配接,query執行一條sql,接着在回調裡處理下面的邏輯,包括第一個參數err,err不是空的,則将err抛出,後面的字段rows和fields是請求處理得到的結果。
FIBJS: 同步更清爽
首先用rows将代碼全部拿出,接着處理相關的邏輯。看似FIBJS的程式設計風格更同步、更清爽,少了NodeJS中大量的回調過程,同時,回調不等于異步。
如上圖所示為Node處理檔案寫入的一個官方處理方式,當在targetFile中寫入内容時,判斷執行過程是成功還是失敗,可以通過在回調裡處理。這樣的方式沒問題,如果要寫十個if write,如果第一個write的結果給第二個write作為一個依賴,要看第一個檔案寫成功再寫第二個,那麼就會發生下圖所示的情況。這樣做是為了提高性能,但是當回調的嵌套層級過多時,性能看似就沒有那麼好了。
FIBJS ORM 重構前後對比
在重構前,流程控制方式為子產品大量使用回調來控制流程,如嵌套層級很深的代碼。而重構之後,子產品各處采用直接調用傳回結果的方式。測試結果在寫入十萬行資料的時候,FIBJS在重構前比重構後要慢一些。重構前寫入100萬行時耗時太久,處理平均TPS有4694個,而重構之後可以處理5017個。通過FIBJSORM重構前後的對比,性能優化不是很大,但是可以說明的一點是,不使用回調要比使用回調性能要好。
概念對比
上圖是兩種程式設計風格的概念對比,NodeJS的JS線程是一個單線程,FIBJS也是一個單線程。當單線程處理多任務時,NodeJS的選擇是事件循環或者多路複用JS主線程,但不建議在JS線程中寫繁重的任務。兩者都有callback線程,NodeJS異步邏輯控制推薦使用callback或Promise,FIBJS也支援這種方式,但是不建議這樣做,建議使用輕量邏輯鎖。在處理并發的時候,NodeJS也會使用JSTimer或者Promise.all,但是不推薦FIBJS使用這兩種線程,而推薦使用coroutine.parallel處理并發任務,因為它的執行效率更高。
NodeJS中有兩種線程,一個是JS的主線程,另一個是工作線程。JS的主線程的全局隻有一個,NodeJS的JS線程是一個單線程,同時有多個線程在背景為它工作。但是,通常工作線程不使用,将所有的任務都放到主線程中,這種做法是一種浪費工作線程的表現。
FIBJS也有三種類型的線程,FIBJS也是一個單線程。
把密集計算托管到 Worker 線程
在FIBJS中,把密集計算托管到Worker線程,運作于6C32G的Mac OSX Catalina 15,優化之前,直接将UUID子產品放到主線程執行,寫入10萬行資料的時間比優化後要慢2s左右,寫入100萬行的資料時,時間差已經擴大到了半分鐘,寫入1000萬行時,時間差已經擴大到分鐘,TPS在優化後的提升不是很明顯。
總結FIBJS ORM子產品重構中運作的兩種手段,一是用回調表達異步流程改成直接調用執行。另一種是CPU密集型高頻計算,如UUID等,托管到工作線程。以上經驗可以完全應用到 NodeJS。
上圖為兩種程式設計風格的對比,NodeJS使用node-sqlite3(單線程)來寫入資料,FIBJS使用sqlite子產品(單線程)來寫入資料。對于流程控制方式,NodeJS使用回調的方式,FIBJS使用直接調用傳回結果的方式,其它方面都保持一緻。當寫入10萬行資料時,FIBJS隻領先了1s左右,寫入100萬行時,領先了20s左右,寫入100萬行時,NodeJS的記憶體直接炸了,平均TPS如上圖中資料所示。
FIBJS 的并發性能名額
當随着并發數上升時,從10萬到100萬時,每個應用的記憶體占用的趨勢如上圖所示,nginx非常平滑,fibjs和nodejs兩者的走勢是相識的,但總體來講,fibjs占用記憶體相對比較少。
上圖是FIBJS 0.26與早期FIBJS的對比。其中last表示0.26的版本,newfiber表示0.27版本。FIBJS在任務排程上力求充分利用系統的線程。
上圖所示,當在Google搜尋nodejscallback hell時,有8萬多個詞條,說明NodeJS裡的回調風格僅僅在表達上給很多人造成了困擾,相對FIBJS的同步執行可能不是最終或者更好的答案,至于使用哪種風格表達異步流程,是見人見智的問題。但是,FIBJS在NodeJS之外提供了另外一種異步程式設計的風格,回調帶來的問題在FIBJS不會發生,同時,通過上文資料可以看出FIBJS表現的更好,記憶體管理也更好。
NodeJS 生态現狀
Npm社群大部分的包都是給NodeJS服務的,包的總數量如上圖所示,NodeJS有專門的委員會、完整的開源流程,并且API穩定,同時也是前端不可或缺的工具,與 php/.Net/cocoapods等生态關聯極多,被世界上大多數公司采用。使用NodeJS可以做出高性能的案例,但是過程是相當痛苦的,世界上已經有非常成功的應用案例。
FIBJS 生态現狀
FIBJS相容NodeJS包管理機制,通過SandBox可使用大部分npm包,目前獨屬于FIBJS的包還比較少,主要用于遊戲伺服器、雲平台後端、區塊鍊開發、嵌入式開發,但是有個很大的問題是FIBJS的國際化不足,尚無知名國際應用案例。
關注「Alibaba F2E」
把握阿裡巴巴前端新動向