含義
ES2017 标準引入 async 函數,使得一步操作變得更加友善
函數
async
對Generator 函數進行了改進,主要展現在以下方面
- 内置執行器
Generator 函數的執行必須要有執行器,而
async
函數自帶執行器。
async
函數的執行,與普通函數一樣,隻要一行。
- 更好的語義
函數
async
和
await
比起
*
和
yield
,語義更加清楚。
async
表示函數裡有異步操作,
await
表示其後的表達式需要更待結果。
- 更廣的适用性
函數
async
函數的
await
後面,可以是
Promise
對象和原始類型的值(數值、字元串和布爾值但會自動轉換為resolved 的Promise 對象)
- 傳回值是Promise
函數
async
的傳回值是Promise對象,比Generator函數傳回的值是Iterator對象友善多了。可以直接使用
then
方法執行下一步動作
基本用法
函數
async
函數傳回一個Promise對象,可以使用
then
方法添加回調函數。當函數執行的時候,一旦遇到
await
就會先傳回,等待異步操作完成,再接着執行函數體内後面的語句。
例子:指定多少秒後輸出一個值
function timeout(ms) {
return new Promise( (resolve) => {
setTimeout(resolve, ms);
});
}
async function asyncPrint(value, ms){
await timeout(ms);
console.log(value);
}
asyncPrint('hello async',1000);
由于
async
函數傳回的是Promise 對象,可以作為
await
指令的參數。是以,上面的代碼也可以改寫為:
async function timeout(ms){
await new Promise( (resolve) => {
setTimeout(resolve, ms);
});
}
async function asyncPrint(value, ms){
await timeout(ms);
console.log(value);
}
asyncPrint('hello async',1000);
函數
async
有多種使用方式
// 函數聲明
async function abc() {}
// 函數表達式
const foo = async function (){};
// 對象的方法
let obj = { async abc (){}};
// class 的方法
class Storage {
constructor() {
this.cachePromise = caches.open('avatars');
}
async getAvatar(name) {
const cache = await this.cachePromise;
return cache.match(`/avatars/${name}.jpg`);
}
}
const storage = new Storage();
storage.getAvatar('jake').then(…);
// 箭頭函數
const foo = async () => {};
文法
傳回Promise 對象
函數
async
傳回一個Promise對象,
async
函數内部
return
語句傳回的值,會成為
then
方法回調函數的參數
async function f() {
return 'hello async';
}
f().then(value=>console.log(value)); // 'hello async'
函數
async
函數内抛出錯誤,會導緻傳回的Promise 對象變為
reject
狀态。抛出的錯誤對象會被
catch
方法回調函數接收到。
async function f() {
throw new Error('Error');
}
f().then(
value => console.log(value),
error => console.log(error),
)
// 'Error'
Promise對象的狀态變化
函數
async
傳回的Promise對象,必須要等到内部所有
await
指令後面的
Promise
對象完成才會發生狀态改變,或者遇到
return
語句或者抛出錯誤。隻有
async
函數内部的異步操作執行完,才會執行
then
方法指定的回調函數。
async function getTitle(url) {
let response = await fetch(url);
let html = await response.test();
return html.match(/<title>([\s\S]+)<\/title>/i)[1];
}
getTitle('https://tc39.github.io/ecma262/').then(console.log)
await 指令
- 正常情況下,
指令後面是一個Promise對象,傳回Promise對象的結果。如果不是Promise對象,就直接傳回對應的值。await
async function f() {
return await 123;
// 等同于 return 123;
}
f().then(value => console.log(value));
- 當
指令後面是一個await
對象(即:定義thenable
方法的對象),此時then
會将其等同于Promise對象。await
- 指令
後面的Promise對象如果變為await
狀态,則reject
的參數被reject
方法的回調函數接收到。catch
- 任何一個
語句後面的Promise對象變為await
狀态,那麼整個reject
函數都會中斷執行async
async function f() {
await Promise.reject('Error');
await Promise.resolve('Hello async');
}
如果希望即使第一步出錯了,也不要中斷後面的異步操作,有兩種方法解決:
- 将其放入
代碼塊中,這樣不管這個異步操作是否成功,後面的try…catch
都會執行。await
async function f(){
try {
await Promise.reject('Error');
} catch(e) {
}
return await Promise.resolve('Hello async');
}
f().then(value => console.log(value));
- 指令
後面的Promise 對象再跟一個await
方法,處理前面可能會出現的錯誤catch
async function f(){
await Promise.reject('Error').catch( e => console.log(e));
return await Promise.resolve('Hello async');
}
f().then( value=>console.log(value)); // 'Hello async'
錯誤處理
- 如果
後面的異步操作出錯,那麼等同于await
函數傳回的Promise對象被async
.reject
- 為了防止出錯,可以将其放在
代碼塊中,有多個try…catch
的時候,可以統一放在await
代碼塊中.try…catch
注意
- 指令
後面的Promise對象, 運作結果可能是await
,是以最好把reject
指令放在await
代碼塊中try…catch
- 多個
指令後面是異步操作,如果不存在繼發關系,最好讓他們同時觸發await
let foo = await getFoo();
let bar = await getBar();
上面的代碼中,
getFoo
和
getBar
是兩個獨立的異步操作(互不依賴),被寫成繼發關系,這樣比較耗時。
同時觸發的寫法
// 寫法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
// 寫法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise();
let bar = await barPromise();
- 指令
隻能在await
函數中,如果用在其他普通函數中,就會報錯async
- 函數
函數可以保留運作堆棧async
const a = () => {
b().then( () => c());
};
上面代碼中,函數
a
内部運作了一個異步操作
b()
。當
b()
運作的時候,函數
a()
不會中斷,而是繼續執行。等到
b()
運作結束,可能
a()
早就結束了,
b()
所在的上下文環境已經消失了,如果此時的
b()
或
c()
報錯,錯誤對戰将不包括
a()
.
改為
async
函數
const a = async () =>{
await b();
c();
}
此時,
b()
運作的時候,
a()
是暫停執行的,上下文環境都是儲存着的,一旦
b()
或者
c()
報錯,錯誤堆棧将包括
a()
.
async 函數實作原理
函數
async
實作原理就是将Generator 函數和自動執行器封裝在一個函數裡。
async function fn(args){
// ...
}
// 等同于
function fn(args){
return spawn(function* (){
// ...
});
}
function spawn(genF){
return new Promise(function(resolve,reject){
const gen = genF();
function step(nextF){
let next;
try{
next = nextF();
}catch(e){
return reject(e);
}
if(next.done){
return resolve(next.value);
}
Promise.resolve(next.value).then(function(value){
step(function() {return gen.next(v)});
},function(e){
step(function() {return gen.throw(e)});
})
}
step(function() {return gen.next(undefined);});
});
}
與其他異步處理方法比較
- Promise 的寫法比回調函數的寫法有很大的改進,但是代碼完全都是Promise 的API,操作本身的語義不容易看出來
- Generator 函數語義比Promise 寫法清晰,但是必須要有一個任務運作器,自動執行Generator函數。
- async 函數的實作簡潔,符合語義,幾乎沒有語義不相關的代碼。
備注:本文是自己學習阮一峰老師的《ECMAScript 6 入門》所做的筆記,大部分例子來源于此書。