メモを揉めスマートフォン用の表示で見るメモを揉めお勉強の覚書。この広告は、90日以上更新していないブログに表示しています。Angularものすごい久しぶりのブログ更新になってしまいましたが、今回はJavaScriptの非同期処理について書いてみたいと思います。このテーマはうまく説明できない部分が毎回ちょいちょいあるのですが、こうしてまとめることで頭の中が整理されていくということもあるので、最後まで頑張りたいと思います。前提としてブラウザの実装の詳細や仕様については分からない部分も多いため、間違っている部分もあるかもしれません。お気づきの点などありましたら、教えていただけると助かります。余談その3同期と非同期の比較その1以下の2つの例はどちらも10msごとに1/100づつ背景の色を黒に近づけていくという処理が書かれています。同期的な実行では背景は白から黒に段々と変化するのではなく、一気に黒に変化します。それに対し、非同期的な実行では背景は白から黒へ段々と変化していき、なめらかにアニメーションします。CodePen.上の「実行」を押すと1秒ほど後に背景が突然黒くなることが確認できます。それに対し、下の「実行」を押すと背景が白から黒へとなめらかに変化することが確認できます。CodePen.この2つの挙動の違いは以下のようになります。同期的の例では、whileでループを回しつつ、その内部でDate.now()を実行し一定時間が過ぎていたら、処理を進める、というような書き方をしています。この場合whileループを抜けるまでこの関数の実行は完了しないことになります。同期的実行JavaScriptはその関数が実行されている間、他の関数が並列で実行されることは絶対にありません。1関数の実行に紐付いて画面の更新(再描画)もブロックされるため、whileループ完了後にはじめて再描画が走り、突然背景が黒くなったように見えます。これはJavaScriptがシングルスレッド2であることと、コールスタック、実行キューの仕組みを考えると説明できます。ある関数が呼ばれると図中右方向に伸びる実行キューに関数が追加されます。実行キューは左から右に順に関数を実行していき、関数の処理に入るとその関数を抜けるまで他の処理が全てブロックされます。シングルスレッドなので実行キューの関数を一つづつしか処理できないわけです。最初に呼び出した関数の中でwhileループを回しているため、背景の色を黒に近づけていくという処理は全部で101回実行されていますが、その間ブラウザの再描画はブロックされ、関数の実行が終わったタイミングで再描画が走ります。非同期的実行それに対し、非同期の例ではsetIntervalによって関数が図中右方向にスケジューリングされています。ここで言うスケジューリングとは、実行キューに関数を追加するタイミングを時間により指定しタイマーに登録することを指します。3一つ一つの処理が別の関数として実行キューに追加されているため、全体の処理がブラウザの再描画をブロックすることなく、背景の色を黒に近づけるという処理が一つ終わるたびに関数を抜け、ブラウザの再描画を走らせることができます。4このようにコールスタックに積まれるのではなく、図中右方向に伸びる実行キューへの関数の追加が行われ、一つの関数から別の関数へと処理がまたがって行われることを非同期処理あるいは非同期的実行という風に呼びます。JavaScriptの非同期処理とはまさしくこの実行キュー内の関数をまたぐような処理のことを言います。対して、最初のwhileを使った例のように、一つの関数の中で完結するような処理を同期処理あるいは同期的実行という風に呼びます。非同期的実行という呼び方をします。同期と非同期の比較その2同期と非同期の動作をより詳しく見ていくために、別の例を見てみましょう。}a(2);上のコードを実行した様子を図にしたものが下の図になります。関数の中で別の関数が呼ばれると、図中下方向に伸びるコールスタックにその関数が積まれます。積まれた関数の中で更に別の関数が呼ばれると、その関数が更にコールスタックに積まれ、と続いていきます。それ以上呼ばれる関数が無くなると、最後に積まれた関数から順に関数の実行が完了していき、最終的に一番最初に呼ばれた関数に処理が戻り、そこを抜けると、はじめてブラウザの再描画が走ります。このように、図中下方向に積まれていく処理は全て同期処理です。コールスタック内で行われる処理は、どれだけ多くの関数呼び出しが行われたとしても5、それによって時間がかかったとしても同期的に実行されます。eの内容だけ違います。aを呼び出しています。このコードを実行した様子を図にしたものが下の図になります。aに処理が移る際、図中下方向のコールスタックに積まれるのではなく、図中右方向の実行キューに登録されていることが分かります。このように、図中右方向の実行キューへと登録される処理は全て非同期処理です。全ての非同期処理は非同期APIを介して処理を実行キューへ送り込むことによって実現しています。クリックイベントのハンドリング、APIリクエストのレスポンス待ちなども全て非同期APIを介してコールバックが実行キューに送り込まれることで非同期処理が行われます。クリックイベントであれば、クリックされたタイミングで実行キューにコールバックが登録され、APIリクエストのレスポンスであれば、レスポンスが返ってきたタイミングで実行キューにコールバックが登録されます。並行処理と並列処理こうしてみると、非同期と言いつつも実際には本当の意味で別々の処理が同時に実行されているわけではないという事が分かります。メインスレッドは実行キューに登録された関数を一つづつ実行することに徹し、同時に2つ以上の関数が実行されることはありません。JavaScriptはシングルスレッドなので並行処理と並列処理で言うところの、並行処理しかできません6。非同期APIsetIntervalのようにスケジューリングを行ったり、実行キューへ関数を追加するような非同期処理を行うAPIのことを、非同期APIや非同期関数7と呼んだりします。代表的な非同期APIには次のようなものがあります。window.setTimeoutwindow.setIntervalwindow.requestAnimationFrameEventTarget.addEventListenerXMLHttpRequestwindow.fetchPromise非同期APIが使われている場合、その処理は非同期処理8となります。逆に言うと、非同期APIが使われない限り、どれだけ複雑で重い処理をしていたとしても、それは同期処理と言えます。非同期APIによく見られる特徴として以下のようなものが挙げられます。コールバックを受け取るPromiseを返すそれぞれ、後に続く処理がコールスタックではなく、実行キューに追加される挙動を連想させるものです。見られる特徴なので、コールバックを受け取ったり、Promiseを返したからといって、必ずしも非同期処理が行われるとは限りません。その他の非同期API少し脱線しますが、Zone.jsの非同期API一覧ページを見ると他にも色々なAPIが非同期処理を行っていることが分かります。github.comZone.jsはAngularの内部で使用されているライブラリで、非同期処理の内部でコンテキストを形成するためのものです。Zone.jsは非同期API全てに対しパッチを当てることで、この機能を実現しているそうです。addEventListenerに渡したコールバックの同期的実行addEventListenerは使い方によっては一つの関数のコールスタックから抜けることなく、同期処理となる場合があります。3このコードのように、イベントの発火を同期的に行うと、コールバックも同期的に呼び出されます。このことから、イベントハンドラの登録は同期的に行われており、非同期処理になるかどうかはイベントの発火タイミングが同期的実行か非同期的実行かの違いだということが分かります。非同期処理のユースケースここまで非同期処理の仕組みについて見てきましたが、今度は非同期処理を伴う具体的なユースケースを見ていきたいと思います。非同期処理が必要なシーンにはどんなものがあるでしょうか?以下に代表的なものを挙げてみます。通信のハンドリングリクエスト・レスポンスキャンセルリトライこうしてみると、何を作るにも何かしらの非同期処理が発生するということが分かります。ユーザーがキー入力をするまで、処理がすべてブロックされて画面も固まってしまう、というようなことがあっては困るわけです。これらの非同期処理を扱うための様々な方法が、これまでに発明され発展してきました。コールバック非同期で実行してほしい処理を関数として引数に渡す方法です。});一番スタンダードな方法ですが、処理が複雑になってくると管理が難しくなるという問題があります。Node.jsでは多くのAPIが非同期APIなため、少し長めの処理を書いただけでコールバックがどんどんネストしていき、コールバックヘルと呼ばれる状態になってしまったりすることがあります。hellで検索するとこんな感じの画像がたくさん出てきます🔥PromisePromiseの詳細な説明はここでは省略しますが、イメージで言うと、その時点では扱えないような値を、さも扱える状態であるかのように(またはその時点では完了していない待機中の処理が完了しているかのように)抽象化したオブジェクト、という感じでしょうか。コールバックとの違いについて考えてみます。Promiseも引数に処理を渡しているという点ではコールバックと言えます。console.log('10秒経ちました'));一番の違いは、Promiseは「さも扱える状態であるかのように抽象化したオブジェクト」であるという点です。コールバックと違い、Promiseはその時点で扱えるか分からない値(または待機中の処理が完了したという状態)をオブジェクトとして扱うことができるため、配列に突っ込んだり、オブジェクトの値にしたり、イベントのペイロードとして送ったり、自由に扱うことができます。コールバックの場合、あくまでそのコールバックの中でしか、その値が扱える(または待機中の処理が完了している)ことを保証できません。console.log('10秒経っていてかつ、document.bodyがクリックされていてかつ、windowのloadが完了した状態'));Promiseの登場により複雑な非同期処理をコントロールすることができるようになりました。複雑な非同期処理を扱うための標準的なAPIが提供されたことで、非同期処理へのアプローチが統一されたという点も大きいと思います。Promiseの本がおすすめです。azu.github.ioasync/awaitPromiseの登場でいくつかの問題は解消されましたが、PromiseのAPIはコールバックを渡すという点ではそれまでと同じように実行順序が分かりにくかったり、処理を追いにくかったりという部分がありました。async/awaitはJavaScriptの構文レベルでPromiseをサポートしてしまおう、というものです。Functionという必ずPromiseを返す関数の導入と、Promiseが解決されるまで処理を中断し、解決されたら処理を再開することができるawait演算子の導入です。この「処理を中断する」というのはこれまで説明してきた同期的実行のブロッキングとは異なり、中断した時点で関数を抜け実行キューを先に進めつつ、解決された時点で続きの処理が実行キューに登録されるといった形になります。;fn();上のコードを実行した様子を図にしたものが下の図です。APIのシンタックスシュガーと言っても良いかもしれません。上記のコードをPromiseで書くと下のようになります。;fn();Promiseは様々な非同期処理を行うための汎用的なインターフェイスとなっていますが、実際には多くのケースで、単純にその処理が終わるのを待ってから次に進みたい、逐次的に書き下せれば十分という側面があります。async/awaitはこのように非同期処理を逐次的に書くのに打って付けのシンタックスで、最近では様々なライブラリでasync/awaitを想定した利用方法が解説されています。github.comgithub.com余談であり本題のRxJSはい!ここまで長かった、ここからが余談であり本題です。JavaScriptの非同期処理についていろいろ書いてきましたが、このRxJSのことを書きたかったからなんですね。github.com自分はプロダクションでの運用経験は無いのですが、数年前に存在を知って以来9注目していて、どんな用途で使うと効果的だろう、みたいなイメージを膨らませていたりします。そんな状態ではありますが、RxJSについても触れておきたいと思います。RxJSのObservableの考え方1まずはざっくりとRxJSのイメージを掴みたいと思います。下の表はRxJSの公式ページに載っているものです。ObservableObservableはこの表にあるように、複数の値をPushすることができるPromiseと言えます。Promiseは一度値が解決(Resolved)されると、それ以降同じコールバックが呼ばれることはありません。Observableは複数回値を流すことができます。このことはよくイベントストリームであるとか、値が次々に流れてくる様子が川のようである、という風に例えられたりします。Promiseのことを「その時点では扱えないような値を、さも扱える状態であるかのように(またはその時点では完了していない待機中の処理が完了しているかのように)抽象化したオブジェクト」と紹介しましたが、Observableはそれに似ている部分があります。Observableは上述したように、値が次々に流れてくる川の流れのような時間軸を、そのまま抽象化したオブジェクトと言えるかもしれません。RxJSを使うと、その川の流れのようなオブジェクト同士を、つなげたり、束ねたり、分岐したり、フィルターしたりできます。考え方2この表も以前RxJSの公式ページに載っていたものです(記憶をもとに書いているので実際と少し違うかもしれません)。この表現もObservableのイメージを掴む上でとても役に立つと思います。Observable厳密にはObservableは同期的に値を流すこともできます。そのためおそらく表現が変わったのだと思います。面白いのはArrayとObservableの比較で、同期がArray、非同期がObservableとなっています。実際、ObservableにはArrayにあるメソッドと同名のメソッドがいくつかありますし、挙動も似ています。mapfilterreduceこの3つのメソッドはObservableにも存在します。非同期なイベントの連続を、配列を操作するかのように扱える、というイメージです。ダブルクリックのデモhttps://codepen.io/all-user/live/ppXrbdこのデモは、RxJSを使ってダブルクリックを判定する様子を可視化したものです。上から縦に4つのObservableが並んでいて、Observableを生成するコードとそのObservableに値が流れる様子を可視化しています。このRxJSのエディターを使っています。all-user.github.iomemowomome.hatenablog.comRxJSの使い所じゃあこのRxJSは一体何に使うべきなのか、という話になると思うのですが、正直本格的なアプリケーションをRxJSを使って書いた経験がないので分かりません。Mapのようなアプリケーションを実装するとしたら、RxJSを採用すると思います。ユーザーのインタラクション、画像やデータのロード、拡大縮小、座標の移動など、色々な変数が複雑に絡み合い、それぞれが非同期で発生するため、それらをハンドリングするのにRxJSが役立ちそうです。他にもゲームやエディタなどの複雑なユーザーインタラクションをハンドリングするのにも向いていると思います。AngularはこのRxJSがフレームワーク自体に組み込まれています。Angularは通常の値、Promiseに加えこのObservableがサポートされているAPIが多くあります。FiberReactがバージョン16に上がる際、Reactの内部実装がガッツリ書き直されるということがありました。具体的には仮想DOMを更新してUIに反映するアルゴリズムが変更されました。その前後の様子を比較した分かりやすい例が下のツイートにあります。Fiberをoffにすると画面の更新がカクつく様子が見て取れます。Fiberが入る前は、仮想DOMの更新を全て同期的に行っていたため、この記事でも紹介したように、その間レンダリングがブロックされてしまい、画面がカクつくようなことが起きていました。Fiberが入る前後でレンダリング時のコールスタックの様子を比較したものです。Fiber後この記事で紹介した、同期と非同期の図に似ていると思いませんか?実はこれは上の画像に似せて描いたからです。Reactの内部実装でも重い同期処理によるブロッキング問題を抱えていて、その処理を分割して非同期で処理するようにしたら改善したよ、ということがあったのです。JavaScriptの非同期処理を理解する上での一つの良い例かと思います。Fiberについての詳細は下の記事が分かりやすかったです。html5experts.jp余談その32017年11月からkurashiruを運営するdelyでフロントエンドエンジニアとして頑張らせていただいております。そのdelyで開発部LT大会というものが開催されたことがありました(もう一年以上前だった)。各々自由なテーマで話すという趣旨で、それぞれトークテーマを持ち寄りワイワイやるという感じで、とても楽しかったです。そのとき自分はJavaScriptの非同期処理とRxJSについて話したのですが、この記事はその時の資料をベースに書き足していったものです。LTの持ち時間は10分だったので、当然深ぼった話は出来ず、めちゃくちゃ駆け足かつフィーリングでこうっ!みたいな感じになってしまいました。その時話せなかったことをガッツリ書いてみようと思い立って書いてみた次第でございました。6へのアップデートを行いましたが、APIのインターフェイスが大きく変更されているのに驚きました。pipeを使った書き方は、きっとパイプライン演算子が来ることを見越した設計なんだろうなと思います。パイプライン演算子も楽しみですね!developer.mozilla.org正確にはシングルスレッドで動作している範囲においては並列で実行されることはありません。APIのような例外は除きます。https://developer.mozilla.org/ja/docs/Web/API/Web_Workers_API↩ブラウザのメインスレッドはUIスレッドとも呼ばれ、DOMの更新や画面の再描画なども行っているため、例で示したようなブロッキングが発生します。関数を実行するタイミングではなく、実行キューに関数を追加するタイミングなので注意が必要な場合があります。指定された時間に必ず関数が実行されるとは限りません。↩ブラウザの再描画を明示的に呼び出すAPIは無いため、再描画のタイミングはあくまでもブラウザ側に任されています。実際にはコールスタックに積める数には限りがあり、それを超えるとエラーになります。正確にはシングルスレッドで動作している範囲においては並列で実行されることはありません。APIのような例外は除きます。Functionのことを指して使用されている例もみるので、個人的には非同期APIという呼び方を使っています。https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/async_function↩TargetElement.addEventListenerの場合は同期処理となる場合もあります。詳細は後項を参照のこと。↩このブログへいただいたコメントがきっかけでした。ライブラリを公開しました。Swif...2016-08-03width、heightがautoの要素にtransitionを適用するCSS3のtransition。)はてなブログをはじめよう!alluserさんは、はてなブログを使っています。ブログを報告するスターをつけました引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。