好用的 async/await

舞動乾坤發表於2017-11-01

ES7 提出的async 函式,終於讓 JavaScript 對於非同步操作有了終極解決方案。No more callback hell。
async 函式是 Generator 函式的語法糖。使用 關鍵字 async 來表示,在函式內部使用 await 來表示非同步。
想較於 Generator,Async 函式的改進在於下面四點:

  • 內建執行器。Generator 函式的執行必須依靠執行器,而 Aysnc 函式自帶執行器,呼叫方式跟普通函式的呼叫一樣
  • 更好的語義asyncawait 相較於 *yield 更加語義化
  • 更廣的適用性co 模組約定,yield 命令後面只能是 Thunk 函式或 Promise物件。而 async 函式的 await 命令後面則可以是 Promise 或者 原始型別的值(Number,string,boolean,但這時等同於同步操作)
  • 返回值是 Promiseasync 函式返回值是 Promise 物件,比 Generator 函式返回的 Iterator 物件方便,可以直接使用 then() 方法進行呼叫

Async 與其他非同步操作的對比

先定義一個 Fetch 方法用於獲取 github user 的資訊:

function fetchUser() { 
    return new Promise((resolve, reject) => {
        fetch('https://api.github.com/users/superman66')
        .then((data) => {
            resolve(data.json());
        }, (error) => {
            reject(error);
        })
    });
}
複製程式碼

Promise 方式

/**
 * Promise 方式
 */
function getUserByPromise() {
    fetchUser()
        .then((data) => {
            console.log(data);
        }, (error) => {
            console.log(error);
        })
}
getUserByPromise();
複製程式碼

Promise 的方式雖然解決了 callback hell,但是這種方式充滿了 Promise的 then() 方法,如果處理流程複雜的話,整段程式碼將充滿 then。語義化不明顯,程式碼流程不能很好的表示執行流程。
Generator 方式

/**
 * Generator 方式
 */
function* fetchUserByGenerator() {
    const user = yield fetchUser();
    return user;
}

const g = fetchUserByGenerator();
const result = g.next().value;
result.then((v) => {
    console.log(v);
}, (error) => {
    console.log(error);
})
複製程式碼

Generator 的方式解決了 Promise 的一些問題,流程更加直觀、語義化。但是 Generator 的問題在於,函式的執行需要依靠執行器,每次都需要通過 g.next() 的方式去執行。
async 方式

/**
 * async 方式
 */
 async function getUserByAsync(){
     let user = await fetchUser();
     return user;
 }
getUserByAsync()
.then(v => console.log(v));
複製程式碼

async 函式完美的解決了上面兩種方式的問題。流程清晰,直觀、語義明顯。操作非同步流程就如同操作同步流程。同時 async 函式自帶執行器,執行的時候無需手動載入。

語法

async 函式返回一個 Promise 物件

async 函式內部 return 返回的值。會成為 then 方法回撥函式的引數。

async function  f() {
    return 'hello world'
};
f().then( (v) => console.log(v)) // hello world
複製程式碼

如果 async 函式內部丟擲異常,則會導致返回的 Promise 物件狀態變為 reject 狀態。丟擲的錯誤而會被 catch 方法回撥函式接收到。

async function e(){
    throw new Error('error');
}
e().then(v => console.log(v))
.catch( e => console.log(e));
複製程式碼

async 函式返回的 Promise 物件,必須等到內部所有的 await 命令的 Promise 物件執行完,才會發生狀態改變

也就是說,只有當 async 函式內部的非同步操作都執行完,才會執行 then 方法的回撥。

const delay = timeout => new Promise(resolve=> setTimeout(resolve, timeout));
async function f(){
    await delay(1000);
    await delay(2000);
    await delay(3000);
    return 'done';
}

f().then(v => console.log(v)); // 等待6s後才輸出 'done'
複製程式碼

正常情況下,await 命令後面跟著的是 Promise ,如果不是的話,也會被轉換成一個 立即 resolve 的 Promise
如下面這個例子:

async function  f() {
    return await 1
};
f().then( (v) => console.log(v)) // 1
複製程式碼

如果返回的是 reject 的狀態,則會被 catch 方法捕獲。

Async 函式的錯誤處理

async 函式的語法不難,難在錯誤處理上。
先來看下面的例子:

let a;
async function f() {
    await Promise.reject('error');
    a = await 1; // 這段 await 並沒有執行
}
f().then(v => console.log(a));
複製程式碼

如上面所示,當 async 函式中只要一個 await 出現 reject 狀態,則後面的 await 都不會被執行。
解決辦法:可以新增 try/catch

// 正確的寫法
let a;
async function correct() {
    try {
        await Promise.reject('error')
    } catch (error) {
        console.log(error);
    }
    a = await 1;
    return a;
}

correct().then(v => console.log(a)); // 1
複製程式碼

如果有多個 await 則可以將其都放在 try/catch 中。

如何在專案中使用

依然是通過 babel 來使用。
只需要設定 presetsstage-3 即可。
安裝依賴:

npm install babel-preset-es2015 babel-preset-stage-3 babel-runtime babel-plugin-transform-runtime
複製程式碼

修改.babelrc:

"presets": ["es2015", "stage-3"],
"plugins": ["transform-runtime"]
複製程式碼

這樣就可以在專案中使用 async 函式了。


相關文章