こんにちは。株式会社朝日ネット開発部のxfuzzyです。 JavaScriptで非同期プログラムを書くためのasync/awaitについて考えてみました。 また、プログラムを使って同期的なコードを非同期なコードに変換する方法も考えてみました。
async/await とは
非同期処理をするためのJavaScriptの機能としてPromiseがあります。さらに、Promiseを扱いやすくするためにES2017で導入されたのがasync/awaitです。
C言語などの言語では、scanf等の関数の実行中に、キーボード入力を待ったりすることができます。scanfは関数の実行中に入力待ちを行い、入力があったら関数の実行が終わるようになっています。一方、JavaScriptでI/Oを待つ際は一般に、関数の実行を終わらせた後で、I/O待ちを行い、I/Oがあったらコールバックという仕組みで処理を行います。
この、「関数の実行を終わらせる〜コールバックが起動する」という部分を簡単に記述できるようにするための糖衣構文がasync/awaitになります。サンプルコードは次のようになります。
var $ = id => document.getElementById(id); var log = text => ($("output").innerText = text); function waitClick() { return new Promise(resolve => ($("button1").onclick = resolve)); } async function count() { log("start"); for (var i = 0; i < 5; ++i) { await waitClick(); log(i); } } count();
async await example - JSFiddle
上記コードにおいて、countは「非同期関数」として宣言されています(async functionによって定義された関数を非同期関数と呼びます)。 countの中では、waitClickが呼び出され、ボタンがクリックされると次の行にあるlog(i)が実行されます。
クリックを待ち、クリック後に処理が実行されるようにするには、count関数の実行を中断しlog(i)をコールバックとして登録する必要がありますが、そのままコードにすると、複雑になってしまいます。しかし、awaitと書くことで、あたかもcount関数の実行中にクリック待ちが行われているかのように書くことができました。
非同期プログラムへの変換
上記のcount関数は、次のような「同期的な」関数に、適切にasync/awaitキーワードを加えたものになっています。
function count_sync() { log("start"); for (var i = 0; i < 5; ++i) { waitClick(); log(i); } }
この例を見る限り、次のような手順で、同期的なコードを非同期対応のコードに変換することができそうです。
- I/O待ちを含む関数を特定する(上記ではwaitClick)
- 特定した関数の呼び出しにawaitをつける
- awaitを含む関数宣言/関数式にasyncをつける
以下、このような操作を自動で行う方法について考えてみたいと思います。
Babelを使ったソースコードの操作
ソースコードを入力としてソースコードを出力とするようなプログラムはどのように作ったらよいでしょうか。今回はBabelのプラグインを作って実現することにしました。
Babelは主に、新しい機能を使っているJavaScriptコードを互換性のあるコードに変換するためのツールです。
Babel · The compiler for next generation JavaScript
互換性の目的以外にも、プラグインという形で独自に定義した変換を実行することもできますので、使ってみることにしました。
Babelのインストール
Babelをインストールするには次のようなコマンドを入力します。
$ npm init $ npm install babel-cli --save-dev
この後、Babelがインストールされていることは、次のコマンドで確認できます。
$ node_modules/.bin/babel --version 6.26.0 (babel-core 6.26.3)
バージョンが表示されているので、インストールできていると確認できます。
プラグインの設定
Babelは多くの機能がありますが、プラグインで機能を追加することもできます。ここではBabel実行時に特定のプラグインを起動し変換を行うための設定方法を説明します。
Babelの動作設定は.babelrcというファイルで行います。次の内容のファイルを作成しておきます。
{ plugins: [["./convert.js", { name: "waitClick" }]], compact: false }
今回作成するプラグインではI/O待ちのある関数名をオプションとして指定できるようにしています。上記では、waitClickを指定しています。
.babelrcを作成したら、シェルから次のようなコマンドを実行することで、convert.jsで定義した変換が実行されます。
$ node_modules/.bin/babel (入力ファイル).js -o (出力ファイル).js
プラグインの作成方法は、次のページに書いてありますので、参考にしました(他のページも参考にしました)。
babel-handbook/plugin-handbook.md at master · jamiebuilds/babel-handbook · GitHub
変換プログラム
変換プログラムとして、次のようなコードを作りました。オプションで指定された名前の関数の参照箇所をみて、関数呼び出しになっていれば、awaitをつけるプログラムになっています。また、その呼び出しを含む関数宣言にasyncをつけるようにしています。
module.exports = function({ types: t }) { return { name: "add-await-to-call", visitor: { FunctionDeclaration(path, state) { if (path.node.id.name === state.opts.name) { // referencePathsで参照箇所を取得する path.scope.getBinding(state.opts.name).referencePaths.forEach(x => { if (x.parentPath.node.type !== "CallExpression") return; // 呼び出しにawaitをつける x.parentPath.replaceWith(t.awaitExpression(x.parent)); // waitClickの呼び出しを含む関数宣言を見つける for (z = x; z != null; z = z.parentPath) { if (z.node.type === "FunctionDeclaration") break; } if (z == null) { console.log("not function"); return; } // 見つけた関数宣言にasyncをつける z.node.async = true; }); } } } }; };
実行例は、次の入力に対し、
function waitClick() { return new Promise(resolve => ($("button1").onclick = resolve)); } function count() { log("start"); for (var i = 0; i < 5; ++i) { waitClick(); log(i); } } count();
次の出力となりました。
function waitClick() { return new Promise(resolve => ($("button1").onclick = resolve)); } async function count() { log("start"); for (var i = 0; i < 5; ++i) { await waitClick(); log(i); } } count();
意図した通りに、async/awaitが追加されました。
まとめ
今回の記事では、async/awaitの説明と、Babelのプラグインを作るところを紹介しました。
私は、JavaScriptのコードに手作業でasync/awaitをつけるということをやっていました。しかし、非同期にしたい関数は1つでも、その関数を多数の箇所で使っている場合は手作業ではかなり大変だということがわかってきました。そのため、今回のような変換プログラムを作ろうと思いました。
今後も、便利なJavaScriptの機能は活用していきたいです。また、手作業でできない作業は、プログラムを使ってできるようにしていきたいと思っています。さらには、手作業でやっているけど自動化可能な作業については、自動化していきたいと思います。
採用情報
朝日ネットでは新卒採用・キャリア採用を行っております。