JavaScriptの非同期処理とPromiseを学んだので、わかりやすくまとめてみる【入門編】


これはJSの非同期処理についての入門編の記事です。初めて非同期処理を学ぶ、学んだことはあるけど自信がない。そう言った方向けの記事となっています。 

非同期処理って、本当にJavaScript初心者に立ちはだかる大きな壁ですよね…

非同期処理は僕も今少し理解しだしたばかりで完全ではないのですが、学んだばかりだからこそ、どのようなプロセスで理解に至ったのかを新鮮に覚えているので、参考記事を紹介しつつ具体例を交えつつわかりやすく説明します。

わかりやすく説明すると宣言したからには僕自身がしっかり理解してないといけないという状況を作り出しています😏

非同期処理ってなに?

まずは以下のの動画をご覧ください。

非同期処理とは何か?【超入門編/JavaScript/プログラミング】

この動画は「Web万屋エンジニアチャンネル」のおさないさんが解説しているのですが、18分で非同期処理の概要を母と息子の家での家事の例を中心に説明していてとってもわかりやすかったです。

さて、動画をみる時間が無いという方もいると思うので、簡単に説明します。(ただ、動画を見るのを推奨します)

非同期処理

プログラミングは基本的に、コードを上から順番に実行します。

これは普通ですよね。

しかし、コードを上から順番に実行していく時に途中で時間のかかる処理があったとすると、次の処理、また次の処理と渋滞してしまいますよね?

もう少し具体的に説明します。

処理A→処理B→処理C→処理Dという感じでコードを上から書いていくとします。 
ここで処理Bに1年かかるとしましょう。その間、処理Cと処理Dは待っているのでしょうか?

流石に1年も待てません。

そこで考え出されたのが、「処理Bは時間かかるから、先にCとDやっちゃおうよ」というものです。

これが、非同期処理 です!

ちなみに同期処理は、処理AからDまで順番に行っていくものです。

コードの具体例も紹介!

さて、ここまで非同期の概念を紹介しました。 
次にコードで具体例を紹介します。

ここでは、以下の記事を参考に説明します。

非同期処理ってどういうこと?JavaScriptで一から学ぶ

さて、コードを少しお借りしまして、、解説します。

以下のコードをご覧ください。

async_readfile.js
console.log("start");

var fs = require('fs');
fs.readFile("number.txt", "utf-8", function(err, data){
  if (err) throw err;
  console.log("ファイルの読み取り準備ができました。");
  console.log(data);
});

console.log("end");

ファイルnumber.txtには、数字の1、2のみ記述します。

number.txt
1
2

async_readfile.jsでは、console.logで出力の順番をみていきます。

ここで、非同期処理の存在を知らない方であれば、「console.logを上から順に、、、つまり、start→ファイルの読み取り準備ができました。→data(ファイルの中身)→endの順に実行されそう!!」

このように思うかもしれません。

しかしながら、ファイルを開くというのはコンピューター的には、めちゃくちゃ時間がかかるんです!

先ほどの処理A→処理B→処理C→処理Dを思い出してみてください。

今回は処理A~Cまでにasync_readfile.jsを当てはめて考えてみましょう。

  • 処理A:console.log(“start”) 
  • 処理B:ファイルを開く処理
  • 処理C:console.log(“end”)

このようにして考えます。

先ほども言った通り、ファイルを開くという処理はコンピューターにとっては非常に時間のかかる作業です。

そこで、処理Bが終わる前に処理Cを実行してしまいます。

なので、出力は処理A→処理C→処理B→(かなり時間が経って)→完了のようになります。

実際のコンソールの表示は以下のようになります。

start
end
ファイルの読み取り準備ができました。
1
2

処理A→処理C→処理Bの順に表示されてますよね?

こんな感じで、「時間のかかるものはあとでやるよ。」というのが非同期処理でした。

コールバック関数

上記の例での処理B、つまりファイルを開いた際に実行されるfunction(err, data){…}のことをコールバック関数と言います。

時間がかかるので、後回しにされ後から呼び戻される。そんなイメージでしょうか。

コールバック関数については以下の記事が非常にわかりやすかったです。

JavaScriptの「コールバック関数」とは一体なんなのか

Promiseって何?

さて、ここからはpromiseについて解説します。

これについてもまずは、動画で学習するのをおすすめします。以下の動画をみてください。

JavaScriptのPromise徹底攻略-前編-【Promiseとは/非同期処理】

この動画ではPromiseについて大まかな説明がされています。

動画を見れば大体理解できるかと思います。

Promise

一応、ここでも解説します。

まず、promiseは実行に時間がかかる処理がとりあえず値を返しておき、その間に処理を進めて、処理が終わったタイミングで、その本来渡したい値を渡すというものです。

上で紹介したQiitaの記事にも書かれていますが、本来返してもらいたい値が何かしらの商品だとすると、promiseはその商品の引換券みたいなものです。

なぜPromiseが必要なの?

非同期関数が生まれたのに理由があれば、もちろんPromiseにも生まれた理由があります。

結論からいうと、コールバック地獄という入れ子地獄、ネスト地獄にならないようにするためです。

英語でcallback hellというのですが、これでググってみるといろいろすごい画像が出てきます😅

このように、無限に入れ子が続いたら、何がなんだかわかりません!!w

そして、そもそもなんでこんなに入れ子の形で記述しないといけないの? 
そう思うかもしれません。

上で説明した非同期処理の例では、consoleでstartが出力され、endが出力され、ファイルが出力され、という順番でした。ですが、start→ファイルの処理→endと表示したい場合にはどうすればいいでしょうか?

