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的公眾號,在上面隨時向我提問哦。👋
