Posted at

【初心者向け】JavaScriptの非同期処理を理解する callback、Promiseそしてasync/awaitへ

image.png

非同期処理は最近のフロントエンド開発において、もはや必須ともいえるようになってきました。

WebAPIに対して問い合わせる際や、ファイルを読み込む処理などJavaScriptではいたるところで非同期処理を実装する機会があります。非同期処理にすることでパフォーマンスを向上するようにNode.jsが設計されていることもあるのですが、同期的な処理を得意とする言語ばかり書いてきた人からするとどうしてもJavaScriptの非同期処理は受け入れづらいところがあるようです。

今回はJavaScriptの非同期処理として実装される3パターンを理解してみましょう。


  • コールバックによる実装とその地獄

  • コールバック地獄を解決するPromiseによる実装(ES2015)

  • さらに可読性を向上させるasync/awaitによる実装(ES2017)


コールバックによる実装とその地獄

まずはコールバックによる実装方法から確認してみましょう。

JavaScriptには非同期処理でファイルを読み込む fs.readFile というメソッドが用意されています。(公式ドキュメントはこちら

シンプルにjsonファイルを読み込んでコンソール出力するプログラムを実装してみましょう。

jsonファイルは以下の3つを使用します。


data_001.json

{

"id": "1",
"name": "bob"
}


data_002.json

{

"id": "2",
"name": "john"
}


data_003.json

{

"id": "3",
"name": "steve"
}

何も考えずに fs.readFile メソッドを実行し、jsonファイルを読み込んでみましょう。


callback.js

const fs = require('fs');

fs.readFile('data_001.json', 'utf-8', (err, data) => {
console.log(data);
});
fs.readFile('data_002.json', 'utf-8', (err, data) => {
console.log(data);
});
fs.readFile('data_003.json', 'utf-8', (err, data) => {
console.log(data);
});


出力結果

$ node callback.js

{
"id": "3",
"name": "steve"
}
{
"id": "1",
"name": "bob"
}
{
"id": "2",
"name": "john"
}

fs.readFileメソッドは非同期に実行されますので、jsonファイルの読み込み処理が終わったものからコンソールに出力されていることがわかります。このあたりが初心者がつまづきやすいポイントですね。

さて、「data_001.json が読み終わってから data_002.json を読み込みたい」 といったように複数のファイルを順に読み込みたい場合は以下のように記述する必要があります。


callback.js

const fs = require('fs');

fs.readFile('data_001.json', 'utf-8', (err, data) => {
console.log(data);
fs.readFile('data_002.json', 'utf-8', (err, data) => {
console.log(data);
fs.readFile('data_003.json', 'utf-8', (err, data) => {
console.log(data);
});
});
});


実行結果は以下のようになります。期待通り、3つのファイルが順番に読み込まれて出力されます。

$ node callback.js

{
"id": "1",
"name": "bob"
}
{
"id": "2",
"name": "john"
}
{
"id": "3",
"name": "steve"
}

ここでは3つのファイルを順に読むだけですので簡単ですが、実際にフロントエンド開発の現場では複数のWebAPIの問い合わせなど、10段階以上ネストしてしまうなんてこともありがちです。このネストが多くなり、ソースの可読性が落ちることを俗に コールバック地獄 と呼ぶようです。


コールバック地獄を解決するPromiseによる実装(ES2015)

EC2015では、このコールバック地獄に対応できるようにPromiseという仕組みが用意されました。このPromiseを使用することで随分と読みやすく非同期処理を繋げることができるようになりました。

本来であれば非同期処理の結果をコールバックで取得して、後続の処理に繋げる方法でしたが、Promiseによる解決方法は、非同期処理の結果として得られる値ではなく、その代わりにPromiseというオブジェクトを返しておいて処理が完了したところで値を返してやろうという仕組みです。

実際にPromiseを使用した例は以下のようになります。


promise.js

const fs = require('fs');

// 非同期処理を行い、Promiseを返却する関数を定義
function readFile(file) {
return new Promise((resolve) => {
fs.readFile(file, 'utf-8', (err, data) => {
resolve(data)
})
})
}

readFile('data_001.json')
.then((data) => {
console.log(data);
return readFile('data_002.json');
})
.then((data) => {
console.log(data);
return readFile('data_003.json');
})
.then((data) => {
console.log(data);
})


出力結果は1, 2, 3と順番に読み込まれています。

$ node promise.js

{
"id": "1",
"name": "bob"
}
{
"id": "2",
"name": "john"
}
{
"id": "3",
"name": "steve"
}

ただ、それでもまだ少し読みづらいコードだなぁと思ってしまいます。

以下のように書くことができれば処理の流れを追いやすいのですが、これでは1, 2, 3を順番に処理することはできません。


promise.js

const fs = require('fs');

// 非同期処理を行い、Promiseを返却する関数を定義
function readFile(file) {
return new Promise((resolve) => {
fs.readFile(file, 'utf-8', (err, data) => {
resolve(data)
})
})
}
readFile('data_001.json').then((data) => {
console.log(data);
})
readFile('data_002.json').then((data) => {
console.log(data);
})
readFile('data_003.json').then((data) => {
console.log(data);
})


出力結果

$ node promise.js

{
"id": "3",
"name": "steve"
}
{
"id": "1",
"name": "bob"
}
{
"id": "2",
"name": "john"
}
j

Promiseを使用することでコールバック地獄はなくなりましたが、(処理).then(()=>{次の処理})と繋ぐ必要がある記述方式は依然としてそのままです。


さらに可読性を向上させるasync/awaitによる実装(ES2017)

最後にES2017で登場した async/await という方法を理解しましょう。まずはサンプルプログラムから


async.await.js

const fs = require('fs');

// 非同期処理を行い、Promiseを返却する関数を定義
function readFile(file) {
return new Promise((resolve) => {
fs.readFile(file, 'utf-8', (err, data) => {
resolve(data)
})
})
}

async function readFiles() {
data001 = await readFile('data_001.json');
console.log(data001);
data002 = await readFile('data_002.json');
console.log(data002);
data003 = await readFile('data_003.json');
console.log(data003);
}

readFiles();


Promiseオブジェクトを返却する readFile メソッドには変わりありませんが、Jsonファイルを順番に読み込んで行く処理が少し変わっています。非同期処理で後続の実行を待たせたい場合は await を関数を呼び出す前につける必要があります。また、awaitを使う関数には async の記述をする必要があります。

出力結果を確認すると、順番に読み込めていることがわかります。

$ node async.await.js

{
"id": "1",
"name": "bob"
}
{
"id": "2",
"name": "john"
}
{
"id": "3",
"name": "steve"
}

JavaScriptを使用して非同期処理を実装するパターンを3つご紹介しました。

JQueryを使っていた頃はコールバック地獄で随分と苦労しましたが、最近ではECMAScriptをブラウザにも対応させるべくBabelが登場し、 React や Vue などのフロントエンドフレームワークが整備されていった結果、随分と非同期処理が記述しやすくなってきたように思います。

できることなら async/await を使いたいところですがECMAScriptが対応していないブラウザもあるので注意してください。各ブラウザの対応状況はこちらで確認することができます。

古いブラウザだったとしても Babel を使用してトランスパイルするなどの工夫をしましょう。フロントエンド開発の開発者体験がぐんと向上すると思いますよ。