ECMAScript 2017の機能「非同期関数」は、Brian Terlsonによって提案されました。
非同期関数には以下の種類があります。すべての場所でキーワード`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`(非同期関数内でのみ使用可能)は、オペランドであるPromiseが解決されるまで待機します。
単一の非同期結果の処理
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にyieldするたびに、コールバックは中断されます。Promiseが解決されると、coはコールバックを再開します。Promiseが解決された場合、`yield`は解決値を返し、拒否された場合、`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
}
`
);
}
}
内部的には、非同期関数はジェネレーターと非常によく似ています。
非同期関数はこのように実行されます。
非同期関数の本体を実行している間、`return x`はPromise `p`を`x`で解決し、`throw err`は`p`を`err`で拒否します。解決の通知は非同期的に行われます。つまり、`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
以下の順序を信頼できます。
Promiseの解決は標準的な操作です。`return`はそれを用いて非同期関数のPromise `p`を解決します。つまり
したがって、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`を忘れることがあります。
async
function
asyncFunc
()
{
const
value
=
otherAsyncFunc
();
// missing `await`!
···
}
この例では、`value`はPromiseに設定されます。これは通常、非同期関数では望ましいものではありません。
`await`は、非同期関数が何も返さない場合でも意味があります。その場合、そのPromiseは単に呼び出し元に終了を伝えるシグナルとして使用されます。例えば
async
function
foo
()
{
await
step1
();
// (A)
···
}
行 (A) の`await`は、`foo()`の残りの部分が実行される前に`step1()`が完全に終了することを保証します。
非同期計算をトリガーするだけで、それがいつ終了するのかに関心がない場合があります。次のコードはその例です。
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はラップされないため、`await` `writer.close()`の代わりに`return`を使用することもできます。
async
function
asyncFunc
()
{
const
writer
=
openFile
(
'someFile.txt'
);
writer
.
write
(
'hello'
);
writer
.
write
(
'world'
);
return
writer
.
close
();
}
どちらの方法にも長所と短所があり、`await`を使用する方法の方が少し理解しやすいでしょう。
次のコードは、2つの非同期関数呼び出し`asyncFunc1()`と`asyncFunc2()`を行います。
async
function
foo
()
{
const
result1
=
await
asyncFunc1
();
const
result2
=
await
asyncFunc2
();
}
しかし、これらの2つの関数呼び出しは順次実行されます。並列実行すると、速度が向上する傾向があります。`Promise.all()`を使用して並列実行することができます。
async
function
foo
()
{
const
[
result1
,
result2
]
=
await
Promise
.
all
([
asyncFunc1
(),
asyncFunc2
(),
]);
}
2つのPromiseを待つ代わりに、2つの要素を持つ配列のPromiseを待つようになりました。
非同期関数の制限の1つは、`await`が直接囲んでいる非同期関数にのみ影響を与えることです。そのため、非同期関数はコールバック内で`await`を使用できません(ただし、後述のように、コールバック自体は非同期関数にすることができます)。そのため、コールバックベースのユーティリティ関数とメソッドは使いにくくなります。例としては、Arrayメソッド`map()`と`forEach()`があります。
Arrayメソッド`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
;
});
}
このコードには2つの問題があります。
Promiseの配列を配列のPromiseに変換する`Promise.all()`を使用して、両方の問題を解決できます(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
);
}
URLを介して指された複数のファイルの内容をログに記録するために、Arrayメソッド`forEach()`を使用してみましょう。
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
}
これは機能しますが、1つの注意点があります。`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()`は、最初の呼び出しが終了した*後*にのみ2回目に呼び出されます。処理手順を並列実行する場合は、`Promise.all()`を使用する必要があります。
async
function
logContent
(
urls
)
{
await
Promise
.
all
(
urls
.
map
(
async
url
=>
{
const
content
=
await
httpGet
(
url
);
console
.
log
(
content
);
}));
}
`map()`を使用してPromiseの配列を作成します。その結果を解決することには関心がなく、すべてが解決されるまで待つだけです。つまり、この非同期関数の終了時点で完全に完了します。`Promise.all()`を返すこともできますが、その場合、関数の結果は、すべての要素が`undefined`である配列になります。
非同期関数の基礎はPromiseです。そのため、Promiseを理解することが、非同期関数を理解するために不可欠です。特に、Promiseに基づいていない古いコードを非同期関数と接続する場合、Promiseを直接使用せざるを得ないことがよくあります。
例えば、これはXMLHttpRequest
の「Promise化」されたバージョンです。
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はコールバックに基づいています。非同期関数を使用してPromise化することは、コールバック内から関数が返すPromiseを解決または拒否することを意味します。これは不可能です。なぜなら、return
とthrow
でのみそうできるからです。そして、コールバック内から関数の結果をreturn
することはできません。throw
にも同様の制約があります。
したがって、非同期関数の一般的なコーディングスタイルは次のようになります。
さらに読む:「Exploring 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の返却も処理されるという2つの利点があります。
JavaScriptエンジンは、処理されていない拒否に関する警告を生成するのがますます上手になっています。例えば、次のコードは過去にはしばしばサイレントに失敗していましたが、最新のほとんどのJavaScriptエンジンは、未処理の拒否を報告するようになりました。
async
function
foo
()
{
throw
new
Error
(
'Problem!'
);
}
foo
();