天天看點

ES8新特性:async/await

在ES8中加入了對async/await的支援,也就我們所說的異步函數,這是一個很實用的功能。 async/await将我們從頭痛的回調地獄中解脫出來了,使整個代碼看起來很簡潔。

async 起什麼作用

這個問題的關鍵在于,async 函數是怎麼處理它的傳回值的!

我們當然希望它能直接通過 return 語句傳回我們想要的值,但是如果真是這樣,似乎就沒 await 什麼事了。是以,寫段代碼來試試,看它到底會傳回什麼:

async function testAsync() {
    return "hello async";
}
const result = testAsync();
console.log(result);
           

看到輸出就恍然大悟了——輸出的是一個 Promise 對象。

c:\var\test> node --harmony_async_await .
Promise { 'hello async' }
           

是以,async 函數傳回的是一個 Promise 對象。從文檔中也可以得到這個資訊。async 函數(包含函數語句、函數表達式、Lambda表達式)會傳回一個 Promise 對象,如果在函數中 return 一個直接量,async 會把這個直接量通過 Promise.resolve() 封裝成 Promise 對象。

async 函數傳回的是一個 Promise 對象,是以在最外層不能用 await 擷取其傳回值的情況下,我們當然應該用原來的方式:then() 鍊來處理這個 Promise 對象,就像這樣

testAsync().then(v => {
    console.log(v);    // 輸出 hello async
});
           

現在回過頭來想下,如果 async 函數沒有傳回值,又該如何?很容易想到,它會傳回 Promise.resolve(undefined)。

聯想一下 Promise 的特點——無等待,是以在沒有 await 的情況下執行 async 函數,它會立即執行,傳回一個 Promise 對象,并且,絕不會阻塞後面的語句。這和普通傳回 Promise 對象的函數并無二緻。

那麼下一個關鍵點就在于 await 關鍵字了。

await 到底在等啥

一般來說,都認為 await 是在等待一個 async 函數完成。不過按文法說明,await 等待的是一個表達式,這個表達式的計算結果是 Promise 對象或者其它值(換句話說,就是沒有特殊限定)。

因為 async 函數傳回一個 Promise 對象,是以 await 可以用于等待一個 async 函數的傳回值——這也可以說是 await 在等 async 函數,但要清楚,它等的實際是一個傳回值。注意到 await 不僅僅用于等 Promise 對象,它可以等任意表達式的結果,是以,await 後面實際是可以接普通函數調用或者直接量的。是以下面這個示例完全可以正确運作

function getSomething() {
    return "something";
}

async function testAsync() {
    return Promise.resolve("hello async");
}

async function test() {
    const v1 = await getSomething();
    const v2 = await testAsync();
    console.log(v1, v2);
}

test();
           

await 等到了要等的,然後呢

await 等到了它要等的東西,一個 Promise 對象,或者其它值,然後呢?我不得不先說,await 是個運算符,用于組成表達式,await 表達式的運算結果取決于它等的東西。

如果它等到的不是一個 Promise 對象,那 await 表達式的運算結果就是它等到的東西。

如果它等到的是一個 Promise 對象,await 就忙起來了,它會阻塞後面的代碼,等着 Promise 對象 resolve,然後得到 resolve 的值,作為 await 表達式的運算結果。

看到上面的阻塞一詞,心慌了吧……放心,這就是 await 必須用在 async 函數中的原因。async 函數調用不會造成阻塞,它内部所有的阻塞都被封裝在一個 Promise 對象中異步執行。

async/await 幫我們幹了啥

作個簡單的比較

上面已經說明了 async 會将其後的函數(函數表達式或 Lambda)的傳回值封裝成一個 Promise 對象,而 await 會等待這個 Promise 完成,并将其 resolve 的結果傳回出來。

現在舉例,用 setTimeout 模拟耗時的異步操作,先來看看不用 async/await 會怎麼寫

function takeLongTime() {
    return new Promise(resolve => {
        setTimeout(() => resolve("long_time_value"), 1000);
    });
}

takeLongTime().then(v => {
    console.log("got", v);
});
           

如果改用 async/await 呢,會是這樣

function takeLongTime() {
    return new Promise(resolve => {
        setTimeout(() => resolve("long_time_value"), 1000);
    });
}

async function test() {
    const v = await takeLongTime();
    console.log(v);
}

test();
           

眼尖的同學已經發現 takeLongTime() 沒有申明為 async。實際上,takeLongTime() 本身就是傳回的 Promise 對象,加不加 async 結果都一樣,如果沒明白,請回過頭再去看看上面的“async 起什麼作用”。

又一個疑問産生了,這兩段代碼,兩種方式對異步調用的處理(實際就是對 Promise 對象的處理)差别并不明顯,甚至使用 async/await 還需要多寫一些代碼,那它的優勢到底在哪?

async/await 的優勢在于處理 then 鍊

單一的 Promise 鍊并不能發現 async/await 的優勢,但是,如果需要處理由多個 Promise 組成的 then 鍊的時候,優勢就能展現出來了(很有意思,Promise 通過 then 鍊來解決多層回調的問題,現在又用 async/await 來進一步優化它)。

假設一個業務,分多個步驟完成,每個步驟都是異步的,而且依賴于上一個步驟的結果。我們仍然用 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);
}
           

現在用 Promise 方式來實作這三個步驟的處理

function doIt() {
    console.time("doIt");
    const time1 = 300;
    step1(time1)
        .then(time2 => step2(time2))
        .then(time3 => step3(time3))
        .then(result => {
            console.log(`result is ${result}`);
            console.timeEnd("doIt");
        });
}

doIt();