一つの手として、ファイルを開く処理をした、fs.readFileの第三引数のfunctionの中で順にconsole.logを記述するという方法があります。

async_readfile.js
var fs = require('fs');
fs.readFile("number.txt", "utf-8", function(err, data){
  if (err) throw err;
  console.log("start");
  console.log("ファイルの読み取り準備ができました。");
  console.log(data);
 console.log("end");
});

こうすることで、以下のようにstart→ファイルの処理→endの順番で表示されます。

start
ファイルの読み取り準備ができました。
1
2
end

ここまで理解できましたか?

この例では関数の中にconsole.logが4つ。 
だんだん見にくくなってきました。

それでは、ここからはcallback地獄でよく使われる例をご紹介します。

setTimeout.js
setTimeout(function(){
  console.log("1秒経ったよ");
  setTimeout(function(){
    console.log("2秒経ったよ");
    setTimeout(function(){
      console.log("3秒経ったよ");
      setTimeout(function(){
        console.log("4秒経ったよ");
      }, 1000)
    }, 1000)
  }, 1000)
}, 1000)

このコードではどのような結果になるのでしょうか?

console.logの値を見ればわかるかもしれませんが、結果は以下の通りです。

"1秒経ったよ"
"2秒経ったよ"
"3秒経ったよ"
"4秒経ったよ"

codepenのURLを貼っておくので、ぜひ実行してみてください。

CodePen

このように、時間のかかる処理がある時に、処理の順番を決めつつ、表示したい。

そんな時は上記のsetTimeoutのようにfunctionの引数ににfunctionを。そのまた引数にfunctionをと言ったように、どんどん入れ子の形にして表示することはできるのですが、とにかく見にくいし、いわゆる保守性の低いコードになってしまいます。

バグがあってもどこで起きてるのかを探しにくくなります。

じゃあ入れ子、ネストを深くしないで意図した通りの順番で処理を行いたい場合、どうすればいいのか。

そこで、Promiseを使います。

コードをみてみよう!

まずは、promiseがどのように渡されるのかをみてみましょう。

以下のコードをご覧ください。

const result = fetch('https://api.music.apple.com/v1/catalog/us/songs/203709340');

result.then((response) => {
  console.log(response);
})
console.log(result);

fetchメソッドは外部のリソースを通信して取得するものです。

今回は試しにApple MusicのAPIを取得してみます。 
今回は時間のかかる外部との通信をするためにAPIを取得しているので、APIが何かというのは関係ないと思って大丈夫です。

出力は以下のようになります。

[Object Promise]{}
[Object Response]{(中身は省略)}

このようになります。これは、fetchで通信して情報を取得するのは、コンピューターにとってとてつもなく時間がかかるので、先にPromiseだけ渡して置いて、情報を取得したら、その結果(Response)を返すという流れになっています。

ちなみに、promiseがコンソールに出力されたのは、console.log(result)と記述していたからです。 
このconsole.log(result)はthenの前に記述しても後に記述しても結果は変わらず、先にpromiseが出力されます。

さて、ここでthen()メソッドが出てきました。

このthenは上の例で言えば、resultが実行された時にどのような処理を行うのかを記述できます。

つまり、resultが実行された時に、responseという引数を渡してconsoleで出力するアロー関数が実行されます。

thenは何かが実行された時にどのような処理をするのかを記述するものと覚えておくと良いと思います。

それでは、setTimeout.jsに戻りましょう。

これをthenを用いて記述するとどのようになるでしょうか。

setTimeout.js
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

sleep(1000)
  .then(() => console.log("1秒経ったよ"))
  .then(() => sleep(1000))
  .then(() => console.log("2秒経ったよ"))
  .then(() => sleep(1000))
  .then(() => console.log("3秒経ったよ"))
  .then(() => sleep(1000))
  .then(() => console.log("4秒経ったよ"));

sleepという関数は引数にms(これはsetTimeoutの第二引数に渡され単位がミリ秒なのでmsとしています)をとり、new PromiseでPromiseオブジェクトを生成します。

そのPromiseの中ではresolveを引数にとり、setTimeoutにmsで渡ってきた数値を実行した結果を足せるようにします。

そして、sleep(1000)では、sleepの引数に1000ms、つまり1秒を指定して関数を実行します。 
関数sleepはとりあえずpromiseを返し、実行されます。そして、実行が終わった時(then)にconsole.logで1秒たったよと出力されるのです。

あとはこれの繰り返しです。

またsleepを実行し、promiseが返され、その間にsleepが実行され、終わるとconsoleで出力される。

こんな感じです。そしてこのようなthenで実行順序を繋げて記述する方法、これを

thenチェーン

と言います。

CodePenで実際の挙動をご確認ください。

"1秒経ったよ"
"2秒経ったよ"
"3秒経ったよ"
"4秒経ったよ"

このような表示が1秒ごとに出力されます。

thenチェーンを使うことで、コールバック地獄になるのを防ぎ、ネストが深くならず、保守性の高いコードが実現できます。

これがPromiseを使うメリットであり、特徴です。

さいごに

いかがだったでしょうか。

今回はPromiseと非同期の入門編ということで説明してきました。

僕は理解するのにかなり時間がかかってしまいました。ググっては理解できず、またググっては理解できずの繰り返しでした。

なので、これから理解しているみなさんが、僕みたいにわざわざ時間をかけなくても済むようにこの記事は作成しました。

お役に立てたら幸いです。

もう少し非同期を深掘りした記事も作成予定ですので、お楽しみに。

また、もし間違っている点などがありましたら、コメントで教えていただけると幸いです🙏

Twitterやってます。ぜひフォローお願いします。

以上です。長文でしたが、お読みいただきありがとうございました🙇‍♂️