一篇文章瞭解前端非同步程式設計方案演變
對於JS而言,非同步程式設計我們可以採用回撥函式,事件監聽,釋出訂閱等方案,在ES6之後,又新添了Promise,Genertor,Async/Await的方案。本文將闡述從回撥函式到Async/Await的演變歷史,以及它們之間的關係。
1. 非同步程式設計的演變
首先假設要渲染一個頁面,只能非同步的序列請求A,B,C,然後才能拿到頁面的資料並請求頁面
針對於不同的非同步程式設計方式,我們會得到如下的程式碼:
1.1 回撥函式
// 假設request是一個非同步函式 request(A, function () { request(B, function () { request(C, function () { // 渲染頁面 }) }) })
回撥函式的巢狀是愈發深入的。在不斷的回撥中,request(A)回撥函式中的其他邏輯會影響到request(B),request(C)中的邏輯,同理,request(B)中的其他邏輯也會影響到request(C)。在這個例子中,request(A)呼叫request(B),request(B)呼叫request(C),request(C)執行完畢返回,request(B)執行完畢返回,request(A)執行完畢返回。我們很快會對先後順序產生混亂,從而很難直觀的分析出非同步回撥的結果。這就被稱為回撥地獄。
為了解決這種情況,ES6新增了Promise物件。
1.2 Promise
// 假設request是一個Promise函式 request(A).then(function () { return request(B) }).then(function () { return request(C) }).then(function () { // 渲染頁面 })
Promise物件用then函式來指定回撥。所以,之前在1.1中回撥函式的例子可以改為上文中的模樣。可以看到,Promise並沒有消除回撥地獄,但是卻通過then鏈將程式碼邏輯變得更加清晰了。在這個例子中,request(A)呼叫request(B),request(B)呼叫request(C),request(C)執行完畢返回。現在,request(A)中的內容只能通過顯示宣告的data來影響到request(C)——如果沒有顯示的在回撥中宣告,則影響不了request(C),換言之,每段回撥被近乎獨立的分割了。
但是Promise本身還是有一堆的then,還是不能讓我們像寫同步程式碼一樣寫非同步的程式碼,因此JS又引入了Generator。
1.3 Generator
function* gen(){ var r1 = yield request(A) var r2 = yield request(B) var r3 = yield request(C) // 渲染頁面 };
Generator是協程在ES6上的實現,協程是指一個執行緒上不同函式間執行權可以相互切換。如本例,先執行gen(),然後在遇到yield時暫停,執行權交給request(A),等到呼叫了next()方法,再將執行權還給gen()。
通過協程,JS就實現了用同步的方式寫非同步的程式碼,但是Generator的使用要配合執行器,這自然是麻煩的。於是就有了Async/Await。
Generator的自動執行器是co函式庫,有興趣的同學可以通過閱讀《co 函式庫的含義和用法 》來進行了解。
1.4 Async/Await
async function gen() { var r1 = await request(A) var r2 = await request(B) var r3 = await request(C) // 渲染頁面 }
如果比較程式碼的話,1.4的程式碼只是把1.3的程式碼中* => async,yield變為await。但Async函式的實現,就是將 Generator函式和自動執行器,包裝在一個函式裡[1] 。spawn就是自動執行器。
async function fn(args){ // ... } // 等同於 function fn(args){ return spawn(function*() { // ... }); }
除此以外,Async函式比Generator函式有更好的延展性——yield接的是Promise函式/Thunk函式,但await還可以包括普通函式。對於普通函式,await表示式的運算結果就是它等到的東西。否則若await等到的是一個Promise函式,await就會協程到這個Promise函式上,直到它resolve或者reject,然後再協程回主函式上[2]。當然,Async函式也比Generator函式更加易讀和易理解。
2. 總結
本文闡述了從回撥函式到Async/Await的演變歷史。Async函式作為換一個終極解決方案,儘管在並行非同步處理上還要藉助Promise.all(),但其他方面已經足夠完美。