async函式學習筆記。

Lessong發表於2019-02-16

含義

async函式是什麼?一句話,它就是Generator函式的語法糖。

const fs = require(`fs`)
const readFile = function(fileName){
    return new Promise(function(resolve,reject){
        fs.readFile(fileName,function(error,data){
            if(error) return reject(error);
            resolve(data);
        })
    })
}
const gen = function*(){
    const f1 = yield readFile(`/etc/fstab`);
    const f2 = yield readFile(`/etc/shells`);
    console.log(f1.toString());
    console.log(f2.toString());
}
//上面程式碼的函式gen可以寫成async函式,就是下面這樣。
const asyncReadFile = async function(){
    const f1 = await readFile(`/etc/fstab`);
    cosnt f2 = await readFile(`/etc/shells`);
    console.log(f1.toString());
    console.log(f2.toString());
}

一比較就會發現,async函式就是將Generator函式的星號替換成async,將yield替換成await,僅此而已。
async函式對Generator函式的改進,體現在以下四點。
(1)內建執行器
Generator函式的執行必須靠執行器,所以才有了co模組,而async函式自帶執行器。也就是說,async函式的執行,與普通函式一模一樣,只要一行。

asyncReadFile()

上面的程式碼呼叫了asyncReadFile函式,然後它就會自動執行,輸出最後結果。這完全不像Generator函式,需要呼叫next方法,或則用co模組,才能真正執行,得到最後結果。
(2)更好的語義
async和await,比起星號和yield,語義更清楚了。async表示函式裡有非同步操作,await表示緊跟在後面的表示式需要等待結果。
(3)更廣的適用性
co模組約定,yield命令後面只能是Thunk函式或Promise物件,而async函式的await命令後面,可以是Promise物件和原始型別的值(數值、字串和布林值,但這時等同於同步操作)。
(4)返回值是Promise
async函式的返回值是Promise物件,這比Generator函式的返回值Iterator物件方便多了。你可以用then方法指定下一步的操作。
進一步說,async函式完全可以看作多個非同步操作,包裝成的一個Promise物件,而await命令就是內部then命令的語法糖。

基本用法

async函式返回一個Promise物件,可以使用then方法新增回撥函式。當函式執行的時候,一旦遇到await就會先返回,等到非同步操作完成,再接著執行函式體內後面的語句。
下面是一個例子。

async function getStockPriceByName(name){
    const symbol = await getStockSymbol(name);
    const stockPrice = await getStockPrice(symbol);
    return stockPrice;
}
getStockPriceByName(`goog`).then(function(result){
    console.log(result);
})

上面程式碼是一個獲取股票報價的函式,函式前面的async關鍵字,表明該函式內部有非同步操作。呼叫該函式時,會立即返回一個Promise物件。
下面是另一個例子,指定多少毫秒後輸出一個值。

function timeout(ms){
    return new Promise((resolve)=>{
        setTimeout(resolve,ms);
    })
}
async function asyncPrint(value,ms){
    await timeout(ms);
    console.log(value)
}
asyncPrint(`hello world`,50);

由於async函式返回的是Promise物件,可以作為await命令的引數。所以,上面的例子也可以寫成下面的形式。

async function timeout(ms){
    await new Promise((resolve)=>{
        setTimeout(resolve,ms);
    })
}

async function asyncPrint(value, ms) {
    await timeout(ms);
    console.log(value);
}

asyncPrint(`hello world`, 5000);

async函式有多種使用形式。

//函式宣告
async function foo(){}
//函式表示式
const foo = async function(){};
//物件的方法
let obj = {async foo(){}};
obj.foo().then(...)
//Class的方法
class Storage{
    constructor(){
        this.cachePromise = caches.open(`avatars`);
    }
    async getAvatar(name){
        const cache = await this.cachePromise;
        return cache.match(`/avatars/${name}.jpg`);
    }
}
const storage = new Storage();
storage.getAvatar(`jake`).then(...);
//箭頭函式
const foo = async()=>{};

返回Promise物件

async函式返回一個Promise物件。
async函式內部return語句返回的值,會成為then方法回撥函式的引數。

async function f(){
    return `hello world`;
}
f().then(v=>console.log(v))//`hello world`

