見出し画像

【JavaScript】非同期処理について(callback)


はじめに

こんにちは。SHIFTのDevOps推進部に所属しているozakiです。

今回は、JavaScriptを学んでいる上で躓きやすい「非同期処理」(特にコールバック関数)についてまとめていこうと思います。

同期処理/非同期処理

まずは同期処理、非同期処理ついて軽く触れておきます。

同期処理(sync)

プログラムが記述された順番通りに実行されるものを同期処理と言います。ひとつの処理が終わるまで次の処理は行いません。

ただし、1行のプログラムに時間がかかると、それを待ってから後のプログラムが動き出すので、全体が重くなってしまうデメリットがあります。

function A() {
    // この処理に1分かかるとする
}

function B() {
    // すぐ終わる処理
}

function A();
function B(); // 同期処理だとB()は1分待ってから実行される

非同期処理(async)

非同期処理はコードを順番に処理していきますが、ひとつの処理が終わるのを待たずに次の処理をスタートさせます。 つまり、非同期処理では同時に実行している処理が複数あります。

非同期処理は、上記の同期処理のデメリットを解消してくれます。
しかしこの特性をちゃんと理解していないと、

「書いてある順番(A → B)で動いてほしいのに、前の処理が終わる前にBが動いちゃってる、、、」

のような事に陥ってしまいます。

function A() {
    // この処理に1分かかるとする
}

function B() {
    // すぐに終わる処理
}

function A(); //
function B(); //Aの処理が終わる前にBが開始される。Bのほうが先に処理が終わる

同期処理のデメリットを解消した代わりに、同期的に動かしたいときには「待つという動作を自分で入れる必要がある」ということですね。

ちなみに

並列処理並行処理という似たような単語がありますが、それぞれ別の意味を持ちます。

並列処理は、マルチスレッドでプログラムを実行し計算負荷を分散させる処理のことです。
よーいどんで3つの処理を同時にスタートさせるイメージですね。

一方の並行処理は、マルチスレッドではなくシングルスレッドで処理を行います。
複数の処理をそれぞれ分割し、切り替えながら進めていくので、同時に処理が行われているように見えます。
JavaScriptの非同期処理の多くは、この並行処理に当たります。

コールバック関数

非同期処理では、自分で処理の順番を制御しないといけないことがお分かりいただけたと思います。それではここから、JavaScriptの基本的な非同期処理である、コールバック関数についてみていこうと思います。

コールバック関数とはほかの関数に引数として渡す関数のことです。
何かしらの条件が成立した場合や、非同期イベントが発生した場合に、引数として渡された関数を呼び出す(コールバックする)ことからこの呼び名がついています。

timer

まずは簡単な形から理解をしていきましょう。
もっとも単純な非同期処理はsetTimeout()です。

setTimeout(checkForUpdates, 60000);

60000ミリ秒後(1分後)にcheckForUpdates()関数を呼び出す処理です。
checkForUpdates()がコールバック関数にあたります。

タイマーをストップさせたい場合は、setTimeout()の戻り値をclearTimeout()に渡します。

ところで、setTimeout()はコールバック関数を1度だけしか呼び出しません。
もし繰り返したい場合は、setInterval()関数を使います。

const updateIntervalId = setInterval(checkForUpdates, 60000);

function stopCheckingForUpdates() {
    clearInterval(updateIntervalId);
}

一定時間ごとにコールバック関数を呼び出す、繰り返しの非同期処理です。

こちらもsetTimeout()と同じように、setInterval()の戻り値をclearInterval()に渡すことで停止させることができます。

Node.jsのコールバック

Node.jsのサーバーサイド環境では、コールバックやイベントを使うAPIが多く定義されています。それらのAPIを例に、さらにコールバック関数について見ていきます。

以下はファイルの内容が読み込めた時に、コールバックを呼び出すプログラムです。

import fs from 'fs';

let options = {
    // デフォルトのオプションを記述
}

fs.readFile("config.json", "utf-8", (err, text) => {
    if(err) console.log("Could not read config file:", err);
    // ファイルの内容を解釈し、optionsオブジェクトに代入
    else Object.assign(options, JSON.parse(text));

    startProgram(options);
})

※ fs.readFile("ファイルパス", "文字コード", )

