async 與 await 的用法詳解


文章出自個人博客https://knightyun.github.io/2019/08/02/js-async-await,轉載請申明


async

概念

用於聲明異步函數,返回值爲一個 Promise 對象,它以類似 同步 的方式來寫異步方法,語法與聲明函數類似,例如:

async function fn() {
    console.log('Hello world!');
}

console.log(fn().constructor); // Promise()
// 這裏證明其返回值爲一個 Promise 對象;

返回值

也許這裏會有疑問,返回值是 Promise 對象,那麼函數本身定義的返回值跑到哪裏去了呢?其實,熟悉 Promise 的就知道其異步結果是通過 .then() 或者 .catch() 方法來獲取並進行進一步處理的,這樣一個道理,定義的異步函數中的返回值會當成 resolve 狀態來處理,一般用 .then() 方法處理,而如果定義的異步函數拋出錯誤,例如變量未定義,則會被當做 reject 狀態來處理,一般使用 .catch() 方法來處理;

舉例:

// 使用 .then() 的情況
async function fn1() {
    return 'Hello world!';
}

fn1().then(function(res) {
    console.log(res);
});
// Hello world!

// 使用 .catch() 的情況
async function fn2() {
    console.log(aaa); // 這裏的變量 aaa 未定義,爲了製造錯誤
}

fn2().catch(function(error) {
    console.log(error);
});
// ReferenceError: aaa is not defined

假如是既有返回值,又有錯誤的話,來看看結果如何:

async function fn3(){
    console.log(aaa); // aaa 依然未定義;
    return 'Hello world!';
}

fn3().then(function(res){
    console.log(res);
}).catch(function(error){
    console.log(error);
});
// ReferenceError: aaa is not defined

結果證明只會執行 reject 狀態的情況下的語句,忽略了 resolve 時的代碼,所以此處值得 注意

await

概念

用法顧名思義,有 等待 的意思,語法爲:

var value = await myPromise();

所謂 等待 其實就是指暫停當前 async function 內部語句的執行,等待後面的 myPromise() 處理完返回結果後,繼續執行 async function 函數內部的剩餘語句;myPromise() 是一個 Promise對象,而自定義的變量 value 則用於獲取 Promise 對象返回的 resolve 狀態值;

用法

值得 注意 的是,await 必須在 async function 內使用,否則會提示語法錯誤;如果 await 後面跟的是其他值,則直接返回該值:

async function fn() {
    console.log(1);
    var result = await new Promise(function(resolve, reject) {
        setTimeout(function(){
            resolve(2);
        }, 2000);
    });
	console.log(result);
    console.log(3);
    console.log(await 4); // 4 會被直接返回
}
fn();
// 1
// 2 (2 秒後輸出)
// 3
// 4

如果不用獲取返回值,也可以直接執行語句:

async function fn() {
    console.log(1);
    await new Promise(function(resolve, reject) {
        setTimeout(function() {
            console.log(2);
            resolve(0);
        }, 2000);
    });
    console.log(3);
}
fn();
// 1
// 2 (2 秒後)
// 3

返回結果

如之前所說,await 會等到後面的 Promise 返回結果 後纔會執行 async 函數後面剩下的語句,也就是說如果 Promise 不返回結果(如 resolve 或 reject),後面的代碼就不會執行,例如:

async function fn() {
    console.log(1);
    await new Promise(function(resolve, reject) {
        setTimeout(function() {
            console.log(2);
        }, 2000);
    });
    console.log(3);
}
fn();
// 1
// 2 (2 秒後輸出,並且後面不會繼續輸出 3)

這裏可以理解爲函數會一直等待 await 返回結果(resolve / reject)纔會執行剩下的語句,沒有返回結果就會一直等下去,也就一直等不到剩下的語句執行了(還挺癡情-_-);

如果 await 後面的 Promise 返回一個 reject 狀態的結果的話,則會被當成錯誤在後臺拋出,例如:

async function fn() {
    console.log(1);
    var result = await new Promise(function(resolve, reject) {
        setTimeout(function() {
            reject(2);
        }, 2000);
    });
    console.log(3);
}
fn();
// 1
// Uncaught (in promise) 2 (2 秒後輸出)

如上,2 秒後會拋出出錯誤,並且 3 這個數並沒有被輸出,說明後面的執行也被忽略了;

匿名函數

async 也可以用於申明匿名函數用於不同場景,或者嵌套使用 async 函數,如 await async 的形式,只是要在 await 後面使用 async 形式的函數的話,需要這個函數立即執行且有返回值;

let fn = async function() {
    let a = await (async function() {
        console.log(1);
        return 2;
    })();
    console.log(a);

    async function fn2() {
        return 3;
    }
    console.log(await fn2());
}
fn();
// 1
// 2
// 3

另外,await 後面的 Promise 返回的 reject, 也可以被該 async 函數返回的 Promise 對象以 reject 狀態獲取,例如:

async function fn() {
    console.log(1);
    var result = await new Promise(function(resolve, reject) {
        setTimeout(function() {
            reject(2);
        }, 2000);
    });
    console.log(3);
}
fn().catch(function(error) {
    console.log(error);
});
// 1
// 2 (2 秒後輸出)

這種情況就不會以錯誤拋出,直接對異常值進行了處理,並且最後同樣沒有輸出數字 3,即後面的代碼依然被忽略了;

