JS異步編程 (2) - Promise、Generator、async/await
上篇文章我們講了下JS異步編程的相關知識,比如什麽是異步,為什麽要使用異步編程以及在瀏覽器中JS如何實現異步的。
最後我們捎帶講了幾種JS異步編程模式(回調,事件和發布/訂閱模式),這篇我們繼續去深入了解下其他的幾種異步編程模式。
其實這幾個函數用來解決,異步中 回調函數嵌套問題 (callback hell) 回調地獄
Promise
Promise是ES6推出的一種異步編程的解決方案。其實在ES6之前,很多異步的工具庫就已經實現了各種類似的解決方案,而ES6將其寫進了語言標準,統一了用法。Promise解決了回調等解決方案嵌套的問題並且使代碼更加易讀,有種在寫同步方法的既視感。
我們先來簡單了解下ES6中Promise的用法
var p = new Promise(function async(resolve, reject){ // 這裏是你的異步操作 setTimeout(function(){ if(true){ resolve(val); }else{ reject(error); } }, 1000) }) p.then(function(val){ console.log(‘resolve‘); },function(){ console.log(‘reject‘); })
首先,ES6規定Promise是個構造函數,其接受一個函數作為參數如上面代碼中的async
函數,此函數有兩個參數,resolve、reject分別對應成功失敗兩種狀態,我們可以選擇在不同時候執行resolve或者reject去觸發下一個動作,執行then方法裏的函數。
我們可以簡單對比下回調的寫法和promise的寫法的不同
對於傳統回調寫法來說,一般會寫成這樣
asyncFn1(function () { asyncFn2(function() { asyncFn3(function() { // xxxxx }); }); });
或者我們將各個回調函數拆出來獨立來寫以減少耦合,像是這樣:
function asyncFn1(callback) { return function() { console.log(‘asyncFn1 run‘); setTimeout(function(){ callback(); }, 1000); } } function asyncFn2(callback) { return function(){ console.log(‘asyncFn2 run‘); setTimeout(function(){ callback(); }, 1000); } } function normalFn3() { console.log(‘normalFn3 run‘); } asyncFn1(asyncFn2(normalFn3))()
最後我們看下Promise的寫法
function asyncFn1() { console.log(‘asyncFn1 run‘); return new Promise(function(resolve, reject) { setTimeout(function(){ resolve(); }, 1000) }) } function asyncFn2() { console.log(‘asyncFn2 run‘); return new Promise(function(resolve, reject) { setTimeout(function(){ resolve(); }, 1000) }) } function normalFn3() { console.log(‘normalFn3 run‘); } asyncFn1().then(asyncFn2).then(normalFn3);
這樣來看無論是第一種還是第二種寫法,都會讓人感到不是很直觀,而Promise的寫法更加直觀和語義化。
Generator
Generator函數也是ES6提供的一種特殊的函數,其語法行為與傳統函數完全不同。
我們先直觀看個Generator實際的用法
function* oneGenerator() { yield ‘Learn‘; yield ‘In‘; return ‘Pro‘; } var g = oneGenerator(); g.next(); // {value: "Learn", done: false} g.next(); // {value: "In", done: false} g.next(); // {value: "Pro", done: true}
Generator函數是一種特殊的函數,他有這麽幾個特點:
-
聲明時需要在
function
後面加上*
,並且配合函數裏面yield
關鍵字來使用。 -
在執行Generator函數的時候,其會返回一個Iterator遍歷器對象,通過其next方法,將Generator函數體內的代碼以yield為界分步執行
-
具體來說當執行Generator函數時,函數並不會執行,而是需要調用Iterator遍歷器對象的next方法,這時程序才會執行
從頭或者上一個yield之後
到到下一個yield或者return或者函數體尾部
之間的代碼,並且將yield後面的值,包裝成json對象返回。就像上面的例子中的{value: xxx, done: xxx}
。 -
value取的yield或者return後面的值,否則就是undefined,done的值如果碰到return或者執行完成則返回true,否則返回false。
我們知道了簡單的Generator函數的用法以後,我們來看下如何使用Generator函數進行異步編程。
首先我們先來看下使用Generator函數能達到怎樣的效果。
// 使用Generator函數進行異步編程 function* oneGenerator() { yield asyncFn1(); yield asyncFn2(); yield normalFn3(); } // 我們來對比一下Promise asyncFn1().then(asyncFn2).then(normalFn3);
我們可以看出使用Generator函數進行異步編程更像是在寫同步任務,對比Promise少了很多次then方法的調用。
好,那麽接下來我們就來看下如何實際使用Generator函數進行異步編程。
這裏我要特別說明一下,事實上Generator函數不像Promise一樣是專門用來解決異步處理而產生的,人們只是使用其特性來產出了一套異步的解決方案,所以使用Generator並不像使用Promise一樣有一種開箱即用的感覺。其更像是在Promise或者回調這類的解決方案之上又封裝了一層,讓你可以像上面例子裏一樣去那麽寫。
我們還是具體來看下上面的例子,我們知道單寫一個Generator是不能運行的對吧,我們需要執行他並且使用next方法來讓他分步執行,那麽什麽時候去調用next呢?答案就是我們需要在異步完成時去調用next。我們來按照這個思路補全上面的例子。
var g; function asyncFn() { setTimeout(function(){ g.next(); }, 1000) } function normalFn() { console.log(‘normalFn run‘); } function* oneGenerator() { yield asyncFn(); return normalFn(); } g = oneGenerator(); g.next(); // 這裏在我調用next方法的時候執行了asyncFn函數 // 然後我們的希望是在異步完成時自動去再調用g.next()來進行下面的操作,所以我們必須在上面asyncFn函數體內的寫上g.next(); 這樣才能正常運行。 // 但其實這樣是比較奇怪的,因為當我定義asyncFn的時候其實是不知道oneGenerator執行後叫什麽名兒的,即使我們提前約定叫g,但這樣asyncFn就太過於耦合了,不僅寫法很奇怪而且耦合太大不利於擴展和重用。反正總而言之這種寫法很不好。
那麽怎麽解決呢,我們需要自己寫個方法,能自動運行Generator函數,這種方法很簡單在社區裏有很多,最著名的就是大神TJ寫的co模塊,有興趣的同學可以看下其源碼實現。這裏我們簡單造個輪子:
// 如果我們想要去在異步執行完成時自動調用next就需要有一個鉤子,回調函數的callback或者Promise的then。 function autoGenerator(generator){ var g = generator(); function next(){ var res = g.next(); // {value: xxx, done: xxx} if (res.done) { return res.value; } if(typeof res.value === ‘function‘){ // 認為是回調 res.value(next); }else if(typeof res.value === ‘object‘ && typeof res.value.then === ‘function‘){ // 認為是promise res.value.then(function(){ next(); }) }else{ next(); } } next(); } // ---- function asyncFn1(){ console.log(‘asyncFn1‘); return new Promise(function(resolve){ setTimeout(function(){ resolve(); }, 1000) }) } function asyncFn2() { console.log(‘asyncFn2‘); return function(callback){ setTimeout(function(){ callback(); }, 1000); } } function normalFn() { console.log(‘normalFn‘); } function* oneGenerator() { yield asyncFn1(); yield asyncFn2(); yield normalFn(); } autoGenerator(oneGenerator);
這個方法我們簡單實現了最核心的部分,有些判斷可能並不嚴謹,但大家理解這個思路就可以了。有了這個方法,我們才可以方便的使用Generator函數進行異步編程。
Async/Await
如果你學會了Generator函數,對於Async函數就會很容易上手。你可以簡單把Async函數理解成就是Generator函數+執行器。我們就直接上實例好了
function asyncFn1(){ console.log(‘asyncFn1‘); return new Promise(function(resolve){ setTimeout(function(){ resolve(‘123‘); }, 2000) }) } function asyncFn2() { console.log(‘asyncFn2‘); return new Promise(function(resolve){ setTimeout(function(){ resolve(‘456‘); }, 2000) }) } async function asyncFn () { var a = await asyncFn1(); var b = await asyncFn2(); console.log(a,b) } asyncFn(); // asyncFn1 // asyncFn2 // 123,456
當然async裏實現的執行器肯定是跟咱們上面簡單實現的有所不同,所以在用法上也會有些註意的點
-
首先async函數的返回值是一個Promise對象,不像是generator函數返回的是Iterator遍歷器對象,所以async函數執行後可以繼續使用then等方法來繼續進行下面的邏輯
-
await後面一般跟Promise對象,async函數執行時,遇到await後,等待後面的Promise對象的狀態從pending變成resolve的後,將resolve的參數返回並自動往下執行直到下一個await或者結束
-
await後面也可以跟一個async函數進行嵌套使用。
對於異步來說,還有很多的知識點我們沒有講到,比如異常處理,多異步並行執行等等,這篇和上篇文章主要還是希望大家對異步編程有個直觀的了解,清楚各種解決方案之間的區別和優劣。
轉 : https://www.cnblogs.com/learninpro/p/9271813.html
JS異步編程 (2) - Promise、Generator、async/await