※本編は続きものとなっている&Part1から話が繋がっております。
Part1はこちら。
はじめに
こんにちは、筆者です。
前回の記事では「JSの非同期処理の仕組みをまとめてみる」と題し、
- JavaScriptの非同期処理の種別
- JavaScriptにおける非同期処理について
- 非同期処理その1
setTimeout
について
…を見ていきました。
今回は「非同期処理その2Promise
について」をまとめていこうかと。
それではいきましょう٩( ‘ω’ )و
非同期処理その2Promise
について
非同期処理の仕組みの話…の前に、まずはPromise
自体の簡単な説明から。
※既にご存知な場合はPromise
の非同期処理の仕組みからお読みください
Promise
とは
Promise
とは何か、MDNさんでは冒頭にこう書かれています:
Promise オブジェクトは、非同期処理の完了 (もしくは失敗) の結果およびその結果の値を表します。
〜中略〜
プロミス (Promise) は、作成された時点では分からなくてもよい値へのプロキシーです。
非同期のアクションの成功値または失敗理由にハンドラーを結びつけることができます。
これにより、非同期メソッドは結果の値を返す代わりに、未来のある時点で値を提供するプロミスを返すことで、同期メソッドと同じように値を返すことができるようになります。
Promise
は下記の3つの状態を持ちます:
・待機 (pending): 初期状態。成功も失敗もしていません。
・履行 (fulfilled): 処理が成功して完了したことを意味します。
・拒否 (rejected): 処理が失敗したことを意味します。
参考: Promise - JavaScript | MDN
上記をコードを元に解説していくとこんな感じです:
function foo(){ console.log("foo"); // 状態が履行(fulfilled)になった=処理が成功したということをPromise側に伝達 // この処理の後に`resolve`で指定されたコールバック関数が実行される // `resolve`には引数を1つ指定可能、コールバック関数に伝達できる // もしこれが`reject('foo')`であれば、処理が失敗したということをPromise側に伝達することになる // なお、本関数を直接実行すると「Promiseの世界の中にいる`resolve`が存在しない」ためエラーとなる resolve("test"); } // Promiseを作成(関数の定義と同じで、この段階では中身の処理は実行されない) const myPromise = new Promise(foo); // ここでPromise内部の処理を実行 // `.then`でPromise処理が完了した後(状態が待機(pending)以外の時)に実行したい処理を記す事ができる // →第1引数:処理成功時(状態が履行(fulfilled))に実行したい処理 // →第2引数:処理失敗時(状態が拒否(rejected))に実行したい処理 // Promise内部の処理では`resolve`を返しているので、この後に`handleResolvedA`が実行される myPromise .then((result)=>{console.log(result+"success!")}, ()=>{console.log(result+"failed!")}); console.log("test"); // 出力は`test`→`foo`→`testsuccess!`となる
これまでお話ししたsetTimeout
と大きく異なる点としては、「秒数を指定して遅延というのができない」点です。
ただし、上記コードにある様にPromise
は.then
をくっつけることで「Promise
内部で処理された関数を実行した後の処理」を設定できます。
これがPromise
の強みとなっています。
また.then
自体もPromise
オブジェクトなので、ES6以前ではよく見かけた下記の様なコールバック地獄を:
doSomething(function(result) { doSomethingElse(result, function(newResult) { doThirdThing(newResult, function(finalResult) { console.log('Got the final result: ' + finalResult); }, failureCallback); }, failureCallback); }, failureCallback); // コード引用: https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Using_promises#%E9%80%A3%E9%8E%96
下記の様にすっきりと書く事ができます。
doSomething() .then(function(result) { return doSomethingElse(result); }) .then(function(newResult) { return doThirdThing(newResult); }) .then(function(finalResult) { console.log('Got the final result: ' + finalResult); }) // `.then`の第2引数しか受け付けないバージョン // この場合だと、上記実行中のどこかでエラーとなった場合に下記が実行される .catch(failureCallback);
実際の現場では、API通信処理にこのPromise
が使われる事が多いかなと。
※具体的な使用例: Twitter APIを使ってツイート情報を取得し、その取得データをconsole.log()
で出したい
API通信は「API通信が成功した後のデータ」をもとに後続の処理が実行されるので、Promise
の様に「〇〇の処理をした後の処理を定義できる」のと相性がいいのです。
また、API通信失敗時もreject
を返却&.catch
でエラー時の処理を書くこともできるので最高です。
実際のコード例はこんな感じかなと:
// fetchは元々Promiseオブジェクトを返す処理 // API通信が成功すれば(400エラーや500エラーも含む)`resolve`、ネットワークのエラーやリクエストが妨害された際は`reject`を返す fetch('https://hoge.fuga') // 成功時: APIデータをConsole上に出す .then(response => console.log(response.json())) // 失敗時: エラーデータをConsole上に出す .catch(err => console.log(err))
余談:
下記の様に、.then
にコールバック関数を指定しないとどうなるか.then
にコールバック関数を指定しなくても:
const promise1 = new Promise((resolve, reject) => { resolve('Success!'); }); promise1.then();
JavaScriptエラーとはなりませんし、処理も正常終了します。
これについてはMDNさんの記述を参考に:
片方または両方の引数が省略されたり、関数ではないものが渡されたりした場合、 then にはハンドラーが不足しますが、エラーは発生しません。
Promise が状態 (fulfillment (完了) または rejection (拒否)) を受け入れるに当たって then が呼び出された際に、 then がハンドラーを持たない場合は、 then が呼び出された元の Promise の最後の状態を受け入れた、追加のハンドラーのない新しい Promise が生成されます。
参考: Promise.prototype.then() - JavaScript | MDN
ちなみに…。
.catch
についてもコールバック関数を指定しないでの実行は可能かどうかやって見たところ、特にエラーは出ませんでした。
サンプルコード:
const promise1 = new Promise((resolve, reject) => { throw 'Uh-oh!'; }); promise1.catch();
Promise
オブジェクト自体はしっかり「状態が拒否(rejected)」になっていたので、おそらく.then
と同様だと思いますが…。
MDNさんの公式ページにその様な言及はなかったため、そうとは確証はできないかもです。
…ちょっと長くなってしまいましたが、Promise
の説明はここまで。
お次はいよいよPromise
の非同期処理の仕組みについて見ていきます。
Promise
の非同期処理の仕組み
今回もサンプルコードをもとに処理を追っていきます。
setTimeout(function() { // (A) console.log('A'); }, 0); // ① Promise.resolve().then(function() { // (B) console.log('B'); }).then(function() { // (C) console.log('C'); }); // ② console.log('D'); // ③ // コード引用: https://meetup-jp.toast.com/896
setTimeout
も入った上記サンプルコードですが、出力される順序は「D->B->C->A」となります。
setTimeout
の待機時間は0
ですが、Promise
の方が早く実行されます…この理由はPromise
が「マイクロタスク」というものを使用するからです。
順に説明していきます。
まず、前回記事のおさらいから…。
(1)の時点でsetTimeout
自体はすぐに実行され、Timer側で「コールバック関数をタスクキューに入れる」という処理が実行されます。
※0秒を指定しているので、この処理はすぐに実行されます
そして(2)の時点ですが…。
Promise
は即座にresolve
されるので、Promise
の状態は既に履行(fulfilled
)となっています。
あとはその後のコールバック関数を実行するだけですが、このコールバック関数はマイクロタスクキューというタスクキューとは別のキューに格納されます。
その後(3)まで到着し、console.log('D');
が即座に実行され…。
setTimeout
だけであればお次はconsole.log('A');
が実行されます…が。
イベントループはタスクキューよりもマイクロタスクキューを優先して処理するため、先にPromise
側の処理が実行されます。
その処理実行後に再度イベントループが発動した際も、イベントループは引き続きマイクロタスクキューを優先するため.then
内部の処理が実行され…。
最後にタスクキューに格納されているsetTimeout
側の処理が実行され、処理は終了となります。
この「イベントループはタスクキューよりもマイクロタスクキューを優先して処理する」というのが、Promise
の方がsetTimeout
より早く実行されるメカニズムとなります。
小咄:マイクロタスクとマクロタスク
本記事でPart1より話しているタスクキューについてですが…。
マイクロタスクのお話が出ている記事では、タスクキューは「マクロタスクキュー」と呼ばれていました。
従来のタスクキューと区別するためにそう呼ばれているのかなと…。
ES6以前のお話や広義の意味でのお話をするなら「タスクキュー」という言葉で通用しそうですが、それ以降ではきちんと「マイクロタスク」と「マクロタスク」というふうに用語を切り分けた方が良さそうです。
小咄:以前の
Promise
はブラウザによって処理順序が異なった
Promise
について調べていると、興味深い記述があったので引用させていただきます:
原文では、ブラウザ毎にプロミスの呼び出し手順が異なると指摘されていますが、その理由は、プロミスがECMAScriptに定義されているのに対し、マイクロタスクはHTMLスペックに定義され、両方の関連付けが明確でないためです。
(ECMAScriptはES6からプロミス向けにジョブキュー(Job Queue)という項目が追加されましたが、HTMLスペックのマイクロタスクとは別の概念です。)
しかし、最近のLiving Standard状態であるHTMLスペックを見ると、JavaScriptのジョブキューをどのようにイベントループと連動するかについての項目が含まれています。
また現在では、ほとんどのブラウザで問題が修正されていることを確認できます。
(プロミスA+スペック文書を見ると、実装時に一般タスク、マイクロタスクの両方とも使用できていると書かれています。
実際にプロミスが初めてJavaScriptに導入された時点では、プロミスをどのような手順で実行するかについて多く議論されたようです。
しかし前述したように、現在ではプロミスをマイクロタスクと定義しても無理がなさそうです。)
マイクロタスクか一般タスクかによって実行されるタイミングが異なるため、両方を正しく理解し、区別して使用することが重要です。
例えば、マイクロタスクが継続して実行される場合、一般タスクであるUIレンダリングが遅延する現象が発生することもあるでしょう。
参考: JavaScriptとイベントループ | NHN Cloud Meetup
なお、上記に書かれている「ブラウザ毎にプロミスの呼び出し手順が異なる」という話題ですが…。
こちらの記事の真ん中くらいに当時の各ブラウザごとの違いが載ってます: Tasks, microtasks, queues and schedules - JakeArchibald.com
※引用文にあった「原文」というのも、上記記事のことを指します
筆者も見てみましたが、各ブラウザで恐ろしいほどに異なっていたので…。
きちんと整理されて統一化されたのは安堵しかないですね…。
まとめ&おわりに
本記事では、Promise
での非同期処理の仕組みについてまとめました。
今回も色々話してしまいましたので、軽くまとめておきます:
Promise
はsetTimeout
とは異なる形で非同期処理を記述可能Promise
はsetTimeout
よりも早く実行されるPromise
の処理はタスクキュー(マクロタスクキュー)よりもイベントループでの参照優先度が高い「マイクロタスクキュー」に入れられる
また、余談となりますが…。
Promise
の様に時が来たらマイクロタスクに処理が突っ込まれるのは他にもあります。
例えばMutationObserver
もマイクロタスクに処理が突っ込まれるものの様です。
この辺りの理解が深まれば「JavaScriptでなんかうまく動かない…」という時に参考になるかもしれません。
また、なんと「マイクロタスクキューに直接処理を登録する」なんてこともできちゃいます。こんな感じ:
queueMicrotask(() => { log("The microtask has run.") }); // コード参照: https://developer.mozilla.org/ja/docs/Web/API/HTML_DOM_API/Microtask_guide#examples
詳しくはこちらの記事を参照に: JavaScript で queueMicrotask() によるマイクロタスクの使用 - Web API | MDN
…とまぁ色々語りましたが、ひとまず本記事の本編はここまでとしたいと思います。
自分自身のJavaScriptの理解も数段深まった、良いテーマでございました…_:(´ཀ`」∠):
それでは|д゚*)
参考記事
今回の記事も下記の記事をもとに、自分なりに理解しまとめたものになります。
もし本分野を深掘りしたい場合は、下記の記事がとても参考になるかと思います。
- JavaScriptとイベントループ | NHN Cloud Meetup
- Tasks, microtasks, queues and schedules - JakeArchibald.com
- イベントループ(event loop): microtask と macrotask
- プロミスの使用 - JavaScript | MDN
- JavaScript で queueMicrotask() によるマイクロタスクの使用 - Web API | MDN
- Promise - JavaScript | MDN
- Promise.prototype.then() - JavaScript | MDN
- https://wiki.suikawiki.org/n/%E3%83%9E%E3%82%A4%E3%82%AF%E3%83%AD%E3%82%BF%E3%82%B9%E3%82%AF
余談:async
,await
について
本題から逸れてしまうと思い言及を避けましたが…。
Promise
について話すのであれば、async
,await
もせっかくだし言及したいなと思い余談として設けました。
async
,await
はざっくりいうと「Promise
を簡単に使えるためのもの」です。
例えば下記のPromise
の処理は:
fetch('coffee.jpg') .then(response => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.blob(); }) .then(myBlob => { let objectURL = URL.createObjectURL(myBlob); let image = document.createElement('img'); image.src = objectURL; document.body.appendChild(image); }) .catch(e => { console.log('There has been a problem with your fetch operation: ' + e.message); }); // コード引用: https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Async_await
async
,await
を使うと、こんなふうに超直感的に書けちゃいます。
async function myFetch() { let response = await fetch('coffee.jpg'); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } let myBlob = await response.blob(); let objectURL = URL.createObjectURL(myBlob); let image = document.createElement('img'); image.src = objectURL; document.body.appendChild(image); } myFetch() .catch(e => { console.log('There has been a problem with your fetch operation: ' + e.message); }); // コード引用: https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Async_await
なお、async
,await
モダンブラウザであれば大概使用できますが…。
微妙に古いバージョンだと「Promise
は使えるのにasync
,await
は使えない」バージョンのブラウザもあるのでお気をつけて。
※詳しくはこちら参照:
とりあえず使い方だけ覚えたい場合は、下記を押さえておけば問題ないです:
※筆者はこの理解でasync
,await
を使ってました
async
は関数につける接頭詞として使え、この中ではawait
が使えるawait
はasync
関数かPromise
を返す関数の前に使用できる接頭詞await
をつけると、それ以降の処理はawait
の行が完了した後に実行される
…悪い点としては、先にasync
,await
を覚えちゃうとPromise
の理解が覚束なくなる所でしょうか…。
実際筆者も先にこれらを覚えた口であり、ぶっちゃけ本記事をまとめるまでPromise
の理解が浅いままでした…orz
async
,await
について詳しく知りたい場合は、下記の記事をご参考に:
How to use promises - Learn web development | MDN