【Javascript】 非同期処理
Javascriptでは、Promise
やasync
とawait
を利用した非同期処理が可能です。しばしばHTTP通信に使われます。
ここで注意点として、非同期処理と並列処理は異なります。非同期な処理は、特定の手が空くタイミングで処理するというものです。
目次
非同期処理が使われている例
例えば、http通信はよくあるJavascriptの非同期処理の例です。
import axios from "axios"; axios.get("https://example.com").then(res => { console.log(res.data); }); console.log("foobar");
foobar (https://example.com のHTML)
このプログラムは、https://example.com
のHTMLを取得するプログラムです。axios.get
の戻り値はPromiseのオブジェクトなので、.then(data => {...})
で結果を取得しています。
実行結果を見ると、HTMLの取得より次の行のfoobar
が先に実行されています。これが非同期処理の一例です。
このプログラムを最小構成で動かすには、Node.jsを導入し、npm i axios
をしてからNode.jsで動作させる必要があります。
また、非同期処理は特定のタイミングで実行されるので、処理によってはなかなか非同期処理が始まらない場合もあります。
特定のタイミングというのは、Javascriptでは各関数またはグローバルスコープでの実行終了後です。
const axios = require("axios").default; // 下のfor文が終わらないと始まらない axios.get("https://example.com").then(res => { console.log(res.data); }); for(let i = 0; i < 5000000000;i++){ if (i % 500000000 == 0) { console.log("hgoe"); } }
Promise
async
, await
の説明の前に、まずはPromiseを理解するべきです。
Promiseは、非同期処理をして結果を返すものです。と言われても想像がしづらいので例を見て考えていきましょう。
Promise成功時の処理
function f() { return new Promise((resolve, reject) => { resolve(1); }); } // Promiseのインスタンスのthenメソッドを使う f().then(data => { console.log(data); });
1
ポイントはnew Promise
をしているところです。Promise
の引数には関数を入れ、その関数の引数はresolve
とreject
です。
コールバック関数が多く出現します。詳しくはコールバックの章を確認してください。
hoge().fuga().hige()
のような書き方をメソッドチェーンと呼びます。
resolve
は関数で、Promise
内でresolve
を呼ぶとPromiseの処理は成功したということになります。逆に、reject
を呼ぶ、または例外が排出されると失敗したということになります。
成功したデータは、Promiseのインスタンスに.then(data => {...})
とすると取れます。これは上の例の通りです。上の例では、そのインスタンスがf
の戻り値なので、f().then(...)
としています。
Promise失敗時の処理
rejectの例もみてみましょう。
function f() { return new Promise((resolve, reject) => { reject("err"); }); } // Promiseのインスタンスのcatchメソッドを使う f().catch(err => { console.error(err); });
err
reject
を呼ぶとPromiseは失敗扱いになり、その時はcatch
が呼ばれます。
次に、then
とcatch
をつなげたパターンを見てみましょう。
let x = 0; function asyncf(resolve, reject) { if (x == 0) { resolve("x is 0"); } else { reject("Err!! x is not 0"); } } function f() { return new Promise(asyncf); } // Promiseのインスタンスのthenとcatch両方を使う f().then(data => { console.log(data); }).catch(err => { console.error(err); });
x is 0
この場合、Promiseが成功すればthen
が、失敗すればcatch
が呼ばれます。
catchを書かなかった状態で失敗した場合
例外がスローされます。
function f() { return new Promise((resolve, reject) => { reject("err"); }); } // Promiseのインスタンスのthenを使う f().then(data => { console.log(data); }); // Uncaught (in promise) rejected
例外に対応するため、then
とcatch
は必ず両方書きましょう。
try-catch
でこれに対応することはできません。.then
や.catch
が呼び出されるのは、非同期処理が走るタイミングであり、その時点ではもうtry
ブロックの処理が終わっているためです。
finally
then
とcatch
以外にfinally
というものもあります。これは、Promiseの成功失敗に関わらず実行されるものです。
let x = 0; function asyncf(resolve, reject) { if (x == 0) { resolve("x is 0"); } else { reject("x is not 0"); } } function f() { return new Promise(asyncf); } // Promiseのインスタンスのthenとcatch両方を使う f().then(data => { console.log(data); }).catch(err => { console.error(err); }).finally(() => { console.log("finally実行"); });
x is 0 finally実行
通信中のフラグを戻すなど、使い所はいくつかあります。
Promiseは待てない
Promiseしたものは非同期で実行されますが、場面によっては同期的に動いてほしい場面が多くあります。例えば、Web上のAPIをたたき、結果を取得してから先の処理を進めたい場合などです。
const axios = require("axios"); let data = null; axios.get("https://example.com").then(res => { data = res.data; }); // ここにdataの処理を書きたい!
1つは、この後紹介するasync
, await
を利用する方法です。もう一つは、メソッドを重ねることです。
const axios = require("axios"); let data = null; axios.get("https://example.com").then(res => { dataProcess(res.data); }); function dataProcess(data) { // ... }
しかし、この方法はメソッドの階層が深くなる、過剰にメソッドが増えるなどの問題があります。
resolveやrejectを呼ぶタイミング
これを呼ぶのは、Promise内に入れた関数の処理が走りきってからでも問題ありません。つまり、setTimeout
などで後から呼び出しても良いというわけです。
function f() { return new Promise((resolve, reject) => { setTimeout(() => { resolve("1秒後にresolve"); }, 1000); }); } console.time("await time"); f().then(data => { console.log(data); console.timeEnd("await time"); });
1秒後にresolve await time: 1001.1640625 ms
結果の通り、1秒経ってから.then
が実行されていることが分かります。
asyncとawait
Promiseの記法を省略したのがasyncです。
// new Promiseの代わりに, 関数の頭にasyncを付ける async function f() { return "resolved"; } // ここは普通に呼び出す場合は変化なし f().then(data => { console.log(data); }).catch(err => { }); console.log("foobar");
foobar resolved
function
や、() => {...}
の前にasync
を付けると、その関数は非同期処理を実行することができます。
クラスのメソッドにもasyncを付けることができます。同じようにメソッド名の前にasync
を付けます。
このプログラムは, 直前の節で紹介したプログラムと同等です。つまり、
async function f() { return "resolved"; }
function f() { return new Promise((resolve, reject) => { resolve("resolved"); }); }
の2つは同等です。
行を増やしたパターンを考えると、
async function f() { for (let i = 0; i < 10; i++) { // ... } return "resolved"; }
function f() { return new Promise((resolve, reject) => { for (let i = 0; i < 10; i++) { // ... } resolve("resolved"); }); }
の2つが同等になります。
await
awaitは、asyncの処理が終わるのを待つというものです。つまり、同期的に実行できます。
async function f() { return "resolved"; } async function f2() { const res = await f(); console.log(res); console.log("foobar"); } f2();
resolved foobar
f2
内に処理を入れたのは、まだトップレベルでawait
できないためです。
次期ECMAScriptではこれが可能になる可能性があります。
注意点として、await
はasync
な関数内でしか利用できません。非常に重要なので今すぐ覚えておきましょう。
この実行結果とコードから、async
な関数をawait
を付けて呼び出すと、あたかも同期処理のように実行できる事がわかります。
await時のcatch
await
をつけると同期実行されるので、then
などが不要になります。このとき、reject
されるとどうなるかというと、例外が排出されます。
function f() { return new Promise((resolve, reject) => { reject("rejected") }); } async function f2() { try { const res = await f(); console.log(res); // 実行される前にthrowされる } catch (err) { console.error(err); } } f2();
rejected
つまり、await
を使えば.then(data => {...})
のdata
は普通の戻り値に、.catch(err => {...})
は普通の例外処理に、ということです。
async内でrejectする
お察しの方はいるかもしれませんが、asyncの関数ではresolve
とreject
は使えません。
async
な関数内でreturn
すると、それはresolve
と同等になるということはこれまでのプログラムからわかりますが、reject
するにはどうすればよいでしょうか。
これは、例外をthrowすればよいです。
async function f() { throw "rejected"; // throwは, Promise内でrejectを呼んだのと同等 } async function f2() { try { let res = await f(); } catch (err) { console.error(err); } } f2();
rejected
これで、async/awaitとPromiseの置き換えは把握できたのではないでしょうか。
まとめ
まず、非同期処理と並列実行は異なります。
Promiseでは、指定したコードが特定のタイミングで自動で動作し、その動作結果をPromiseのインスタンスに.then
と.catch
をつなげて処理します。
async
とawait
は、それを短縮し、await
を利用すれば同期的な処理で書けるようになります。
JavascriptではしばしばAPI通信などをするので、非同期処理の動きは知っておくと必ず役に立ちます。