上面程式碼中,函式f內部return命令返回的值,會被then方法回撥函式接收到。
async函式內部丟擲錯誤,會導致返回Promise物件變為reject狀態。丟擲的錯誤物件會被catch方法回撥函式接收到。

async function f(){
    throw new Error(`出錯了`);
}
f().then(
    v=>console.log(v),
    e=>console.log(e)
)
//Error;出錯了

Promise物件的狀態變化

async函式返回的Promise物件,必須等到內部所有await命令後面的Promise物件執行完,才會發生狀態改變,除非遇到return語句或則丟擲錯誤。也就是說,只有async函式內部的非同步操作執行完,才會執行then方法指定的回撥函式。

async function getTitle(url) {
  let response = await fetch(url);
  let html = await response.text();
  return html.match(/<title>([sS]+)</title>/i)[1];
}
getTitle(`https://tc39.github.io/ecma262/`).then(console.log)

上面程式碼中,函式getTitle內部有三個操作:抓取網頁、取出文字、匹配頁面標題。只有這三個操作全部完成,才會執行then方法裡面的console.log()
await命令
正常情況下,await命令後面是一個Promise物件,返回該物件的結果。如果不是Promsie物件,就直接返回對應的值。

async function f(){
    //等同於123;
    return await 123;
}
f().then(v=>console.log(v))

上面程式碼中,await命令的引數是數值123,這時等同於return 123.
另一種情況是,await命令後面是一個thenable物件(即定義then方法的物件),那麼await會將其等同於Promise物件。

class Sleep{
    constructor(timeout){
        this.timeout = timeout;
    }
    then(resolve,reject){
        const startTime = Date.now();
        setTimeout(
            ()=>resolve(Date.now()-startTime)
            this.timeout
        )
    }
}
(async()=>{
    const actualTime = await new Sleep(1000);
    console.log(actualTime);
})();

上面程式碼中,await命令後面是一個Sleep物件的例項。這個例項不是Promise物件,但是因為定義了then方法,await會將其視為Promise處理。
await命令後面的Promise物件如果變為reject狀態,則reject的引數會被catch方法的回撥函式接收到。

async function f(){
    await Promise.reject(`出錯了`);
}
f().then(v=>console.log(v)).catch(e=>console.log(e));

注意,上面程式碼中,await語句前面沒有return,但是reject方法的引數依然傳入到catch方法的回撥函式。這裡如果是await前面加上return,效果是一樣的。
任何一個await語句後面的Promise物件變為reject狀態,那麼整個async函式都會中斷執行。
有時,我們希望即使前一個非同步操作失敗,也不要中斷後面的非同步操作。這時可以將第一個await放在try…catch結構裡面,這樣不管這個非同步操作是否成功,第二個await都會執行。

async function f(){
    try{
        await Promise.reject(`出錯了`);
    }catch(e){
    }
    return await Promise.resolve(`hello world`);
}
f().then(v=>console.log(v))//hello world

另一種方法是await後面的Promise物件再跟一個catch方法,處理前面可能出現的錯誤。

async function f(){
    await Promise.reject(`出錯了`)
        .catch(e=>console.log(e));
    return await Promise.resolve(`hello world`);
}
f().then(v=>console.log(v))
//出錯了
//hello world

錯誤處理
如果await後面的非同步操作出錯,那麼等同於async函式返回的Promise物件被reject。防止出錯的方法,也是將其放在try…catch程式碼塊之中。

async function f(){
    try{
        await new Promise(function(resolve,reject){
            throw new Error(`出錯了`)
        });
    }catch(e){}
    return await(`hello world`);
}

如果有多個await命令,可以統一放在try…catch結構中。

async function main(){
    try{
        const val1 = await firstStep();
        const val2 = await secondStep(val1);
        const val3 = await thirdStep(val1,val2);
        console.log(`Final`,val3);
    }
    catch(err){
        console.error(err);
    }
}

下面的例子使用try…catch結構,實現多次重複嘗試。

const superagent = require(`superagent`);
const NUM_RETRIES = 3;
async function test(){
    let i;
    for(i=0;i<NUM_RETRIES;i++){
        await superagent.get(`http://goole.com/this-throws-an-error`);
        break;
    }catch(err){}
    console.log(i);//3
}
test();

上面程式碼中,如果await操作成功,就會使用break語句退出迴圈;如果失敗,會被catch語句捕捉,然後進入下一輪迴圈。

相關文章