fs.readFile()は非同期でファイルを読み込み、コールバック関数を呼び出しています。
このコールバック関数は、ファイルの読み込みに失敗すれば第1引数にerrを、成功すれば第2引数にファイルの内容を渡しています。

この例では、コールバックをアロー関数で表現しています。
このようにアロー関数を用いて、見やすく表記するのが一般的です。

2段階の非同期処理

次の例は、URLで指定された内容に対してHTTPリクエストを送るプログラムです。
イベントリスナを使い、2段階で非同期処理を行っています。

import https from 'https';

function getText(url, callback) {
    const request = https.get(url);

    request.on('response', (response) => {
        const httpStatus = response.statusCode;
        response.setEncoding('utf-8');
        let body = '';

        response.on('data', (chunk) => { body += chunk; });

        response.on('end', () => {
            if(httpStatus === 200) callback(null, body);
            else callback(httpStatus, null);
        });
    });

    request.on('error', (err) => {
        callback(err, null);
    });
}

function output(error, data) {
    if(error) console.log(err);
    else console.log(data);
}

getText('https://~~~~', output);

※ reqest.on(イベント名, 関数):イベントリスナという。イベントが起こった時に登録されている関数(イベントハンドラ)を呼び出す。

responseイベントが発生したときに、イベントハンドラが呼び出されます。
さらに呼び出された関数の中で、dataイベントやendイベントが発生するとそれぞれでイベントハンドラが呼び出されます。

コールバック関数の注意点

コールバック関数を使うことで、非同期処理を表現してきました。しかし、このコールバック関数には注意しないといけない点があります。その注意点について、

  • 例外処理

  • コールバック地獄

の2点を見ていきます。

例外処理

同期処理の例外処理の方法として、try...catch構文があります。

try {
  throw new Error("エラーメッセージです");
} catch (error) {
  console.error(error);
}

tryブロックのなかでエラーが発生すると、その時点で処理が中断され、catchブロックが呼び出される仕組みです。

ではこのtry...catch構文を使い、コールバック関数で例外処理をしてみましょう。

try {
  setTimeout(() => {
    throw new Error("非同期的なエラー");
  }, 10);
} catch (error) {
    // この中身は実行されない
  console.error(error);
}

tryの中でsetTimeout()を行い、もしエラーが発生すればcatchの中身が作動します。一見何の問題もないように見えます。しかし、try...catch構文ではこのような非同期エラーをキャッチできません。

そのため、setTimeout()のコールバック関数における例外は、次のようにコールバック関数内で同期的なエラーとしてキャッチする必要があります。

setTimeout(function () {
  try {
    throw new Error("エラー");
  } catch (e) {
    console.error(e);
  }
}, 1000);

こうすることでコールバック関数のエラーをキャッチできます。
しかしこれはコードが非常に読みにくいものになってしまいます。

コールバック地獄

もう一つの注意点として、コールバック地獄と呼ばれるものがあります。
以下がその例です。
setTimeout()が入れ子になり、ネストが深くなっていることが分かると思います。

console.log(0);
setTimeout(function(){
  console.log(1);
  setTimeout(function(){
    console.log(2);
    setTimeout(function(){
      console.log(3);
    }, 10);
  }, 10);
}, 10)

これもまた、可読性の低いコードになってしまっています。

このように非同期処理を表現するためにコールバック関数は有効ですが、綺麗に書くことがとても難しいです。

まとめ

  • コールバック関数とは、ほかの関数に引数として渡す関数のこと

  • コールバック関数が呼び出されると、その時点で処理に割って入る

  • 呼び出しには、何かしらのトリガーがある(タイマーやイベントなど)

  • コールバック関数は入れ子ができる

  • 可読性の高い、きれいなコードを書くことが難しい

おわりに

頭の中の整理ということで、まずはコールバックについてまとめてみました。非同期処理はコールバックだけではなく、promiseやasync/awaitもあります。この辺りもしっかりと勉強して、アウトプットしたいなと思います。

参考


この公式ブロガーのおすすめ記事


執筆者プロフィール:Ozaki Yohei
23年に新卒でSHIFTに入社。現在は開発部門のプロジェクトに参画し、技術を学んでいます。

お問合せはお気軽に
https://service.shiftinc.jp/contact/

SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/

SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/

SHIFTの導入事例
https://service.shiftinc.jp/case/

お役立ち資料はこちら
https://service.shiftinc.jp/resources/

SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/