2021/08/19

なぜコールバック関数で非同期処理の順番を順々にできるの?

@ 酒井悠宇

JavaScript

非同期処理について理解する
この記事を復習したいと思いまーす!

非同期処理について理解する


javascriptのプログラムは上から順に実行される(シングルスレッド)。なので外部からたくさんのデータを取ってくるような時間のかかる処理を行なったとき、画面が真っ白なまま止まってしまって、ユーザーからすると非常に使い勝手が悪い。
これを解決するために非同期処理というものが生まれた。これは、時間のかかる処理を一旦裏に回して、データを取ってきているときもページは問題なく操作できるようにするためのもの。

非同期処理を行うことができる関数(非同期関数)として、setTimeout関数がある。引数に受け取った関数(コールバック関数)を実行するタイミングを第二引数に秒数を記述することで指定することができる。

setTimeout(() => console.log("処理が完了しました"), 1000)
console.log("こんにちは");

▶︎こんにちは
▶︎処理が完了しました


しかし実際の非同期処理はこんなにシンプルなものではなく、「いつ処理が完了して返り値が返ってくるのかがわからない」ということが起こりうると予想できる。
関数A、関数B、関数C(それぞれ非同期関数)を前から順番に実行したのに、返り値が返ってくる順番が、関数B、関数C、関数Aみたいな順番になっていたらなんだか気持ち悪い。
後々の処理でif文でどんな順番で返り値が返ってきても対応できるようにコードを書いたりしないといけなさそう(ど素人の勘です。)だから何だか嫌な感じ。

実際にsetTimeout関数をこんな感じで使ってみると、1、2、3と返ってくるんじゃなくて、1、3、2というふうに返ってくる。
こんな感じで処理の実行結果が返ってくるタイミングが保証されていないのはあまりよくなさそう。


なのでこれはいい感じに、「裏で実行されている複数の処理(非同期処理)の実行結果が返ってくるタイミング」をきちんと順々にできる仕組みがあると良さそう。

その方法として最初に使われていたのが、「コールバック関数」を用いる方法。
なぜコールバック関数で非同期処理の実行結果が返ってくるタイミングを順々にできるのかというと、「非同期処理は処理の実行結果が返ってくる前に次の処理が実行されるけど、非同期処理内の処理の実行の順番が変わることはないから」ということになる。

コードで表すとこのような感じになる。

const func = (argFunc) => {
  argFunc();
};

func(() => {
  console.log("1");
  setTimeout(() => {
    console.log("2");
    func(() => {
      console.log("3");
    });
  }, 5000);
});

▶︎1
▶︎2
▶︎3


処理の順番としてはこのようになる。

  1. 引数に関数を受け取り実行する関数funcを定義。
  2. 関数funcが実行される。
  3. 関数funcは引数にコールバック関数を受け取る。
  4. 関数funcは引数に受け取ったコールバック関数を実行する。
  5. console.log("1")が実行される。
  6. setTimeout関数が実行される。
  7. setTimeout関数は引数にコールバック関数を受け取る。
  8. 5秒後にconsole.log("2")が実行される。
  9. 関数funcが実行される。
  10. 関数funcは引数にコールバック関数を受け取る。
  11. console.log("3")が実行される。


setTimeout関数は非同期関数なので、実行されると処理が裏側に回されて次のコードが実行されるけど、その裏側に回る処理自体の実行の順番は変わるわけではない。
コードで表すとつまりこういうこと。

console.log(1);

//処理が裏に回るけど、引数に渡された関数内の処理を実行する順番は変わらない
setTimeout(() => {
  console.log(2);
  console.log(3);
  console.log(4);
}, 1000);

console.log(5);

▶︎1
▶︎5
▶︎2
▶︎3
▶︎4


つまるところ、非同期処理であろうがなんであろうが、コールバック関数内の処理の実行の順番は変わらない。
だからコールバック関数を使うことで非同期処理の実行結果が返ってくる順番を順々にすることができる。

でもこう思うと思う。
「コールバック関数の中にまた非同期処理があったらそこの処理の順番は保証されませんよね?」と。
その通り↓

const func = (argFunc) => {
  argFunc();
};

func (() => {
  console.log("1");
  setTimeout(() => {console.log("2");})
  console.log("3");  
})

▶︎1
▶︎3
▶︎2


だからその処理もまとめてコールバック関数内の処理にする。
コードで表すとつまりこう言うこと。

const func = (argFunc) => {
  argFunc();
};

func(() => {
  console.log("1");
  setTimeout(() => {
    console.log("2");
    console.log("3");
  }, 1000);
});

▶︎1
▶︎2
▶︎3


さらに「console.log("3")」のところも非同期処理でこんな感じだった場合は

func(() => {
  console.log("1");
  setTimeout(() => {
    console.log("2");
    setTimeout(() => {console.log("3")}, 1000)
  }, 1000);
});


こうすればいい

const func = (argFunc) => {
  argFunc();
};

func(() => {
  console.log("1")
  func(() => {
    setTimeout(() => {
      console.log("2");
      func(() => {
        setTimeout(() => {console.log("3");
        }, 1000)
      })
    }, 1000)
  })
})


もうあとは予想できる。
順々に実行したい非同期処理が増えれば増えるほどこのコールバック関数のネストが増えていく。
これが「コールバック地獄」と呼ばれるもの。


はいということで今日はこんな感じで終わりたいと思います。
なぜコールバック関数で非同期処理の実行結果が返ってくるタイミングが順々になるのかがより深くわかりました。
次の記事ではpromiseとかasync/awaitについて書いていきます!
グッバイ!

© 2021 powerd by UnReact