Promise、Generator,Async/await
我們知道JavaScript是單執行緒語言,如果沒有非同步程式設計非得卡死。
以前,非同步程式設計的方法有下面四種
- 回撥函式
- 事件監聽
- 釋出/訂閱
- Promise物件
現在據說非同步程式設計終極解決方案是——async/await
一、回撥函式
所謂回撥函式(callback),就是把任務分成兩步完成,第二步單獨寫在一個函式裡面,等到重新執行這個任務時,就直接呼叫這個函式。
例如Node.js中讀取檔案
fs.readFile('a,txt', (err,data) = >{
if(err) throw err;
console.log(data);
})
上面程式碼中readFile的第二個引數就是回撥函式,等到讀取完a.txt檔案時,這個函式才會執行。
二、Promise
使用回撥函式本身沒有問題,但有“回撥地獄”的問題。
假定我們有一個需求,讀取完A檔案之後讀取B檔案,再讀取C檔案,程式碼如下
fs.readFile(fileA, (err, data) => {
fs.readFile(fileB, (err, data) => {
fs.readFile(fileC, (err,data)=>{
//do something
})
});
});
可見,三個回撥函式程式碼看來就夠嗆了,有時在實際業務中還不止巢狀這幾個,難以管理。
這時候Promise出現了!它不是新的功能,而是一種新的寫法,用來解決“回撥地獄”的問題。
我們再假定一個業務,分多個步驟完成,每個步驟都是非同步的,而且依賴於上一個步驟的結果,用setTimeout()來模擬非同步操作
/**
* 傳入引數 n,表示這個函式執行的時間(毫秒)
* 執行的結果是 n + 200,這個值將用於下一步驟
*/
function A(n) {
return new Promise(resolve => {
setTimeout(() => resolve(n + 200), n);
});
}
function step1(n) {
console.log(`step1 with ${n}`);
return A(n);
}
function step2(n) {
console.log(`step2 with ${n}`);
return A(n);
}
function step3(n) {
console.log(`step3 with ${n}`);
return A(n);
}
上面程式碼中有4個函式,A()返回一個Promise物件,接收引數n,n秒後執行resolve(n+200)。step1、 step2、step3對應三個步驟
現在用Promise實現這三個步驟:
function doIt() {
console.time('do it now')
const time1 = 300;
step1(time1)
.then( time2 =>step2(time2))
.then( time3 => step3(time3))
.then( result => {
console.log(`result is ${result}`)
});
}
doIt();
輸出結果如下
step1 with 300
step2 with 500
step3 with 700
result is 900
result是step3()的引數700+200 = 900。
可見,Promise的寫法只是回撥函式的改進,用then()方法免去了巢狀,更為直觀。
但這樣寫絕不是最好的,程式碼變得十分冗餘,一堆的then。
所以,最優秀的解決方案是什麼呢?
開頭暴露了,就是async/await
講async前我們先講講協程與Generator
三、協程
協程(coroutine),意思是多個執行緒相互協作,完成非同步任務。
它的執行流程如下
- 協程A開始執行
- 協程A執行到一半,暫停執行,執行的權利轉交給協程B。
- 一段時間後B交還執行權
- 協程A重得執行權,繼續執行
上面的協程A就是一個非同步任務,因為在執行過程中執行權被B搶了,被迫分成兩步完成。
讀取檔案的協程程式碼如下:
function task() {
// 其他程式碼
var f = yield readFile('a.txt')
// 其他程式碼
}
task()函式就是一個協程,函式內部有個新單詞yield,yield中文意思為退讓,
顧名思義,它表示執行到此處,task協程該交出它的執行權了。也可以把yield命令理解為非同步兩個階段的分界線。
協程遇到yield命令就會暫停,把執行權交給其他協程,等到執行權返回繼續往後執行。最大的優點就是程式碼寫法和同步操作幾乎沒有差別,只是多了yield命令。
這也是非同步程式設計追求的,讓它更像同步程式設計
四、Generator函式
Generator是協程在ES6的實現,最大的特點就是可以交出函式的執行權,懂得退讓。
function* gen(x) {
var y = yield x +2;
return y;
}
var g = gen(1);
console.log( g.next()) // { value: 3, done: false }
console.log( g.next()) // { value: undefined, done: true }
上面程式碼中,函式多了*號,用來表示這是一個Generator函式,和普通函式不一樣,不同之處在於執行它不會返回結果,
返回的是指標物件g,這個指標g有個next方法,呼叫它會執行非同步任務的第一步。
物件中有兩個值,value和done,value 屬性是 yield 語句後面表示式的值,表示當前階段的值,done表示是否Generator函式是否執行完畢。
下面看看Generator函式如何執行一個真實的非同步任務
var fetch = require('node-fetch');
function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}
var g = gen();
var result = g.next();
result.value.then( data => return data.json)
.then (data => g.next(data))
上面程式碼中,首先執行Generator函式,得到物件g,呼叫next方法,此時
result ={ value: Promise { <pending> }, done: false }
因為fetch返回的是一個Promise物件,(即value是一個Promise物件)所以要用then才能呼叫下一個next方法。
雖然Generator將非同步操作表示得很簡潔,但是管理麻煩,何時執行第一階段,又何時執行第二階段?
是的,這時候到Async/await出現了!
五、Async/await
從回撥函式,到Promise物件,再到Generator函式,JavaScript非同步程式設計解決方案歷程可謂辛酸,終於到了Async/await。很多人認為它是非同步操作的最終解決方案(謝天謝地,這下不用再學新的解決方案了吧)
其實async函式就是Generator函式的語法糖,例如下面兩個程式碼:
var gen = function* (){
var f1 = yield readFile('./a.txt');
var f2 = yield readFile('./b.txt');
console.log(f1.toString());
console.log(f2.toString());
};
var asyncReadFile = async function (){
var f1 = await readFile('./a.txt');
var f2 = await readFile('./b.txt');
console.log(f1.toString());
console.log(f2.toString());
};
上面的為Generator函式讀取兩個檔案,下面為async/await讀取,比較可發現,兩個函式其實是一樣的,async不過是把Generator函式的*號換成async,yield換成await。
1.async函式用法
上面說了async不過是Generator函式的語法糖,那為什麼要取這個名字呢?自然是有理由的。
async是“非同步”,而await是async wait的簡寫,即非同步等待。所以應該很好理解async用於宣告一個function是非同步的,await用於等待一個非同步方法執行完成
下面來看一個例子理解async命令的作用
async function test() {
return "async 有什麼用?";
}
const result = test();
console.log(result)
輸出:
Promise { 'async 有什麼用?' }
可以看到,輸出的是一個Promise物件!
所以,async函式返回的是一個Promise物件,如果直接return 一個直接量,async會把這個直接量通過PromIse.resolve()封裝成Promise物件
注意點
一般來說,都認為await是在等待一個async函式完成,確切的說等待的是一個表示式,這個表示式的計算結果是Promise物件或者是其他值(沒有限定是什麼)
即await後面不僅可以接Promise,還可以接普通函式或者直接量。
同時,我們可以把async理解為一個運算子,用於組成表示式,表示式的結果取決於它等到的東西
- 等到非Promise物件 表示式結果為它等到的東西
- 等到Promise物件 await就會阻塞後面的程式碼,等待Promise物件resolve,取得resolve的值,作為表示式的結果
還是那個業務,分多個步驟完成,每個步驟依賴於上一個步驟的結果,用setTimeout模擬非同步操作。
/**
* 傳入引數 n,表示這個函式執行的時間(毫秒)
* 執行的結果是 n + 200,這個值將用於下一步驟
*/
function takeLongTime(n) {
return new Promise(resolve => {
setTimeout(() => resolve(n + 200), n);
});
}
function step1(n) {
console.log(`step1 with ${n}`);
return takeLongTime(n);
}
function step2(n) {
console.log(`step2 with ${n}`);
return takeLongTime(n);
}
function step3(n) {
console.log(`step3 with ${n}`);
return takeLongTime(n);
}
async實現方法
async function doIt() {
console.time("doIt");
const time1 = 300;
const time2 = await step1(time1);
const time3 = await step2(time2);
const result = await step3(time3);
console.log(`result is ${result}`);
console.timeEnd("doIt");
}
doIt();
輸出結果和上面用Promise實現是一樣的,但這個程式碼結構看起來清晰得多,幾乎跟同步寫法一樣。
2. async函式的優點
(1)內建執行器
Generator 函式的執行必須靠執行器,所以才有了 co 函式庫,而 async 函式自帶執行器。也就是說,async 函式的執行,與普通函式一模一樣,只要一行。
(2) 語義化更好
async 和 await,比起星號和 yield,語義更清楚了。async 是“非同步”的簡寫,而 await 可以認為是 async wait 的簡寫。所以應該很好理解 async 用於申明一個 function 是非同步的,而 await 用於等待一個非同步方法執行完成。
(3)更廣的適用性
yield 命令後面只能是 Thunk 函式或 Promise 物件,而 async 函式的 await 命令後面,可以跟 Promise 物件和原始型別的值(數值、字串和布林值,但這時等同於同步操作)。