
2025.02.28
「経営者がAIに興奮しているかがポイント」 DeNA南場会長が語る、10人でユニコーンを作る時代とは
JavaScript編「JavaScript の ⾮同期処理 Promise、 async/await を理解する」(全1記事)
リンクをコピー
記事をブックマーク
橘ゆう氏(以下、橘):よろしくお願いします。風邪でめちゃくちゃ顔が死んでいるので、カメラオフでいきます。橘です。今日は「JSの非同期処理パターン Promise、async/awaitを理解する」というテーマについて話していきたいと思います。
簡単な自己紹介ですが、もともとDeNAにいて事業統合でそのままGOに移り、今は森下さん(森下篤氏)と同じチームで、主にサーバーサイドやMLOps周りの開発をして、たまにWebアプリの開発もやっています。
ということで、今回はJS(JavaScript)について話すことになりました。あとは技術書典にもよくいます。(スライドを示して)Twitter(現X)はあまり投稿しませんが、こんなハンドルネームとこんなアイコンでやっています。
橘:これからWebアプリの世界での非同期処理について簡単に紹介してから、今日の本題に入っていきたいと思います。
まず、この発表の内容はPCのモダンなWebブラウザ上で動くJSやJSエンジンの話に限っていて、Node.jsとか携帯端末など他のJS環境には触れません。
さて、そのブラウザ上のJSの世界はどんな世界かというと、ユーザーの操作が非常に多くて、突発的なイベントがすごく多く、いろいろな外部APIとの通信が多い世界です。
いつ来るかわからない大量のイベントと、いつ終わるかわからない外部データ通信。これらに素早くスムーズに対応する必要があって、それに加えて非同期処理という概念が今は非常に活躍する世界です。というか、非同期処理がないとキツイと言ってもいいです。
(スライドを示して)ちなみに自分が業務で開発した、めちゃくちゃ簡単な地図アプリがあるんですが、このアプリも非同期処理で溢れています。ユーザーのあらゆる動作によって地図とインタラクトができていて、例えば地図の座標とかに飛ぶ。その操作に対して、地図の画像データをAPIから取ってくる必要があるんですね。
実はそれがけっこう細かくて。例えば今表示されているエリアだけで数十個とか、場合によっては100個以上の画像データを取ってくる必要があります。これらをバックグラウンドで常に非同期で取得していて、読み込み次第表示しています。
それらを全部同期で取ってきたとしたら、地図を読み込んでいる時はユーザー操作ができなくて、ユーザーが操作している時は地図が読み込まれない。アプリとして成り立たなくなります。非同期処理の必要性が非常に痛感できる例ですね。
橘:非同期処理が必要で仕方ない環境なんですが、ブラウザ上のJSは実はシングルスレッドでしか動作しないという難点があります。シングルスレッドで、なにも工夫しないと、普通にデータ待ちやイベント待ちで画面が固まってしまって、誰もうれしくないです。
こういう環境では並列処理などはできないと言いつつ、命令の呼び出しのタイミングなどを工夫することで、非同期処理はシミュレートはできます。「疑似的な非同期処理」と呼んでいる概念です。
橘:では、この疑似的な非同期処理はブラウザやJSエンジン上でどうやって実現しているかというと、まずブラウザのJSランタイムには、queue、イベントループ、Call stackの主に3つの構成要素があります。厳密にはヒープとか、他の部分もいろいろあるんですが、この話にはあまり関係ないのであえて触れません。
queueは一般的なデータqueueタイプと同じFIFOで、実行する命令を順に入れていくものです。イベントループは、データ構造というよりqueueを監視して、よしなにCall stackに命令を積むためのプログラムです。(スライドを示して)右上のコードのように、無限ループの中でqueueにメッセージが来ていないかを確認しています。
そしてCall stackはLIFOで、すぐ実行するための命令が積まれていて、上から順に実行されていきます。即時実行と遅延実行で命令を積む場所、パスが分かれていて、同期処理の命令、例えば右側のコードの最初と最後の行はそのままCall stackに追加されてすぐ実行します。
この中間の2つの行の非同期処理の命令はqueueに積まれて、あとで実行できるようになっています。例えば通信の待ち時間の中でも他の処理を進めることができます。基本的にこういうふうに疑似的非同期を実現しています。
橘:これまではブラウザ上でのJSの仕組みとか、どうやって非同期っぽいことを実現しているかを説明してきましたが、これからはその非同期処理をどうやって実現してきたか、歴史にしたがって現在までのやり方を説明していきたいと思います。この話は主に3つのフェーズに分けて話していきます。
まず標準化されていない非同期ライブラリでがんばっていた時代がありました。もともと初期のWebページは静的なコンテンツが中心で、多くの機能を持ったり、多くの内容を表示したりとかをそんなにしていなくて、わりと同期処理でなんとかなっていた時代がありました。
ユーザーがWebアプリに求める機能や利便性とかのニーズが徐々に高まって、例えば画面遷移せずに画面の一部だけ表示をするなど、Ajax通信が必要となりました。Ajax通信の実現に非同期処理が必要になって、それでさまざまな非同期ライブラリが登場しました。(スライドを示して)これらのライブラリは、赤い枠のような、コールバック関数をいろいろな非同期処理に使っていました。
そこで生まれた2つのつらみポイントがあります。まず、コールバック地獄という現象です。コールバック関数をいろいろな処理に使うので、次々とチェーンする必要もあったりして、コードの階層構造がけっこう早く、非常に深くなっていくことがありました。これは後ほど具体例を見せますね。
そして非同期ライブラリはどんどん数が増えていて、コールバックの使い方とか、引数の指定の仕方とか、エラー処理の仕方とか、いろいろなものの書き方が揃っていませんでした。
例えば、成功と失敗の処理はライブラリによって書き方が違っていて、まちまちです。さらに、ライブラリによって中間的なステップのインターフェイスがあったりなかったりして、コードがわりと読みづらい世の中がありました。こういうつらみポイントもありました。
(スライドを示して)まずコールバック地獄が、具体的にどういう状況かというと、こんな感じのものですね。この4つの非同期処理をチェーンしたいだけで、こんなにネストが深くなっていきます。ここにエラー処理を加えると、さらにさらに深くなります。
橘:これらの問題をどうにかならないかというニーズに対して、Promiseオブジェクトというものが登場しました。Promiseが何かというと、非同期処理が完了した時に結果やエラーを返す、もしくは表すJSのオブジェクトです。
コンストラクターに正常時とエラー処理の関数を渡していて、(非同期処理を)呼び出す時に正常終了・エラー終了の時に呼ぶインターフェイスも用意されています。resolve、reject、then、catch。このPromiseというやつによって、上記の2つの問題はだいぶ解決しました。
例えばコールバック地獄ですが、Promiseのthen、catchのインターフェイスのおかげでどうにかなりましたね。非同期関数をthenで順にチェーンするだけで書けるようになって、先ほどのような深いネストはまずいらなくなりました。いくらチェーンしてもコードの構造が深くならず、読みやすいままですね。
書き方がまちまちな問題も解決してくれて、正常終了と異常終了のケースを超えて共通の書き方に統一したことによって、コードが読みやすくなりました。
あとはエラー処理が書きやすくなって、前は各コールバックの中でif elseをいっぱい積むことが必要でしたがそれも必要なくなって、チェーンしている動作の最後にcatchをチェーンするだけで、そのあとすぐにあるPromiseのどれかにエラーが起きていてもcatchして処理できるようになりました。すごく簡潔に書けるようになりましたね。
PromiseはES6以降はJSの標準仕様になっていて、これを使っているライブラリもいっぱい増えたから、読みやすいコードが世の中に増えて。幸せなことですね。
橘:さらにPromiseをわかりやすくした概念が出てきました。async/await構文です。実は厳密に言うと、これはただのPromiseのsyntactic sugarで、裏でバリバリPromiseで動いているんですね。それによってPromiseのインターフェイスを隠蔽して、インターフェイスが表に出なくなったことによって、thenやcatchすら書く必要がなくなったんです。
(スライドを示して)例えば、Promiseを使った書き方だと左側のコードのようにthenを変えていたんですが、ちょっと複雑に見えますよね。右側のasync/awaitではthenを完全になくすことができていて、右を見ていると非常に同期っぽく見えます。
実はこれはasync/awaitの強みの1つで、非同期処理を同期処理みたいに順に書けるようになっていて。読むとすぐに何をどの順でやっているかわかります。
async/awaitの戻り値はすべてPromiseなので、呼び出しの時にawaitすると「結果を待ってから次の処理を実行しましょう」という制御ができます。fetchDataの関数の中でfetch(url)、response.json()がawaitされているので順に実行されますが、async関数自体を呼び出している側の関数では、この関数の非同期で処理されています。awaitされないと、ただのPromiseと同じようにasync関数が非同期で処理されます。
async/awaitのもう1つの良さは、例外処理がさらに楽になったことです。普通の同期処理の関数で使っているようなtry catch構文を使って、Promiseでやっていたようなresolve、rejectなどの処理すら書く必要がなくなりました。めちゃくちゃ簡単ですね(笑)。
最後に、橘の所感をいくつか話します。まずPromiseを使うならPromise.all()というさらに便利な書き方があります。同時にいくつかのPromiseを取ってきて、同じ処理の中で使いたい時にこのPromise.all()を使うと……。
例えば2番目のPromiseのほうが一番時間がかかっているもので、2番目のPromiseが終わったタイミングで全員分の結果が返ってきて、わりとすごく簡潔に書けるものがあります。
あと一番の感想は、async/awaitの圧倒的な利便性のことですね。同期っぽく上から順に書ける。あと、実は私はコールバック地獄とかPromiseをバリバリ最初から自分で書くことはあまりなくて、最初からasync/awaitを使っていたので、このJS非同期処理のいろいろなつらみを自分で踏むことなんかせずに済み、非常に恵まれている開発者生活をしています。先人たちに非常に感謝です。
最後に、他の発表にもあったように、このテーマについてもっと読みたい場合は『Tech It Up』に先輩の森下さんが記事を出しているので、ぜひご参照ください。
私からは以上です。ご清聴ありがとうございました。
司会者:橘さん、ありがとうございました。質問がいくつか来ています。こちらの3つですね。どうチョイスしましょう。あ、増えた(笑)。
橘:「つらみポイントわかりみ」。ありがとうございます。わかりみですね。
森下篤(以下、森下):感想がいくつか来ていますね。
司会者:そうですね。どれにいきますかね。
橘:「JavaScriptを実行できるブラウザは、スマホ、IE、Chromeなど複数の環境がありますが、すべてのブラウザでasync/awaitは使えるのでしょうか」。ごめんなさい。自分はChromeでしか書いたことがなくて。森下さん、わかったりします?
森下:現行で使われている、サポートされているブラウザでは使えます。async/awaitはほぼ使える状態だと言っていいでしょう。ただ、IEと書いてあるのが難点で、IEはPromiseすら使えないので、async/awaitも使えなかったりします。
橘:そうですね。モダンなブラウザではない(笑)。
森下:基本的には現行でサポートされているブラウザはasync/awaitをサポートしているので、使えると思っていいと思います。
橘:ありがとうございます。「ジェネレーター関数function*やyieldキーワードにも触れても良かったかも」。この内容は、本当は触れたかったんですよ。ただ時間が10分しかなかったのであえてというか。申し訳ないです。これも非同期処理に関する非常に根が深い概念です。
司会者:3つ目です。「JavaScriptはシングルスレッドしか使えないと、できないことが多いのでしょうか?」。こちらはいかがでしょう?
橘:これは森下さんわかったりします? できないことがあるかどうか。
森下:基本的にはそんなに困ることじゃ……。JavaScriptの世界でCPUをめちゃくちゃ使ってがんばることはそこまでないと思うので、実際のブラウザの中で描画をするというのは、JavaScriptのスレッドとは別の世界で実行されているから、そこの負荷通信を別に切り出すことができれば、基本的なシングルスレッドで問題ないということが多いと思います。ですが、CPUをめちゃくちゃ使おうと思うと、ハマるとは思います。
司会者:ありがとうございます。橘さんパートは以上としたいと思います。森下さんもありがとうございました。
橘:ありがとうございました。
関連タグ:
2025.02.27
「頭のいい人」がやらない行動7選 職場でできる人に見られる、「心の知能指数」を高めるポイント
2025.02.25
仕事ができる人は「2分ルール」で動く 今すぐできる“シゴデキ”しぐさ10選
2025.02.21
日本の職場にありがちな「指示待ち部下が多い」問題 ドイツのマネージャーに学ぶ、自律型組織のヒント
2025.02.25
チームの士気を下げる「難しいですね」が口癖の人 職場で評価される人の言い換え方
2025.02.20
残業せずに結果を出すドイツ人の働き方 日本の3倍休んで成果は1.5倍のタスク管理と会議術
2025.02.26
フリーターから年収1億近く…『移動する人はうまくいく』著者の人生遍歴 そこそこ良い会社に就職するより経済的に成功する考え方
2025.01.07
1月から始めたい「日記」を書く習慣 ビジネスパーソンにおすすめな3つの理由
2025.02.20
部下からミスの報告が上がった時の反応の仕方 逆効果になる上司の対応と心がけたい3つのポイント
2025.02.18
「働きやすさ」と「やりがい」で見た4つの職場のパターン 圧倒的に業績の伸び率が高い職場の特徴
2025.02.21
提案がスイスイ通るようになる“思考の順番” 顧客の問題解決に直結する4つのプロセス
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: