ECMAScript 2017 功能「非同步函式」由 Brian Terlson 提出。
非同步函式有以下變體。請注意每個變體中的關鍵字 async
。
async function foo() {}
const foo = async function () {};
let obj = { async foo() {} }
const foo = async () => {};
達成非同步函式的 Promise
async
function
asyncFunc
()
{
return
123
;
}
asyncFunc
()
.
then
(
x
=>
console
.
log
(
x
));
// 123
拒絕非同步函式的 Promise
async
function
asyncFunc
()
{
throw
new
Error
(
'Problem!'
);
}
asyncFunc
()
.
catch
(
err
=>
console
.
log
(
err
));
// Error: Problem!
await
處理非同步運算的結果和錯誤 運算子 await
(僅允許在非同步函式內使用)會等待其運算元(一個 Promise)解決
await
的結果就是達成的值。await
會擲出拒絕值。處理單一非同步結果
async
function
asyncFunc
()
{
const
result
=
await
otherAsyncFunc
();
console
.
log
(
result
);
}
// Equivalent to:
function
asyncFunc
()
{
return
otherAsyncFunc
()
.
then
(
result
=>
{
console
.
log
(
result
);
});
}
循序處理多個非同步結果
async
function
asyncFunc
()
{
const
result1
=
await
otherAsyncFunc1
();
console
.
log
(
result1
);
const
result2
=
await
otherAsyncFunc2
();
console
.
log
(
result2
);
}
// Equivalent to:
function
asyncFunc
()
{
return
otherAsyncFunc1
()
.
then
(
result1
=>
{
console
.
log
(
result1
);
return
otherAsyncFunc2
();
})
.
then
(
result2
=>
{
console
.
log
(
result2
);
});
}
平行處理多個非同步結果
async
function
asyncFunc
()
{
const
[
result1
,
result2
]
=
await
Promise
.
all
([
otherAsyncFunc1
(),
otherAsyncFunc2
(),
]);
console
.
log
(
result1
,
result2
);
}
// Equivalent to:
function
asyncFunc
()
{
return
Promise
.
all
([
otherAsyncFunc1
(),
otherAsyncFunc2
(),
])
.
then
([
result1
,
result2
]
=>
{
console
.
log
(
result1
,
result2
);
});
}
處理錯誤
async
function
asyncFunc
()
{
try
{
await
otherAsyncFunc
();
}
catch
(
err
)
{
console
.
error
(
err
);
}
}
// Equivalent to:
function
asyncFunc
()
{
return
otherAsyncFunc
()
.
catch
(
err
=>
{
console
.
error
(
err
);
});
}
在解釋非同步函式之前,我需要說明如何結合 Promise 和產生器,透過看似同步的程式碼執行非同步作業。
對於計算一次性結果的非同步函式,ES6 的 Promise 已廣受歡迎。一個範例是 用戶端 fetch
API,它是用來擷取檔案的 XMLHttpRequest 替代方案。使用方式如下
function
fetchJson
(
url
)
{
return
fetch
(
url
)
.
then
(
request
=>
request
.
text
())
.
then
(
text
=>
{
return
JSON
.
parse
(
text
);
})
.
catch
(
error
=>
{
console
.
log
(
`ERROR:
${
error
.
stack
}
`
);
});
}
fetchJson
(
'http://example.com/some_file.json'
)
.
then
(
obj
=>
console
.
log
(
obj
));
co 是一個使用 Promise 和產生器的函式庫,可用於啟用看起來更同步的編碼樣式,但運作方式與前一個範例中使用的樣式相同
const
fetchJson
=
co
.
wrap
(
function
*
(
url
)
{
try
{
let
request
=
yield
fetch
(
url
);
let
text
=
yield
request
.
text
();
return
JSON
.
parse
(
text
);
}
catch
(
error
)
{
console
.
log
(
`ERROR:
${
error
.
stack
}
`
);
}
});
每次呼叫回函式 (一個產生器函式!) 將 Promise 傳回給 co 時,呼叫回函式就會暫停。一旦 Promise 解決,co 就會繼續執行呼叫回函式:如果 Promise 已完成,yield
會傳回完成值;如果 Promise 已拒絕,yield
會擲出拒絕錯誤。此外,co 會將呼叫回函式傳回的結果轉換為 Promise (類似於 then()
的運作方式)。
非同步函式基本上是 co 所做工作的專用語法
async
function
fetchJson
(
url
)
{
try
{
let
request
=
await
fetch
(
url
);
let
text
=
await
request
.
text
();
return
JSON
.
parse
(
text
);
}
catch
(
error
)
{
console
.
log
(
`ERROR:
${
error
.
stack
}
`
);
}
}
在內部,非同步函式的運作方式與產生器非常類似。
以下是非同步函式的執行方式
p
。在開始執行非同步函式時,就會建立該 Promise。return
或 throw
永久結束。或者,它可能會透過 await
暫時結束;在這種情況下,執行通常會在稍後繼續進行。p
。在執行非同步函式的主體時,return x
會使用 x
解決 Promise p
,而 throw err
會使用 err
拒絕 p
。解決通知會非同步發生。換句話說:then()
和 catch()
的呼叫回函式會在目前的程式碼結束後才執行。
以下程式碼示範其運作方式
async
function
asyncFunc
()
{
console
.
log
(
'asyncFunc()'
);
// (A)
return
'abc'
;
}
asyncFunc
().
then
(
x
=>
console
.
log
(
`Resolved:
${
x
}
`
));
// (B)
console
.
log
(
'main'
);
// (C)
// Output:
// asyncFunc()
// main
// Resolved: abc
您可以依賴下列順序
return
解決。解決 Promise 是標準作業。return
使用它來解決非同步函式的 Promise p
。這表示
p
。p
現在反映該 Promise 的狀態。因此,您可以傳回 Promise,而該 Promise 也不會包裝在 Promise 中
async
function
asyncFunc
()
{
return
Promise
.
resolve
(
123
);
}
asyncFunc
()
.
then
(
x
=>
console
.
log
(
x
))
// 123
有趣的是,傳回已拒絕的 Promise 會導致非同步函式的結果遭到拒絕(通常,您會使用 throw
進行此操作)
async
function
asyncFunc
()
{
return
Promise
.
reject
(
new
Error
(
'Problem!'
));
}
asyncFunc
()
.
catch
(
err
=>
console
.
error
(
err
));
// Error: Problem!
這與 Promise 解析運作方式一致。它讓您能夠轉送其他非同步運算的完成和拒絕,而不需要 await
async
function
asyncFunc
()
{
return
anotherAsyncFunc
();
}
前述程式碼大致類似於下列程式碼(僅解開 anotherAsyncFunc()
的 Promise,然後再次包裝),但效率更高
async
function
asyncFunc
()
{
return
await
anotherAsyncFunc
();
}
await
的提示 await
在非同步函式中容易犯的一個錯誤是,在進行非同步函式呼叫時忘記 await
async
function
asyncFunc
()
{
const
value
=
otherAsyncFunc
();
// missing `await`!
···
}
在此範例中,value
設定為 Promise,這通常不是您在非同步函式中想要的。
即使非同步函式沒有傳回任何內容,await
仍有意義。然後,其 Promise 僅用作通知呼叫者已完成的訊號。例如
async
function
foo
()
{
await
step1
();
// (A)
···
}
第 (A) 行的 await
保證 step1()
在執行 foo()
的其餘部分之前完全完成。
await
有時候,您只想觸發非同步運算,而對它何時完成不感興趣。下列程式碼是一個範例
async
function
asyncFunc
()
{
const
writer
=
openFile
(
'someFile.txt'
);
writer
.
write
(
'hello'
);
// don’t wait
writer
.
write
(
'world'
);
// don’t wait
await
writer
.
close
();
// wait for file to close
}
在此,我們不關心個別寫入何時完成,只關心它們是否按正確順序執行(API 必須保證這一點,但這是由非同步函式的執行模型鼓勵的,正如我們所見)。
asyncFunc()
最後一行的 await
確保函式僅在檔案成功關閉後才完成。
由於回傳的 Promise 沒有包裝,您也可以使用 return
代替 await
writer.close()
async
function
asyncFunc
()
{
const
writer
=
openFile
(
'someFile.txt'
);
writer
.
write
(
'hello'
);
writer
.
write
(
'world'
);
return
writer
.
close
();
}
這兩個版本各有優缺點,await
版本可能稍微容易理解一些。
await
是順序的,Promise.all()
是並行的 下列程式碼執行兩個非同步函式呼叫,asyncFunc1()
和 asyncFunc2()
。
async
function
foo
()
{
const
result1
=
await
asyncFunc1
();
const
result2
=
await
asyncFunc2
();
}
然而,這兩個函式呼叫是順序執行的。並行執行它們往往可以加快速度。您可以使用 Promise.all()
執行此操作
async
function
foo
()
{
const
[
result1
,
result2
]
=
await
Promise
.
all
([
asyncFunc1
(),
asyncFunc2
(),
]);
}
我們現在不是等待兩個 Promise,而是等待一個包含兩個元素的陣列的 Promise。
非同步函式的其中一個限制是 await
僅影響直接周圍的非同步函式。因此,非同步函式無法在回呼函式中使用 await
(然而,回呼函式本身可以是非同步函式,我們稍後會看到)。這使得基於回呼函式的工具函式和方法難以使用。範例包括陣列方法 map()
和 forEach()
。
Array.prototype.map()
我們從陣列方法 map()
開始。在下列程式碼中,我們想要下載由 URL 陣列指向的檔案,並將它們回傳到陣列中。
async
function
downloadContent
(
urls
)
{
return
urls
.
map
(
url
=>
{
// Wrong syntax!
const
content
=
await
httpGet
(
url
);
return
content
;
});
}
這無法運作,因為 await
在一般的箭頭函式中語法不合法。那麼使用非同步箭頭函式如何呢?
async
function
downloadContent
(
urls
)
{
return
urls
.
map
(
async
(
url
)
=>
{
const
content
=
await
httpGet
(
url
);
return
content
;
});
}
這段程式碼有兩個問題
map()
完成後,回呼函式執行的作業並未完成,因為 await
僅暫停周圍的箭頭函式,而 httpGet()
是非同步解析的。這表示您無法使用 await
等待 downloadContent()
完成。我們可以透過 Promise.all()
修復這兩個問題,它會將 Promise 陣列轉換為陣列的 Promise(其中值由 Promise 完成)
async
function
downloadContent
(
urls
)
{
const
promiseArray
=
urls
.
map
(
async
(
url
)
=>
{
const
content
=
await
httpGet
(
url
);
return
content
;
});
return
await
Promise
.
all
(
promiseArray
);
}
map()
的回呼函式不會對 httpGet()
的結果做太多處理,它只會轉送它。因此,我們這裡不需要非同步箭頭函式,一般的箭頭函式就可以了
async
function
downloadContent
(
urls
)
{
const
promiseArray
=
urls
.
map
(
url
=>
httpGet
(
url
));
return
await
Promise
.
all
(
promiseArray
);
}
我們仍然可以做一個小改進:這個非同步函式有點低效率,它會先透過 await
解開 Promise.all()
的結果,然後再透過 return
重新包裝它。由於 return
沒有包裝 Promise,我們可以直接回傳 Promise.all()
的結果
async
function
downloadContent
(
urls
)
{
const
promiseArray
=
urls
.
map
(
url
=>
httpGet
(
url
));
return
Promise
.
all
(
promiseArray
);
}
Array.prototype.forEach()
讓我們使用陣列方法 forEach()
來記錄透過 URL 指向的幾個檔案的內容
async
function
logContent
(
urls
)
{
urls
.
forEach
(
url
=>
{
// Wrong syntax
const
content
=
await
httpGet
(
url
);
console
.
log
(
content
);
});
}
再次,這段程式碼會產生語法錯誤,因為你無法在一般的箭頭函式中使用 await
。
讓我們使用非同步箭頭函式
async
function
logContent
(
urls
)
{
urls
.
forEach
(
async
url
=>
{
const
content
=
await
httpGet
(
url
);
console
.
log
(
content
);
});
// Not finished here
}
這確實有效,但有一個注意事項:httpGet()
回傳的 Promise 是非同步解析的,這表示當 forEach()
回傳時,呼叫函式尚未完成。因此,你無法等待 logContent()
結束。
如果你不想要這樣,你可以將 forEach()
轉換為 for-of
迴圈
async
function
logContent
(
urls
)
{
for
(
const
url
of
urls
)
{
const
content
=
await
httpGet
(
url
);
console
.
log
(
content
);
}
}
現在,在 for-of
迴圈之後,一切就都完成了。但是,處理步驟是按順序發生的:只有在第一次呼叫完成之後,才會第二次呼叫 httpGet()
。如果你希望處理步驟並行發生,你必須使用 Promise.all()
async
function
logContent
(
urls
)
{
await
Promise
.
all
(
urls
.
map
(
async
url
=>
{
const
content
=
await
httpGet
(
url
);
console
.
log
(
content
);
}));
}
map()
用於建立 Promise 陣列。我們對它們完成的結果不感興趣,我們只 await
直到它們全部完成。這表示我們在這個非同步函式的結尾時已經完全完成。我們也可以回傳 Promise.all()
,但函式的結果將會是一個陣列,其元素都是 undefined
。
非同步函式的基礎是 Promise。這就是為什麼了解 Promise 對了解非同步函式至關重要的原因。特別是在將未基於 Promise 的舊程式碼與非同步函式連接時,你常常別無選擇,只能直接使用 Promise。
例如,這是 XMLHttpRequest
的「promisified」版本
function
httpGet
(
url
,
responseType
=
""
)
{
return
new
Promise
(
function
(
resolve
,
reject
)
{
const
request
=
new
XMLHttpRequest
();
request
.
onload
=
function
()
{
if
(
this
.
status
===
200
)
{
// Success
resolve
(
this
.
response
);
}
else
{
// Something went wrong (404 etc.)
reject
(
new
Error
(
this
.
statusText
));
}
};
request
.
onerror
=
function
()
{
reject
(
new
Error
(
'XMLHttpRequest Error: '
+
this
.
statusText
));
};
request
.
open
(
'GET'
,
url
);
xhr
.
responseType
=
responseType
;
request
.
send
();
});
}
XMLHttpRequest
的 API 基於呼叫函式。透過非同步函式使其 promisified 表示你必須從呼叫函式中完成或拒絕函式回傳的 Promise。這是不可行的,因為你只能透過 return
和 throw
來這麼做。而且你無法從呼叫函式中 return
函式的結果。throw
有類似的限制。
因此,非同步函式的常見編碼樣式將會是
進一步閱讀:「探索 ES6」中的「非同步程式設計的 Promise」章節。
有時候,如果你可以在模組或指令碼的最上層使用 await
會很好。唉,它只能在非同步函數內使用。因此你有多種選擇。你可以建立一個非同步函數 main()
,然後立即呼叫它
async
function
main
()
{
console
.
log
(
await
asyncFunction
());
}
main
();
或者你可以使用立即呼叫非同步函數表達式
(
async
function
()
{
console
.
log
(
await
asyncFunction
());
})();
另一個選擇是立即呼叫非同步箭頭函數
(
async
()
=>
{
console
.
log
(
await
asyncFunction
());
})();
以下程式碼使用 測試架構 mocha 來對非同步函數 asyncFunc1()
和 asyncFunc2()
進行單元測試
import
assert
from
'assert'
;
// Bug: the following test always succeeds
test
(
'Testing async code'
,
function
()
{
asyncFunc1
()
// (A)
.
then
(
result1
=>
{
assert
.
strictEqual
(
result1
,
'a'
);
// (B)
return
asyncFunc2
();
})
.
then
(
result2
=>
{
assert
.
strictEqual
(
result2
,
'b'
);
// (C)
});
});
但是,這個測試總是會成功,因為 mocha 沒有等到 (B) 行和 (C) 行中的斷言執行完畢。
你可以透過傳回 Promise 鏈的結果來修正這個問題,因為 mocha 會辨識測試是否傳回 Promise,然後等到該 Promise 解決(除非有逾時)。
return
asyncFunc1
()
// (A)
很方便的是,非同步函數總是傳回 Promise,這讓它們非常適合這種單元測試
import
assert
from
'assert'
;
test
(
'Testing async code'
,
async
function
()
{
const
result1
=
await
asyncFunc1
();
assert
.
strictEqual
(
result1
,
'a'
);
const
result2
=
await
asyncFunc2
();
assert
.
strictEqual
(
result2
,
'b'
);
});
因此,在 mocha 中使用非同步函數進行非同步單元測試有兩個優點:程式碼更簡潔,而且傳回 Promise 的問題也解決了。
JavaScript 引擎越來越擅長警告未處理的拒絕。例如,以下程式碼在過去通常會在沒有提示的情況下失敗,但現在大多數現代的 JavaScript 引擎都會報告未處理的拒絕
async
function
foo
()
{
throw
new
Error
(
'Problem!'
);
}
foo
();