天天看點

async函數學習筆記

含義

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>([\s\S]+)<\/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語句捕捉,然後進入下一輪循環。

來源:

https://segmentfault.com/a/1190000017469670