// c:\var\test>node --harmony_async_await .
// step1 with 300
// step2 with 500
// step3 with 700
// result is 900
// doIt: 1507.251ms
           

輸出結果 result 是 step3() 的參數 700 + 200 = 900。doIt() 順序執行了三個步驟,一共用了 300 + 500 + 700 = 1500 毫秒,和 console.time()/console.timeEnd() 計算的結果一緻。

如果用 async/await 來實作呢,會是這樣

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 實作是一樣的,但是這個代碼看起來是不是清晰得多,幾乎跟同步代碼一樣

還有更酷的

現在把業務要求改一下,仍然是三個步驟,但每一個步驟都需要之前每個步驟的結果。

function step1(n) {
    console.log(`step1 with ${n}`);
    return takeLongTime(n);
}

function step2(m, n) {
    console.log(`step2 with ${m} and ${n}`);
    return takeLongTime(m + n);
}

function step3(k, m, n) {
    console.log(`step3 with ${k}, ${m} and ${n}`);
    return takeLongTime(k + m + n);
}
           

這回先用 async/await 來寫:

async function doIt() {
    console.time("doIt");
    const time1 = 300;
    const time2 = await step1(time1);
    const time3 = await step2(time1, time2);
    const result = await step3(time1, time2, time3);
    console.log(`result is ${result}`);
    console.timeEnd("doIt");
}

doIt();
           
// c:\var\test>node --harmony_async_await .
// step1 with 300
// step2 with 800 = 300 + 500
// step3 with 1800 = 300 + 500 + 1000
// result is 2000
// doIt: 2907.387ms
           

除了覺得執行時間變長了之外,似乎和之前的示例沒啥差別啊!别急,認真想想如果把它寫成 Promise 方式實作會是什麼樣子?

function doIt() {
    console.time("doIt");
    const time1 = 300;
    step1(time1)
        .then(time2 => {
            return step2(time1, time2)
                .then(time3 => [time1, time2, time3]);
        })
        .then(times => {
            const [time1, time2, time3] = times;
            return step3(time1, time2, time3);
        })
        .then(result => {
            console.log(`result is ${result}`);
            console.timeEnd("doIt");
        });
}

doIt();
           
login(userName) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('1001');
        }, 600);
    });
}

getData(userId) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (userId === '1001') {
                resolve('Success');
            } else {
                reject('Fail');
            }
        }, 600);
    });
}

// 不使用async/await ES7
doLogin(userName) {
    this.login(userName)
        .then(this.getData)
        .then(result => {
            console.log(result)
        })
}

// 使用async/await ES8
async doLogin2(userName) {
    const userId=await this.login(userName);
    const result=await this.getData(userId);
}

this.doLogin()// Success
this.doLogin2()// Success

           

async/await的幾種應用場景

1.擷取異步函數的傳回值

異步函數本身會傳回一個Promise,是以我們可以通過then來擷取異步函數的傳回值。

async function charCountAdd(data1, data2) {
    const d1=await charCount(data1);
    const d2=await charCount(data2);
    return d1+d2;
}
charCountAdd('Hello','Hi').then(console.log);//通過then擷取異步函數的傳回值。
function charCount(data) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(data.length);
        }, 1000);
    });
}

           

2.async/await在并發場景中的應用

對于上述的例子,我們調用await兩次,每次都是等待1秒一共是2秒,效率比較低,而且兩次await的調用并沒有依賴關系,那能不能讓其并發執行呢,答案是可以的,接下來我們通過Promise.all來實作await的并發調用。

async function charCountAdd(data1, data2) {
    const [d1,d2]=await Promise.all([charCount(data1),charCount(data2)]);
    return d1+d2;
}
charCountAdd('Hello','Hi').then(console.log);
function charCount(data) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(data.length);
        }, 1000);
    });
}
           

通過上述代碼我們實作了兩次charCount的并發調用,Promise.all接受的是一個數組,它可以将數組中的promise對象并發執行;

3.async/await的幾種錯誤處理方式

第一種:捕捉整個async/await函數的錯誤

async function charCountAdd(data1, data2) {
    const d1=await charCount(data1);
    const d2=await charCount(data2);
    return d1+d2;
}
charCountAdd('Hello','Hi')
    .then(console.log)
    .catch(console.log);//捕捉整個async/await函數的錯誤
...
           

這種方式可以捕捉整個charCountAdd運作過程中出現的錯誤,錯誤可能是由charCountAdd本身産生的,也可能是由對data1的計算中或data2的計算中産生的。

第二種:捕捉單個的await表達式的錯誤

async function charCountAdd(data1, data2) {
    const d1=await charCount(data1)
        .catch(e=>console.log('d1 is null'));
    const d2=await charCount(data2)
        .catch(e=>console.log('d2 is null'));
    return d1+d2;
}
charCountAdd('Hello','Hi').then(console.log);
           

通過這種方式可以捕捉每一個await表達式的錯誤,如果既要捕捉每一個await表達式的錯誤,又要捕捉整個charCountAdd函數的錯誤,可以在調用charCountAdd的時候加個catch。

...
charCountAdd('Hello','Hi')
    .then(console.log)
    .catch(console.log);//捕捉整個async/await函數的錯誤
...
           

第三種:同時捕捉多個的await表達式的錯誤

async function charCountAdd(data1, data2) {
    let d1,d2;
    try {
        d1=await charCount(data1);
        d2=await charCount(data2);
    }catch (e){
        console.log('d1 is null');
    }
    return d1+d2;
}
charCountAdd('Hello','Hi')
    .then(console.log);

function charCount(data) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(data.length);
        }, 1000);
    });
}