JS非同步程式設計 (2) – Promise、Generator、async/await
上篇文章我們講了下JS非同步程式設計的相關知識,比如什麼是非同步,為什麼要使用非同步程式設計以及在瀏覽器中JS如何實現非同步的。
最後我們捎帶講了幾種JS非同步程式設計模式(回撥,事件和釋出/訂閱模式),這篇我們繼續去深入瞭解下其他的幾種非同步程式設計模式。
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函式進行巢狀使用。
對於非同步來說,還有很多的知識點我們沒有講到,比如異常處理,多非同步並行執行等等,這篇和上篇文章主要還是希望大家對非同步程式設計有個直觀的瞭解,清楚各種解決方案之間的區別和優劣。由於篇幅和精力有限,對於其他我們沒講到的知識點,如果大家有興趣有機會我會再寫文章深入講解的。
另外就是如果你在學習前端的過程中有任何問題想要諮詢,歡迎關注
LearnInPro的公眾號
,在上面隨時向我提問哦。?