摘要: 還不用Async/Await就OUT了。。
- 原文:掌握 Async/Await
- 作者:Jartto
Fundebug經授權轉載,版權歸原作者所有。
前端工程師肯定都經歷過 JS 回調鏈獄的痛苦過程,我們在使用 Promise 的時候總是不盡人意。這時候 Async/Await 應運而生,它到底有什麼魔力,我們來說道說道。
一、回顧 Promise
所謂 Promise,簡單說就是一個容器,裏面保存着某個未來纔會結束的事件(通常是一個異步操作)的結果。
1. 語法
new Promise(executor);
new Promise(function(resolve, reject) { ... });
2. 參數
帶有 resolve 、reject 兩個參數的一個函數。這個函數在創建 Promise 對象的時候會立即得到執行(在 Promise 構造函數返回 Promise 對象之前就會被執行),並把成功回調函數(resolve)和失敗回調函數(reject)作爲參數傳遞進來。調用成功回調函數(resolve)和失敗回調函數(reject)會分別觸發 Promise 的成功或失敗。
這個函數通常被用來執行一些異步操作,操作完成以後可以選擇調用成功回調函數(resolve)來觸發 Promise 的成功狀態,或者,在出現錯誤的時候調用失敗回調函數(reject)來觸發 Promise 的失敗。
3. Promise.all
用 Promise.all
來執行,all
接收一個數組參數,裏面的值最終都算返回 Promise
對象。這樣,三個異步操作的並行執行的,等到它們都執行完後纔會進到 then
裏面。
Promise.all([async1(), async2(), async3()])
.then(function(results){
console.log(results);
});
all
會把所有異步操作的結果放進一個數組中傳給 then
,就是上面的 results
。
4. Promise.race
all
方法的效果實際上是「誰跑的慢,以誰爲準執行回調」,那麼相對的就有另一個方法「誰跑的快,以誰爲準執行回調」,這就是 race
方法:
Promise.race([requestImg(), timeout()])
.then(function(results){
console.log(results);
})
.catch(function(reason){
console.log(reason);
});
上述代碼演示了 race
的基本用法,實現的功能是:請求圖片,如果請求成功就返回圖片,否則就調用超時函數。
更多資源,請查看:
二、Promise 爲何不完美?
乍一看,Promise
還不錯,幫我們解決了回調鏈獄的問題。當然這只是簡單使用,碰到複雜的業務也有很雞肋的場景,比如:
1. 錯誤處理
在下面的 Promise
示例中,Try/Catch
不能處理 JSON.parse
的錯誤,因爲它在 Promise
中。我們需要使用 catch
,這樣錯誤處理代碼非常冗餘。並且,在我們的實際生產代碼會更加複雜。
const makeRequest = () => {
try {
getJSON().then(result => {
// JSON.parse可能會出錯
const data = JSON.parse(result)
console.log(data)
})
// 取消註釋,處理異步代碼的錯誤
// .catch((err) => {
// console.log(err)
// })
} catch (err) {
console.log(err)
}
}
Async/Await
讓 Try/Catch
可以同時處理同步和異步錯誤。使用 Async/Await
的話,Catch
能處理 JSON.parse
錯誤:
const makeRequest = async () => {
try {
// this parse may fail
const data = JSON.parse(await getJSON())
console.log(data)
} catch (err) {
console.log(err)
}
}
Async/Await
最讓人舒服的一點是代碼看起來是同步的。
2. 條件語句
下面示例中,需要獲取數據,然後根據返回數據決定是直接返回,還是繼續獲取更多的數據。
const makeRequest = () => {
return getJSON()
.then(data => {
if (data.needsAnotherRequest) {
return makeAnotherRequest(data)
.then(moreData => {
console.log(moreData)
return moreData
})
} else {
console.log(data)
return data
}
})
}
這些代碼看着就頭痛。嵌套(6層),括號,return
語句很容易讓人感到迷茫,而它們只是需要將最終結果傳遞到最外層的Promise
。如果換成 Async/Await
呢:
const makeRequest = async () => {
const data = await getJSON();
if (data.needsAnotherRequest) {
const moreData = await makeAnotherRequest(data);
console.log(moreData);
return moreData;
} else {
console.log(data);
return data;
}
}
所以,這纔是真正擺脫回調鏈獄的正確做法。
3. 中間值
你很可能遇到過這樣的場景,調用 promise1
,使用 promise1
返回的結果去調用 promise2
,然後使用兩者的結果去調用promise3
。你的代碼很可能是這樣的:
const makeRequest = () => {
return promise1()
.then(value1 => {
return promise2(value1);
.then(value2 => {
return promise3(value1, value2);
})
})
}
// 或者:
const makeRequest = () => {
return promise1()
.then(value1 => {
return Promise.all([value1, promise2(value1)])
})
.then(([value1, value2]) => {
return promise3(value1, value2)
})
}
怎麼寫都會覺得很複雜,那如果 Async/Await
用來實現呢,表現可能如下:
const makeRequest = async () => {
const value1 = await promise1();
const value2 = await promise2(value1);
return promise3(value1, value2);
}
是不是很 6 ,將複雜的場景簡化,這樣的代碼就很有靈性了。
4. 錯誤棧
調用了多個 Promise
,假設 Promise
鏈中某個地方拋出了一個錯誤,Promise
鏈中返回的錯誤棧沒有給出錯誤發生位置的線索。更糟糕的是,它會誤導我們;錯誤棧中唯一的函數名爲 callAPromise
,然而它和錯誤沒有關係。(文件名和行號還是有用的)。
const makeRequest = () => {
return callAPromise()
.then(() => callAPromise())
.then(() => callAPromise())
.then(() => callAPromise())
.then(() => callAPromise())
.then(() => {
throw new Error("oops");
})
}
makeRequest().catch(err => {
console.log(err);
// output
// Error: oops at callAPromise.then.then.then.then.then (index.js:8:13)
})
然而,Async/Await
中的錯誤棧會指向錯誤所在的函數:
const makeRequest = async () => {
await callAPromise();
await callAPromise();
await callAPromise();
throw new Error("oops");
}
makeRequest().catch(err => {
console.log(err);
// output
// Error: oops at makeRequest (index.js:7:9)
})
5. 調試
調試 Promise
有兩個問題:
- 不能在返回表達式的箭頭函數中設置斷點;
- 如果你在
then
代碼塊中設置斷點,調試器不會跳到下一個then
,因爲它只會跳過異步代碼;
而使用 Await/Async
時,你不再需要那麼多箭頭函數,這樣你就可以像調試同步代碼一樣跳過 Await
語句。
這裏只簡單的列出問題,詳細請查看原文:Async/Await 替代 Promise 的 6 個理由
三、新時代的曙光 Async/Await
簡單介紹:
- Await/Async 是寫異步代碼的新方式,以前的方法有回調函數和 Promise。
- Await/Async 是基於 Promise 實現的,它不能用於普通的回調函數。
- Await/Async 與 Promise 一樣,是非阻塞的。
- Await/Async 使得異步代碼看起來像同步代碼,這正是它的魔力所在。
使用 Promise
是這樣的:
const jarttoDemo = () =>
getJSON().then(data => {
return data;
})
jarttoDemo();
使用 Async/Await
是這樣的:
const jarttoDemo = async () => {
let data = await getJSON();
return data;
}
jarttoDemo();
基本規則:
- Async 表示這是一個 Async 函數,Await 只能用在這個函數裏面。
- Await 表示在這裏等待 Promise返回結果了,再繼續執行。
- Await 後面跟着的應該是一個 Promise 對象,當然,其他返回值也沒關係,只是會立即執行,不過那樣就沒有意義了。
四、更多用法示例
1. 簡單示例
var sleep = function (time) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve();
}, time);
})
};
var start = async function () {
// 在這裏使用起來就像同步代碼那樣直觀
console.log('start');
await sleep(3000);
console.log('end');
};
start();
2. 獲得返回值
var sleep = function (time) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
// 返回 ‘ok’
resolve('ok');
}, time);
})
};
var start = async function () {
let result = await sleep(3000);
console.log(result); // 收到 ‘ok’
};
3. 錯誤捕獲
const makeRequest = async () => {
try {
// this parse may fail
const data = JSON.parse(await getJSON())
console.log(data)
} catch (err) {
console.log(err)
}
}
既然 then
不用寫了,那麼 catch
也不用寫,可以直接用標準的 try catch
語法捕捉錯誤。
var sleep = function (time) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
// 模擬出錯了,返回 ‘error’
reject('error');
}, time);
})
};
var start = async function () {
try {
console.log('start');
await sleep(3000); // 這裏得到了一個返回錯誤
// 所以以下代碼不會被執行了
console.log('end');
} catch (err) {
console.log(err); // 這裏捕捉到錯誤 `error`
}
};
4. 條件語句
Promise
寫法:
const makeRequest = () => {
return getJSON()
.then(data => {
if (data.needsAnotherRequest) {
return makeAnotherRequest(data)
.then(moreData => {
return moreData;
})
} else {
return data;
}
})
}
Async/Await
寫法:
const makeRequest = async () => {
const data = await getJSON();
if (data.needsAnotherRequest) {
const moreData = await makeAnotherRequest(data);
return moreData;
} else {
return data;
}
}
5. 循環多個 Await
var start = async function () {
for (let i = 1; i <= 10; i++) {
console.log(`當前是第 ${i} 次等待..`);
await sleep(1000);
}
};
需要注意的是,Await
必須在 Async
函數的上下文中的。
6. 在 forEach
中使用
async function printFiles () {
const files = await getFilePaths();
for (let file of files) {
const contents = await fs.readFile(file, 'utf8');
console.log(contents);
}
}
async function printFiles () {
const files = await getFilePaths();
await Promise.all(files.map(async (file) => {
const contents = await fs.readFile(file, 'utf8')
console.log(contents)
}));
}
示例參考如下文章:
五、總結
我們一直在強調代碼的可讀性和可維護性,對我來說,Async/Await 更加易懂和易用。所以,不管是 Promise 還是 Async/Await ,能解決實際問題的技術就是好技術。
當然,Async/Await 也是基於 Promise 概念的,技術上我們也可以求同存異,不必太過較真。一句話,選擇權在你!