await:Promiseの使用方法awaitと解決済みのPromiseawaitと拒否されたPromiseawaitは浅い(コールバック内では使用できない)awaitの使用 [ES2022]awaitawait:非同期関数を順次実行するawait:非同期関数を並行して実行するawaitは不要awaitして結果を無視しても意味がある場合がある概して、非同期関数はPromiseを使用するコードにとってより良い構文を提供します。非同期関数を使用するには、そのためPromiseを理解する必要があります。前の章で説明されています。
次の非同期関数について考えてみましょう。
async function fetchJsonAsync(url) {
try {
const request = await fetch(url); // async
const text = await request.text(); // async
return JSON.parse(text); // sync
}
catch (error) {
assert.fail(error);
}
}前の、かなり同期的に見えるコードは、Promiseを直接使用する次のコードと同等です。
function fetchJsonViaPromises(url) {
return fetch(url) // async
.then(request => request.text()) // async
.then(text => JSON.parse(text)) // sync
.catch(error => {
assert.fail(error);
});
}非同期関数fetchJsonAsync()に関するいくつかの観察事項
非同期関数はキーワードasyncでマークされます。
非同期関数の本体内では、Promiseベースのコードを同期的なものとして記述します。await演算子を適用する必要があるのは、値がPromiseである場合だけです。その演算子は非同期関数を一時停止し、Promiseが解決されると再開します。
awaitは解決値を返します。awaitは拒否値をスローします。非同期関数の結果は常にPromiseです。
fetchJsonAsync()とfetchJsonViaPromises()の両方は、このようにまったく同じ方法で呼び出されます。
fetchJsonAsync('http://example.com/person.json')
.then(obj => {
assert.deepEqual(obj, {
first: 'Jane',
last: 'Doe',
});
}); 非同期関数は、Promiseを直接使用する関数と同様にPromiseベースです
外部から見ると、非同期関数とPromiseを返す関数との違いを判別することは事実上不可能です。
JavaScriptには、同期の呼び出し可能なエンティティの次の非同期バージョンがあります。それらの役割は常に実際の関数またはメソッドのいずれかです。
// Async function declaration
async function func1() {}
// Async function expression
const func2 = async function () {};
// Async arrow function
const func3 = async () => {};
// Async method definition in an object literal
const obj = { async m() {} };
// Async method definition in a class definition
class MyClass { async m() {} } 非同期関数 vs. 非同期関数
用語「非同期関数」と「async関数」の違いは微妙ですが重要です。
非同期関数とは、結果を非同期的に配信する関数のことです。たとえば、コールバックベースの関数やPromiseベースの関数など。
async関数は、asyncとawaitキーワードを含む特別な構文で定義されます。これらの2つのキーワードのためにasync/awaitとも呼ばれます。async関数はPromiseに基づいており、したがって非同期関数でもあります(やや混乱を招きます)。
各非同期関数は常にPromiseを返します。
非同期関数内では、return(A行)を介して結果のPromiseを解決します。
async function asyncFunc() {
return 123; // (A)
}
asyncFunc()
.then(result => {
assert.equal(result, 123);
});通常どおり、何も明示的に返さない場合、undefinedが返されます。
async function asyncFunc() {
}
asyncFunc()
.then(result => {
assert.equal(result, undefined);
});throw(A行)を介して結果のPromiseを拒否します。
async function asyncFunc() {
throw new Error('Problem!'); // (A)
}
asyncFunc()
.catch(err => {
assert.deepEqual(err, new Error('Problem!'));
});非同期関数からPromise pを返す場合、pは関数の結果になります(あるいはむしろ、結果はpに「固定」され、まったく同じように動作します)。つまり、Promiseは別のPromiseにラップされません。
async function asyncFunc() {
return Promise.resolve('abc');
}
asyncFunc()
.then(result => assert.equal(result, 'abc'));次の状況では、すべてのPromise qが同様に扱われることを思い出してください。
new Promise((resolve, reject) => { ··· })内のresolve(q).then(result => { ··· })内のreturn q.catch(err => { ··· })内のreturn q非同期関数は次のように実行されます。
pは、非同期関数が開始されたときに作成されます。pを解決しながら、実行を永久的に終了できます。returnはpを解決します。throwはpを拒否します。awaitを介して別のPromise qの解決を待っている場合、実行は一時的に終了することもできます。非同期関数は一時停止し、実行は関数を終了します。qが解決されると再開されます。pが返されます。結果pの解決の通知は、Promiseの場合常にそうであるように、非同期的に発生することに注意してください。
次のコードは、非同期関数が同期的に開始され(A行)、次に現在のタスクが終了し(C行)、次に結果のPromiseが非同期的に解決される(B行)ことを示しています。
async function asyncFunc() {
console.log('asyncFunc() starts'); // (A)
return 'abc';
}
asyncFunc().
then(x => { // (B)
console.log(`Resolved: ${x}`);
});
console.log('Task ends'); // (C)
// Output:
// 'asyncFunc() starts'
// 'Task ends'
// 'Resolved: abc'await:Promiseの使用方法await演算子は、非同期関数と非同期ジェネレーター(42.2章「非同期ジェネレーター」で説明)内でのみ使用できます。そのオペランドは通常Promiseであり、次の手順が実行されます。
yieldの動作に似ています。awaitは解決値を返します。awaitは拒否値をスローします。awaitがさまざまな状態のPromiseをどのように処理するかについて、さらに読み進めてください。
awaitと解決済みのPromiseオペランドが解決済みのPromiseである場合、awaitはその解決値を返します。
assert.equal(await Promise.resolve('yes!'), 'yes!');Promise以外の値も許可されており、単に渡されます(非同期関数を一時停止せずに同期的に)。
assert.equal(await 'yes!', 'yes!');awaitと拒否されたPromiseオペランドが拒否されたPromiseである場合、awaitは拒否値をスローします。
try {
await Promise.reject(new Error());
assert.fail(); // we never get here
} catch (e) {
assert.equal(e instanceof Error, true);
} 演習:非同期関数によるFetch API
exercises/async-functions/fetch_json2_test.mjs
awaitは浅い(コールバック内では使用できない)非同期関数内にいて、awaitを使用して一時停止したい場合、その関数内で直接実行する必要があります。ネストされた関数(コールバックなど)内では使用できません。つまり、一時停止は浅いです。
たとえば、次のコードは実行できません。
async function downloadContent(urls) {
return urls.map((url) => {
return await httpGet(url); // SyntaxError!
});
}理由は、通常の矢印関数は本体内でawaitを許可しないためです。
では、非同期矢印関数を試してみましょう。
async function downloadContent(urls) {
return urls.map(async (url) => {
return await httpGet(url);
});
}しかし、これも機能しません。.map()(したがってdownloadContent())は、(ラップされていない)値を含む配列ではなく、Promiseを含む配列を返します。
1つの可能な解決策は、Promise.all()を使用してすべてのPromiseをアンラップすることです。
async function downloadContent(urls) {
const promiseArray = urls.map(async (url) => {
return await httpGet(url); // (A)
});
return await Promise.all(promiseArray);
}このコードを改善できますか?はい、できます。A行では、awaitを介してPromiseをアンラップし、すぐにreturnを介して再ラップしています。awaitを省略すると、非同期矢印関数も必要なくなります。
async function downloadContent(urls) {
const promiseArray = urls.map(
url => httpGet(url));
return await Promise.all(promiseArray); // (B)
}同じ理由で、B行でもawaitを省略できます。
awaitの使用 [ES2022]モジュールの最上位レベルでawaitを使用できます。たとえば、
let lodash;
try {
lodash = await import('https://primary.example.com/lodash');
} catch {
lodash = await import('https://secondary.example.com/lodash');
}この機能の詳細については、27.14章「モジュールでの最上位レベルのawait [ES2022]」を参照してください。
演習:非同期的なマッピングとフィルタリング
exercises/async-functions/map_async_test.mjs
残りのセクションはすべて上級者向けです。
await次の2つの小節では、ヘルパー関数paused()を使用します。
/**
* Resolves after `ms` milliseconds
*/
function delay(ms) {
return new Promise((resolve, _reject) => {
setTimeout(resolve, ms);
});
}
async function paused(id) {
console.log('START ' + id);
await delay(10); // pause
console.log('END ' + id);
return id;
}await:非同期関数を順次実行する複数の非同期関数の呼び出しの前にawaitを付ける場合、これらの関数は順次実行されます。
async function sequentialAwait() {
const result1 = await paused('first');
assert.equal(result1, 'first');
const result2 = await paused('second');
assert.equal(result2, 'second');
}
// Output:
// 'START first'
// 'END first'
// 'START second'
// 'END second'つまり、paused('second')は、paused('first')が完全に終了した後にのみ開始されます。
await:非同期関数を並行して実行する複数の関数を同時に実行したい場合は、ツールメソッドPromise.all()を使用できます。
async function concurrentPromiseAll() {
const result = await Promise.all([
paused('first'), paused('second')
]);
assert.deepEqual(result, ['first', 'second']);
}
// Output:
// 'START first'
// 'START second'
// 'END first'
// 'END second'ここでは、両方の非同期関数が同時に開始されます。両方が解決されると、awaitは解決値の配列、または少なくとも1つのPromiseが拒否された場合は例外を返します。
40.6.2節「並行処理のヒント:操作の開始時期に注目する」から、重要なのはPromiseベースの計算を開始するタイミングであり、結果を処理する方法ではないことを思い出してください。したがって、次のコードは前のコードと同じくらい「並行」です。
async function concurrentAwait() {
const resultPromise1 = paused('first');
const resultPromise2 = paused('second');
assert.equal(await resultPromise1, 'first');
assert.equal(await resultPromise2, 'second');
}
// Output:
// 'START first'
// 'START second'
// 'END first'
// 'END second'awaitは不要Promiseベースの関数を使用する場合は、awaitは必要ありません。返されたPromiseが解決されるまで一時停止して待機したい場合にのみ必要です。非同期操作を開始したいだけの場合、それは必要ありません。
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
}このコードでは、終了するタイミングを気にしないため、.write()を待機しません。ただし、.close()が完了するまで待機する必要があります。
注:.write()の各呼び出しは同期的に開始されます。これにより、競合状態が防止されます。
awaitして結果を無視しても意味がある場合がある結果を無視する場合でも、awaitを使用する方が理にかなう場合があります。たとえば、
await longRunningAsyncOperation();
console.log('Done!');ここでは、awaitを使用して長時間実行される非同期操作を結合しています。これにより、ロギングがその操作が完了した後に確実に実行されます。