注意事項

非 await 部分

async/await 函數以同步的方式書寫異步函數確實方便了不少場景,如定義所講,函數內部遇到 await 會等到返回結果再繼續執行下去,也就是說,非 await 部分仍然會以正常的異步或同步方式執行,例如遇到 setTimeout() 就會放入任務隊列等待同步語句執行完後再執行;

比如以下情況:

async function fn() {
    console.log(0);
    
    await new Promise(resolve => {
        setTimeout(() => {
            console.log(1);
            resolve();
        }, 1000);
    });

    setTimeout(() => {
        console.log(2);
    }, 0);

    console.log(3);
}

fn();
// 0
// 1(2 秒後)
// 3
// 2

await 內部

雖然說函數會等待 await 返回結果在繼續執行,但是 await 內部的代碼也依然按正常的同步和異步執行,例如:

async function fn() {
    console.log(0);
    
    setTimeout(() => {
        console.log(1);
    }, 0);

    await new Promise(resolve => {
        setTimeout(() => {
            console.log(2);
        }, 0);

        console.log(3);

        setTimeout(() => {
            console.log(4);
            resolve();
        }, 1000);

        setTimeout(() => {
            console.log(5);
        }, 0);
    });

    setTimeout(() => {
        console.log(6);
    }, 0);
    console.log(7);
}

fn();
// 0
// 3
// 1
// 2
// 5
// 4(2 秒後)
// 7
// 6

上面的代碼中返回結果的函數 resolve() 是在 setTimeout() 這個 異步任務 中,所以其被丟到事件隊列中等待 2 秒再執行,由於此時 await 還未返回結果,所以還不會去執行 await 以外的代碼(輸出 7、6),而是先執行同爲異步任務、但延時較短的輸出 1、2、5 的代碼;2 秒後結果返回了,就會繼續正常執行 await 以外的同步任務和異步任務了;

但是假如 await 代碼內返回結果的函數(resolve() 或 reject())是在 同步任務 中執行的話,情況就有些不一樣了,例如:

async function fn() {
    console.log(0);

    setTimeout(() => {
        console.log(1);
    }, 0);

    await new Promise(resolve => {
        setTimeout(() => {
            console.log(2);
        }, 0);

        console.log(3);
        resolve();
        console.log(4);

        setTimeout(() => {
            console.log(5);
        }, 0);
    });

    setTimeout(() => {
        console.log(6);
    }, 0);
    console.log(7);
}

fn();
// 0 
// 3
// 4
// 7
// 1
// 2
// 5
// 6

由於同步任務 先於 異步任務執行的機理,在同步任務執行過程中依次輸出了 0、3 後,就立即執行了 resolve() 使得 await 得到了返回結果,再往後就繼續同步的輸出了 4,但是輸出 5 的代碼是異步任務,與輸出 1、2 的代碼一併放入任務隊列,此時由於 await 返回了結果,所以可以執行 await 以外的代碼了,輸出 6 是異步任務,於是先輸出了同步任務的 7,同步任務都執行完了,最後執行任務隊列中的異步任務,按之前進入隊列的順序,就是依次輸出 1、2、5、6,所有代碼運行結束;

函數嵌套

當 async 函數中嵌套着其他 async 函數時,執行過程可能又有些和預想的不一樣,先來看下面的例子:

async function fn() {
    console.log(0);

    setTimeout(() => {
        console.log(1);
    }, 0);

    (async function() {
        console.log(2);
    
        setTimeout(() => {
            console.log(3);
        }, 0);

        await new Promise(res => setTimeout(res, 1000))

        setTimeout(() => {
            console.log(4);
        }, 1000);

        console.log(5);
    })()

    console.log(6)
}

fn();
// 0
// 2
// 6
// 1
// 3
// 5(1 秒後)
// 4(再等 1 秒後)

也許會疑惑,不是說 async 函數會等到 await 返回結果後再繼續執行嗎,爲何就先輸出 6 了?其實不要混淆概念,確實 async 函數內部是這樣乾的(3 後 1秒輸出 5、4),但 async 函數它自身執行時依然是正常的同步任務執行,也就是雖然內部的 async 函數會等待其 await 返回結果才繼續執行後面的代碼,但外部的 async 函數可不會等待內部的那個 await,會照常執行(你不是我的菜,天涯何處無芳草╮(╯▽╰)╭);

如果確實需要等待這個嵌套的 async 函數執行完再執行剩下的代碼,那麼前面加個 await 就行了,原理是也是可行的,因爲 async 函數就是返回的一個 Promise 函數,代碼如下:

async function fn() {
    console.log(0);

    setTimeout(() => {
        console.log(1);
    }, 0);

    await (async function() {
        console.log(2);
    
        setTimeout(() => {
            console.log(3);
        }, 0);

        await new Promise(res => setTimeout(res, 1000))

        setTimeout(() => {
            console.log(4);
        }, 1000);

        console.log(5);
    })()

    console.log(6)
}

fn();
// 0
// 2
// 1
// 3
// 5(1 秒後)
// 6
// 4(再等 1 秒後)

這裏也要 注意,假如嵌套的 async 函數中的 await 不返回結果,並且沒有在嵌套的 async 函數前面添加 await,那麼外部的 async 函數內部剩餘的代碼也不會執行;


技術文章推送
手機、電腦實用軟件分享
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章