Help us understand the problem. What is going on with this article?

JavaScriptの非同期処理について

More than 3 years have passed since last update.

JavaScriptの非同期処理についてまとめてみた

非同期処理とは?

まず、普通のプログラムを考えます
通常のプログラムだと

console.log("aiueo");
console.log("kakikukeko");

aiueo
kakikukeko

このように上から順にプログラムが実行されていきます。
これを同期的な処理と言います

では次のようなプログラムはどうでしょうか?

setTimeout(
  () => {
    console.log("aiueo");
  }, 5 * 1000
)

console.log("kakikukeko");

kakikukeko
aiueo

この場合は5秒後に最初のconsole.logが実行されます。
このようにあるタスク(setTimeoutの処理)が実行されているときに他のタスク(console.log("kakikukeko"))が実行されるような処理を非同期処理と言います。

JavaScriptの非同期処理

JavaScriptの非同期処理の方法はいくつかあり、大まかにcallback時代,ジェネレーター時代,Promise時代async,await時代に分けられます、

この記事では全ては紹介しませんが、callbackとpromiseを重点的にまとめていきたいと思います。

callback時代

callbackは先ほど紹介したサンプルのようにsetTimeoutやsetIntervalなどの関数を使って非同期処理をすることを言います。

スコープ

非同期の処理をする際、スコープに注意する必要があります。
例えば

function countdown() {
  let i;
  console.log("count down");
  for(i = 5; i >= 0; i--) {
    setTimeout(
      () => {
        console.log(i === 0 ? "GO!" : i);
      },(5 - i) * 1000
    )
  }
}

countdown();

このような処理を考えて見ましょう

この場合は-1が6回出力されて終了します。forループが実行され値が-1となるのです。
そしてその後にcallbackが実行されます。
これはcountdownが実行されるときに変数iを含むクロージャーが生成されますforループ内では非同期的に全て同じ変数iにアクセスしています。

しかし、この関数内の遅延時間の計算の部分は同期的に行われる((5-i)*1000の部分)ので5秒間のカウントダウンは正常に行われるのです。
setTimeoutの呼び出し自体も同期的に行われています。

Promise時代

Promiseの生成

Promiseの生成はPromiseの引数に非同期処理の関数を渡して、インスタンスを生成するだけです

new Promise(<Some Ajax Function>);

上の<Some Ajax Function>には二つの引数を指定します。

new Promise(function(onFulfilled, onRejected) {
  //* some ajax process *//
})

上記のような感じで指定します。
onFulfilledは処理が成功したときの処理、
onRejectedは処理が失敗したときの処理です。

最初に提示したcountdownの例をPromiseベースに変えて見ます.

function countdown(seconds) {
  new Promise((onSuccess, onFailed) => {
    for(let i = 0; i <= seconds; i++) {
      setTimeout(() => {
        console.log(`${i}秒経過`);
      }, i * 1000)
    }
  });
} 

このようにPromiseを生成します。

Promiseの利用

上記の例ではcountdown();を実行できますが、それでは戻されるPromiseを利用していないので意味がありません
では、Promiseを利用する処理を考えてみましょう。

function countdown(seconds) {
  return new Promise((onSuccess, onFailed) => {
    for(let i = 0; i <= seconds; i++) {
      setTimeout(() => {
        console.log(`${i}秒経過`);
        if(i === 5)
          onSuccess();
      }, i * 1000)
    }
  });
} 

countdown(5).then(
  () => { //*成功した時onSuccessの処理
    console.log("カウントダウン成功!");
  },
  (err) => { //*失敗した時onFailedの処理
    console.log(err);
  }
)

関数countdownを実行するとPromiseが返され、Promiseのメソッドthenが実行されます
そこでPromiseの中の処理が成功したらthenの第一引数の関数がonSuccessに渡され、失敗したら第二引数の関数がonFailedの渡されるのです。

その他にPromiseには失敗した時用にcatchメソッドが用意されており、これを使うことによって以下のように表現することもできます。

const p = countdown(5);
p.then(() => {
  console.log("カウントダウン成功");
}).catch((err) => {
  console.log("err");
});

エラーを出したい時は以下のようにします。

function countdown(seconds) {
  return new Promise((onSuccess, onFailed) => {
    for(let i = 0; i <= seconds; i++) {
      setTimeout(() => {
        console.log(`${i}秒経過`);
        if(i === seconds)
          onSuccess();
        if(i === 3)
          return onFailed(new Error("数字の3はカウントできません"));
      }, i * 1000)
    }
  });
} 

const p = countdown(5);
p.then(() => {
  console.log("カウントダウン成功");
}).catch((err) => {
  console.log("err");
});

Promiseのチェイニング

Promiseは.thenを繋げてチェイニングできます。

function countdown(seconds) {
  return new Promise((onSuccess, onFailed) => {
    const timeoutIds = [];
    for(let i = 0; i <= seconds; i++) {
      timeoutIds.push(setTimeout(() => {
        if(i === 13) {
          timeoutIds.forEach(clearTimeout); //カウントダウン取り消し
          return onFailed(new Error("13は不吉な数字です"));
        } else if (i === 15) {
          onSuccess(console.log("カウントダウン成功"));
        }
        console.log(`${i}秒経過`);
      }, i * 1000));
    }
  });
}

function launch() {
  return new Promise((onSuccess, onFailed) => {
    console.log("発射!");
    setTimeout(() => {
      onSuccess("周回軌道に乗った!!"); //次のthenの引数msgに渡される
    }, 2 * 1000);
  });
}

countdown(15)
  .then(launch)
  .then((msg) => {
    console.log(msg);
  })
  .catch((err) => {
    console.log(err);
  });

上記例のようにチェイニングできます。

async, await

関数の前にasyncを付けることでasync functionにすることができます。
演算子awaitはasync function内で使うものでPromiseが成功するまで待ちます。

const fs = require('fs');

function readF(filename) {
  return new Promise((onSuccess, onFailed) => {
    fs.readFile(filename, "utf-8", (err, data) => {
      err ? onFailed(err) : onSuccess(data);
    });
  });
}

function writeF(filename, data) {
  return new Promise((onSuccess, onFailed) => {
    fs.writeFile(filename, data, err => {
      err ? onFailed(err) : onSuccess('OK');
    });
  });
}

async function readAndWrite() {
  try {
    let writeData = await readF("a.txt"); // a.txtを読み込んでwriteDataへ代入
    writeData += await readF("b.txt"); // b.txtを読み込んでwriteDataへ追加
    await writeF("d.txt", writeData); // d.txtへ書き込み
  } catch (err) {
    console.error(err);
  }
}

readAndWrite();

a.txt
a
b.txt
b

結果

d.txt
a
b

非同期処理を行うreadF,writeF関数それぞれでawaitを使って、Promiseがresolveするまで待っています。
try catchを使ってエラー処理をすることもできます。

まとめ

この記事ではJavaScriptの非同期処理のうち、callbackとpromiseについて扱ってきました。
ミスがあったらご指摘お願いしますm(__)m

参考 初めてのJavaScript

shinonomeinc
東京理科大学発ベンチャー。提携大学内にソフトウェア研究所を組織し、学生向けのTech教育を提供しています。
http://shinonome.io
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away