今回は、ECMAScript2015(ES2015)で追加された新機能を用いてモダンな非同期処理を学んでいきます。慣れないうちは文法的に戸惑うかもしれませんが、理解してしまえば、従来よりもはるかに簡潔な記述で非同期処理を書けるようになるので、落ち着いて習得していきましょう。
Promise
Promiseとは非同期処理を抽象化したオブジェクト、およびそれを制御する仕組みのことを言います。抽象化したオブジェクトは「Promise Object」と呼ばれ、三種類の状態が存在します。
- 処理が成功した状態 Fulfilled
- 処理が失敗した状態 Rejected
- 処理が完了していない状態 Pending
この状態を深く気にする必要はないのですが、この後に用語として出てきますので頭の片隅に置いておいてください。「Promise Object」が持つ状態を把握したところで、実際のコードに触れながら理解を深めていきましょう。まずは、この仕組みの登場以前のレガシーな非同期処理を見てみます。
リスト1:ES2015以前のレガシーな非同期処理の記述
これをPromiseを用いて書き直してみましょう。新しい機能だからといって難しいことはありません。コールバック完了のタイミングで引数のresolveを実行することでFulfilledの状態にしてやり、その後「.then」を使ってコールバックの処理を記述するだけです。Promise とはその名のとおり、「約束」を表しています。約束が満たされた時は then を使って約束が満たされた後の処理を行うことができます。
リスト2:Promiseを用いた非同期処理
01 | const sleep = (delay) => { |
02 | return new Promise((resolve, reject) => { |
09 | sleep(2000).then(() => { |
Promiseを使った書き方に直したものの、この例だとPromiseを使わない方が簡潔な気さえします。Promiseの効果を実感できる例として、コールバックの中でさらに非同期処理を行い、ネストが深くなる場合を見てみましょう。
リスト3:非同期処理のネスト(レガシーな記述)
2 | console.log('2000ms later') |
5 | console.log('4000ms later') |
ネストが一段深くなり、可読性が落ちました。これを、Promiseを用いて書き換えると下記のようになります。
リスト4:非同期処理をPromiseを用いて記述する
1 | sleep(2000).then(() => { |
2 | console.log('2000ms later') |
5 | console.log('4000ms later') |
いかがでしょうか。非同期処理なのにも関わらず、一連の処理がネストせずに同レベルで記述されているので、実行順序が非常に追いやすくなっているのがわかると思います。
さて、これまでエラー処理のないものを見てきましたが、Promiseはもちろんエラー処理も簡潔に記述できます。まずはいつものように、Promiseを使わない例からみていきましょう。
リスト5:エラー処理(レガシーな記述)
01 | const url = '/api/area.json' |
02 | const responseType = 'json' |
04 | const request = new XMLHttpRequest() |
05 | request.onload = function () { |
06 | if (this.status === 200) { |
07 | successHandler(this.response) |
09 | errorHandler(new Error(this.statusText)) |
12 | request.onerror = function () { |
13 | errorHandler(new Error(`XMLHttpRequest Error: ${this.statusText}`)) |
16 | request.open('GET', url) |
17 | request.responseType = responseType |
これを「new Promise」で包んで抽象化します。と言っても実際にはerrorHandlerをrejectに置き換えてやるだけです。
リスト6:Promiseを用いたエラー処理の記述
01 | const httpGet = (url, responseType = 'json') => { |
02 | return new Promise(function (resolve, reject) { |
03 | const request = new XMLHttpRequest() |
04 | request.onload = function () { |
05 | if (this.status === 200) { |
06 | resolve(this.response) |
08 | reject(new Error(this.statusText)) |
11 | request.onerror = function () { |
12 | reject(new Error(`XMLHttpRequest Error: ${this.statusText}`)) |
15 | request.open('GET', url) |
16 | request.responseType = responseType |
こうしてhttp requestの部分を抽象化してやることで、下記のようにエラー処理の記述も簡潔になります。
リスト7:簡潔に書けるエラー処理
1 | httpGet('/api/area.json').then(data => { |
4 | console.error(`Error: status code ${status}`) |
以上がPromiseの基本的な機能ですが、他にも便利なメソッド群が提供されているので紹介したいと思います。
Promise.all
引数で与えられた全てのPromiseを並列に実行し、全てが成功した時にはじめてresolveが呼ばれます。逆に言うと一つでも失敗するとrejectが呼ばれます。個別の完了時は気にせず、すべて完了したら特定の処理を行いたい場合や、可変長つまり数が決まっていない非同期処理などを記述する時に便利です。
リスト8:Promise.allの使用例
2 | httpGet('/api/airplane'), |
Promise.race
引数で与えられたPromiseが一つでも成功したり失敗したりすると、resolveが実行されます。Promise.allは並列実行し全ての処理がresolveされたらthenで値が取れるのに対し、Promise.raceの場合、どれか一つがresolveされたらthenで値が取れる、という違いがあります。
Promise.raceはtimeoutの実装時に使えますが、どちらか一方がresolveされたとしてもそれまでの処理が中断されたりしないので、使用にはある程度注意が必要な機能です。
リスト9:Promise.raceの使用例
2 | httpGet('/api/food/beef'), |
3 | httpGet('/api/food/chicken') |
Promise.resolve
Promise.resolveには二つの役割があります。一つはresolve関数に与えた値をPromise化させることです。「jQuery.ajax()」を例にとって説明してみましょう。
リスト10:Promise.resolveの使用例(1)
2 | promise.then((value) => { |
こちらのコードを実行すると、「const promise」には「Promise Object」が入っているのを確認できると思います。このように、.thenを持っているオブジェクト、つまり「Thenable Object」を、「Promise Object」に変換することができるのです。
そしてもう一つの役割として、FulfilledなPromise Objectを即座に返す機能が挙げられます。「Array.prototype.reduce」と組み合わせて非同期処理を直列実行する時や、非同期処理の中に同期処理を混ぜて書きたい時などに有効です。
リスト11:Promise.resolveの使用例(2)
01 | const sleep = (delay) => { |
02 | return new Promise((resolve, reject) => { |
15 | const promise = tasks.reduce((prev, current) => { |
16 | return prev.then(delay => { |
25 | promise.then(delay => { |
Promise.reject
「Promise.resolve」の対となる「Promise.reject」も存在し、RejectedなPromise Objectを即座に返します。
リスト12:Promise.rejectの使用例
1 | Promise.reject(new Error("fail")).then(error => { |
4 | console.log(error) // Error: fail |
Async/await
Promiseの利便性は伝わったと思いますが、やはり.thenでネストするのは理想形とは言えません。そこで登場するのが、「Async/await」です。まずはサンプルコードをご覧ください。
リスト13:Async/awaitの使用例
01 | const sleep = (delay) => { |
02 | return new Promise((resolve, reject) => { |
09 | async function sleepAsync() { |
10 | const sleep1 = await sleep(100) |
11 | console.log(sleep1) // 100 # 100ms later |
12 | const sleep2 = await sleep(500) |
13 | console.log(sleep2) // 500 # 600ms later |
いかがでしょうか。「async function」を定義し、その中で「await 式」を実行することで、あたかも同期処理を書くかのように記述することができます。もちろん下記のようにfor loop内での使用も可能です。
リスト14:ループ内でも使用可能
1 | async function forAsync() { |
2 | for (let i = 0; i < 3; i++) { |
3 | const delay = await sleep(i * 100) |
さらにasync functionは、即時関数やアロー関数など使って書くこともできます。
リスト15:async functionを即時関数で記述
2 | console.log(await sleep(1000)) |
リスト16:async functionをアロー関数で記述
2 | console.log(await sleep(1000)) |
これが実現できるのはawaitがPromiseの解決を待ってくれるからです。Promise が解決するとasync functionは処理を再開し、解決された値を返します。
また、async functionはPromiseを返します。下記の例を見ていただくとそれがわかるでしょう。
リスト17:async functionはPromiseを返す
01 | async function sleepAsync() { |
02 | const sleep1 = await sleep(100) |
03 | const sleep2 = await sleep(500) |
04 | return sleep1 + sleep2 |
07 | const promise = sleepAsync() |
09 | promise.then(value => { |
また、「Async/await」のさらなる利点として、tryとcatch節でエラー処理をまとめられる点が挙げられます。
リスト18:Async/awaitを用いてtryとcatch節にエラー処理をまとめる
01 | async function httpAsync() { |
04 | user = await httpGet('/api/user.json') |
06 | user = await httpGet('/api/guest_user.json') |
11 | httpAsync().then(data => { |
ただし、「async function」内でのtry-catchを忘れた時はasync関数の外側でPromiseとしてcatchする必要があるので注意してください。
今回は以上で終了です。「Async/await」はNode.js v7.6.0でも使用可能となり、非同期処理のメインストリームとなっていく可能性が高いので、状況にもよりますが使用を積極的に検討していくと良いでしょう。