背景
ES7提出的async/await是JavaScript爲了解決異步問題而提出的一種解決方案,沒有更多的回調,許多人將其稱爲異步的終極解決方案。async函數是Generator函數的語法糖。使用關鍵字async表示,在函數內部使用await表示異步。JavaScript的發展也經歷了回調、Promise、async/await三個階段,該文章寫了我自己對於async/await的理解。不對的地方還請大家幫忙指出,共同進步。
概念
async是“異步”的簡寫,async申明一個function是異步的,而await是等待一個異步方法執行完成,await 只能出現在async函數中。
async函數完全可以看作多個異步操作,包裝成的一個 Promise 對象,而await命令就是內部then命令的語法糖。
- async的作用
async函數負責返回一個Promise 對象;如果在async函數中return一個變量,async 會把這個變量通過Promise.resolve() 封裝成Promise對象;如果 async函數沒有返回值,它會返回Promise.resolve(undefined)。 - await在等待什麼
一般我們都用await等待一個async函數完成,await 等待的是一個表達式,這個表達式的計算結果是Promise 對象或者其它值,所以,await後面實際可以接收普通函數調用或者直接量。
如果await等到的不是一個promise對象,那跟着表達式的運算結果就是它等到的東西;如果是一個promise對象,await會阻塞後面的代碼,等promise對象resolve,得到resolve的值作爲await表達式的運算結果
雖然await阻塞了,但await在async中,async不會阻塞,它內部所有的阻塞都被封裝在一個promise對象中異步執行。
優點
不同於Generator的是,async函數的改進在下面四點:
- 內置執行器。Generator 函數的執行必須依靠執行器,而async函數自帶執行器,調用方式跟普通函數的調用一樣。
- 更好的語義。async和await 相較於*和yield更加語義化。
- 更廣的適用性。co模塊約定,yield命令後面只能是Thunk函數或 Promise對象。而async 函數的await命令後面則可以是Promise或原始類型的值(Number,string,boolean,但這時等同於同步操作)。
- 返回值是Promise, async函數返回值是Promise對象,比Generator函數返回的Iterator對象方便,可以直接使用then()方法進行調用。
特點
- 建立在promise之上,所以不能把它和回調函數搭配使用,但它會聲明一個異步函數,並隱式返回一個Promise。因此可以直接return變量,無需使用Promise.resolve進行轉換。
- 和promise一樣,是非阻塞的,但不用寫then及其回調函數,減少代碼行數,避免了代碼嵌套,而且,所有異步調用可以寫在同一個代碼塊中,無需定義多餘中間變量。
- 最大價值在於可使異步代碼在形式上更接近同步代碼。
- 總是與await一起使用的,並且,await只能在async函數體內
- await是個運算符,用於組成表達式,會阻塞後面代碼。若等到的是Promise對象,則得到其resolve值, 否則,得到一個表達式的運算結果。
async/await使用規則
我們在處理異步時,比起回調函數,Promise的then方法會顯得比較簡潔和清晰,但是在處理多個彼此之間相互依賴的請求時,會顯的有些累贅。這個時候,用async和await會更加優雅。
- 規則一:凡是在前面添加async的函數在執行後都會自動返回一個Promise對象。
async function fun() {}
let result = fun()
console.log(result) //即使代碼fun函數什麼都沒返回,依然會打印出Promise對象
- 規則二:await必須在async函數裏使用,不能單獨使用
async fun() {
let result = await Promise.resolve('success')
console.log(result)
}
fun()
- 規則三:await後面需要跟Promise對象,不然就沒有意義,而且await後面的Promise對象不必寫then,因爲await的作用之一就是獲取後面Promise對象成功狀態傳遞出來的參數。
function fun() {
return new Promise((resolve, reject) => {
setTimeout(() => {resolve('hello javascript')})
})
}
async fn() {
let result = await fun() //fun會返回一個Promise對象
console.log(result) //打印出Promise成功後傳遞過來的'hello javascript'
}
fn()
也可以這麼寫,但是意義不大:
let fun = async function() {
let result = await 123
console.log(result)
}
fun()
語法
async函數返回一個Promise 對象
async函數內部return返回的值,是then方法回調函數的參數。
async function fun() {
return 'hello node'
};
fun().then( (v) => console.log(v)) // hello node
如果async函數內部拋出異常,則會導致返回的Promise對象狀態變爲 reject狀態。拋出的錯誤會被catch方法回調函數接收到。
async function fun(){
throw new Error('error');
}
fun().then(v => console.log(v))
.catch( e => console.log(e));
async函數返回的Promise對象,必須等到內部所有的await命令的Promise對象執行完,纔會發生狀態改變
即只有當async函數內部的異步操作都執行完,纔會執行then方法的回調。
如下代碼所示:
const time = timeout => new Promise(resolve=> setTimeout(resolve, timeout));
async function f(){
await time(1000);
await time(4000);
await time(7000);
return 'finish';
}
f().then(v => console.log(v)); // 等待12s後才輸出 'finish'
正常情況下,await 命令後面跟着的是 Promise ,如果不是的話,也會被轉換成一個立即resolve的Promise
如下代碼所示:
async function fun() {
return await 1
};
fun().then( (v) => console.log(v)) // 1
如果返回的是reject的狀態,會被catch方法捕獲。
async函數的錯誤處理
其實async函數語法不難,難就在錯誤處理上。有關錯誤處理,如上面規則三所說,await可直接獲取到後面Promise成功狀態傳遞的參數,但卻捕捉不到失敗狀態。我們通過給包裹await的async函數添加then/catch方法來解決,因爲根據規則一,async函數本身就會返回一個Promise對象。
我們先來看一個demo
let b;
async function fun() {
await Promise.reject('error');
b = await 1; // 這段await並未執行
}
fun().then(v => console.log(b));
注意:當async函數中只要一個await出現reject狀態,則後面的await都不會被執行。解決辦法:可以添加try/catch。
正確的寫法如下:
// 正確的寫法
let b;
async function fun() {
try {
await Promise.reject('error')
} catch (error) {
console.log(error);
}
b = await 1;
return b;
}
fun().then(v => console.log(b)); // 1
如果有多個await則可將其都放在try/catch中。
我們來看一個包含錯誤處理的完整demo:
let promise = new Promise((resolve, reject) => {
setTimeout(() => {
let random = Math.random()
if (random >= 0.5) {
resolve('成功')
} else {
reject('失敗')
}
}, 1000)
})
async function fn() {
let result = await promise
//result是promise成功狀態的值,如果失敗了,代碼就直接跳到下面的catch了
return result
}
fn().then(response => {
console.log(response)
}).catch(error => {
console.log(error)
})
// 最終打印出成功
注意:上述代碼需注意兩個地方,一是async函數需要主動return,如果Promise的狀態是成功的,那麼return的這個值就會被下面的then方法捕捉到;二是如果async函數有任何錯誤,都被catch捕捉到!
同步與異步
在async函數中使用await,await這裏的代碼就會變成同步,意思就是隻有等await後面的Promise執行完成得到結果纔會繼續下去,await就是等待,雖然這樣避免了異步,但是它會阻塞代碼,所以我們在使用的時候需要考慮周全。
我們來看個demo:
function fun1(name) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`${name}成功執行`)
}, 1000)
})
}
async function testfun2() {
let p1 = await fun1('張三')
let p2 = await fun1('李四')
let p3 = await fun1('王麻子')
return [p1, p2, p3]
}
testfun2().then(result => {
console.log(result)
}).catch(result => {
console.log(result)
})
這樣寫雖然是ok的,但是await會阻塞代碼,每個await都必須等後面的fun1()執行完成纔會執行下一行代碼,所以testfun2函數執行需要3秒。如果不是遇到特定的場景,最好不要這樣用。
循環中使用async/await
在循環中使用await,需要牢記一條:必須在async函數中使用。
在for…of中使用await,我們來看個demo:
let fun3 = (time) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(time)
}, time)
})
}
let times = [1000, 2500, 3000]
async function testfun3() {
let result = []
for (let item of times) {
let temp = await fun3(item)
result.push(temp)
}
return result
}
testfun3().then(result => {
console.log(result)
}).catch(error => {
console.log(error)
})
// 最後輸出[ 1000, 2500, 3000 ]
async異步回調併發
1請求、2請求同時發,規定請求到達的順序
假如我們有一種這樣的業務需求,併發兩個請求,但是要規定收到請求的順序應該怎麼做的?這裏借鑑阮一峯大神的代碼:
我們看下demo:
// 1請求
function getData1 () {
return new Promise(function (resolve, reject) {
setTimeout(() => {
console.log('1執行了')
resolve('請求到模擬數據1')
}, 2000)
})
}
// 2請求
function getData2 () {
return new Promise(function (resolve, reject) {
setTimeout(() => {
console.log('2執行了')
resolve('請求到模擬數據2!)
}, 1500)
})
}
async function asyncDemo2 () {
const arr = [getData1, getData2]
const textPromises = arr.map(async function (doc) {
const response = await doc()
return response
})
// 按次序輸出
for (const textPromise of textPromises) {
console.log(await textPromise);
}
}
// 2執行了 (因爲2是1500ms後執行) 所以2先執行
// 1執行了
// 請求到模擬數據1 (for .. of )規定了輸出的順序
// 請求到模擬數據2
適用async/await的業務場景
在前端開發中,我們偶爾會遇到這樣的場景:我們需要發送多個請求,而且後面請求的發送總是需要依賴上一個請求返回的數據。對於這個問題,我們既可以用Promise的鏈式調用來解決,也可用async/await來解決,然而後者會更簡潔些。
我們來看個demo:
使用promise鏈式調用處理下面代碼:
function fun2(time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(time)
}, time)
})
}
fun2(500).then(result => {
return fun2(result + 1000)
}).then(result => {
return fun2(result + 1000)
}).then(result => {
console.log(result)
}).catch(error => {
console.log(error)
})
// 最終結果是2500
使用async/await來處理:
function fun2(time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(time)
}, time)
})
}
async function getResult() {
let p1 = await fun2(500)
let p2 = await fun2(p1 + 1000)
let p3 = await fun2(p2 + 1000)
return p3
}
getResult().then(result => {
console.log(result)
}).catch(error => {
console.log(error)
})
從上述代碼中我們可以看出,相對於使用then不停地進行鏈式調用, 使用async/await顯的更加簡潔清晰明瞭易讀一些。
async相對於Promise的優勢
- 能更好地處理then鏈
- 中間值
- 調試,相比於 Promise 更易於調試
我們總結一下async/await的優點:
- 解決了回調地獄的問題
- 支持併發執行
- 可以添加返回值 return 變量
- 可以在代碼中添加try/catch捕獲錯誤