MENU

【Javascript】非同期処理について知る

2021 2/24
【Javascript】非同期処理について知る

おつかれさまです。

Javascriptを学習する上で避けては通れない「非同期処理」。
今まで名前だけは聞いたことはありましたが、少々難しそうだと思って食わず嫌いして全然触れてきませんでした。

ここ数日、非同期処理について勉強することになったので、
アウトプットもかねて今一度自分の知識を整理したいと思います。

目次

同期処理と非同期処理

前提知識

非同期処理について学ぶ上で、以下のことを前提知識として抑えておく必要があります。

  1. コンピュータの計算時間
    一般的にコンピュータは「CPU」を使って計算処理をおこないます。
    単純な計算処理はとても早いのですが、CPUがストレージ内のファイルを読み込みに行ったり外部からデータを取得するようなときに発生する通信処理は前者の単純な計算処理とは比べ物にならないほど時間のかかるものだという認識をしておいてください。

以上の前提知識を抑えた上で同期処理と非同期処理について解説します。

同期処理とは

記述されたコードを上から順に処理していき、一つの処理が終わるまで次の処理が行われない処理の実行を指します。
一般的なプログラム処理の流れですね。(上で言うとこちらがシングルスレッドなプログラムにあたります。)

※この処理が終わったら次の処理、この処理が終わったら次の処理のように
処理の終わるタイミングに合わせて処理が走るので「同期」処理と呼びます。

→ 同期処理だけだと何が困るのか?

もし処理の完了に時間を要するものがあれば、処理の流れがそこで一定時間止まることとなります。
こうなることで、 ブラウザのメインスレッド(ブラウザーがユーザーのイベントや描画を処理するところ)がその処理(時間かかるやつ)で占有されページが反応しなくなり、サイトの使い勝手が悪くなります。

 →少しややこしいと思うので簡単に補足すると、もし処理完了に5秒間かかるような処理があった場合、その処理が終わるまで画面の描画が行われずスクロール操作などが諸々できなくなるという事象が起きてしまいます。
 例えば、コンピュータはストレージにあるファイルを読みに行ったり、外部サーバーと通信をしてデータを取得するようなことにはかなり時間を要するため、データを取得し終わるまで次の処理に移らない → その間画面が固まるといった事象が起きてしまいます。

↓ブラウザで検証ツールを開き、以下の処理を行ってみてください。

// 指定した`timeout`ミリ秒経過するまで同期的にブロックする関数
function blockTime(timeout) {
    const startTime = Date.now();
    // `timeout`ミリ秒経過するまで無限ループをする
    while (true) {
        const diffTime = Date.now() - startTime;
        if (diffTime >= timeout) {
            return; // 指定時間経過したら関数の実行を終了
        }
    }
}
console.log("処理を開始");
blockTime(3000); // 他の処理を1000ミリ秒(1秒間)ブロックする
console.log("この行が呼ばれるまで処理が1秒間ブロックされる");

こちらの例では3秒間処理をブロックしているため、
3秒間スクロールなどの操作が効かないといったことが発生します。

少し難しい言葉が並んだと思うので、日常的なイメージで例えると

  1. お風呂から上がる
  2. ドライヤーで髪を乾かす(1が終わってから)
  3. 歯磨きをする(2が終わってから)

というような一個終わったら次の処理、一個終わったらまた次の処理。。
みたいなイメージです。

非同期処理とは

処理の完了を待たずに、走る処理のことを指します。
つまり、非同期処理では同時に実行している処理が複数ある状況になります。
(上の前提知識でいうところのマルチスレッド・プログラムのがこちらにあたります)

※同期処理とは違い、先の処理を待たず(処理完了のタイミングを合わせない)して処理が走るので、
「非同期処理」と呼びます。

先の日常のシーンで例えると、

・ドライヤーをしながら歯磨きをする
・もしくはお風呂に入っているときに歯磨きをする

の歯磨きのことを指すイメージです。

代表的なもので言うとsetTimeout関数がそれにあたります。

↓ブラウザで検証ツールを開き、以下の処理を行ってみてください。

// 指定した`timeout`ミリ秒経過するまで同期的にブロックする関数
function blockTime(timeout) {
    const startTime = Date.now();
    while (true) {
        const diffTime = Date.now() - startTime;
        if (diffTime >= timeout) {
            return; // 指定時間経過したら関数の実行を終了
        }
    }
}

console.log("1. setTimeoutのコールバック関数を10ミリ秒後に実行します");
setTimeout(() => {
    console.log("3. ブロックする処理を開始します");
    blockTime(1000); // 他の処理を1秒間ブロックする
    console.log("4. ブロックする処理が完了しました");
}, 10);
// ブロックする処理は非同期なタイミングで呼び出されるので、次の行が先に実行される
console.log("2. 同期的な処理を実行します");

上記の処理ですが、同期的な流れで見れば普通

  1. setTimeoutのコールバック関数(10ミリ秒後に実行したい処理)を10ミリ秒後に実行します
  2. ブロックする処理を開始します
  3. ブロックする処理が完了しました
  4. 同期的な処理を実行します

となると思いますが、実際は

  1. setTimeoutのコールバック関数を10ミリ秒後に実行します
  2. 同期的な処理を実行します
  3. ブロックする処理を開始します
  4. ブロックする処理が完了しました

となります。

非同期処理は同期処理とは違い、処理を待たず(並行して処理が走る)ので、
同期処理であったブラウザが固まったりするというようなことが起きなくなります。

繰り返しにはなりますが、コンピュータが外部サーバと通信して、何かデータを取りに行くと言うことは実はとてもなく時間のかかる処理になります。そのため、非同期処理と言う概念なければプログラムの処理を長時間遮断してしまうような状況になり、その間ユーザー操作が効かなくなり、サイトの使い勝手に悪影響を及ぼしてしまいます。

そのため、同期処理だけではどうしても限界があります。非同期処理を用いることで処理に時間を要するものに関しては裏で並行して走らせてしまおうと言う魂胆です。

従来の非同期処理(コールバック地獄)

従来のJavascriptでは非同期処理を実現するためにコールバック関数を用いた実装方法を取っていました。

関数をコールバックとして引数に渡すことで、
非同期処理の完了後のタイミングで、任意の処理をおこなうことができるというものです。

↓ブラウザで検証ツールを開き、以下の処理を行ってみてください。

setTimeout(() => {
    console.log('わん!');
    setTimeout(() => {
        console.log('にゃあ!');
        setTimeout(() => {
            console.log('ぴょん!');
            setTimeout(() => {
               console.log('ぴょん!');
            }, 1000);
        }, 1000);
    }, 1000);
}, 1000);

上の例では、settimeout関数をネストして指定の秒数が経ったらコールバックとして取っている任意の関数処理(ここで言うconsole.log)が走るといった処理が実現できます。

が、これパッと見て見づらいですよね、。
さらに、仮にこちらの記述でどこかの処理内容を書き換えたいといったときに、各処理が上と下で離れているので、とても保守性も悪くなってしまいます。これを俗に「コールバック地獄」と呼ばれていたそうです。

Promiseの登場

前項での非同期処理実装ではいまいち使い勝手が悪い!と言うこともあって、
ES6からPromiseオブジェクトなるものが登場しました。

こちら端的に言いますと、非同期処理内での処理の順番を状況に応じて柔軟に指定することができるといったものになります。
もっと噛み砕くと非同期処理をより描きやすくしたもの、みたいなイメージで良いと思います。

使うメリットとしては

  • 非同期処理を連結する際、コールバック地獄から解放される
  • 非同期処理を行う際に「成功時」「失敗時」の処理を明示的に切り分けて書くことが可能
  • 一連の非同期処理を関数化して再利用しやすくできる

といったメリットがあります。

Promiseって何者?

もう少し深掘りしていきたいと思います。
ここから少しPromiseの仕組みについての話になるのですが(ちょっと難しいです)

Promiseオブジェクトの構成】
・型:オブジェクト
・内部的に以下3つの状態が存在する。

  • Fulfilled
    resolve(成功)したときの状態。このときonFulfilledが呼ばれる(resolveが返る)
    →thenメソッドの引数に後続する任意の関数をコールバックとして登録することができる。
  • Rejected
    reject(失敗)または例外が発生したときの状態。このときonRejectedが呼ばれる
    →catchメソッドの引数に後続する任意の関数をコールバックとして登録することができる
  • Pending:FulfilledまたはRejectedではない状態
    new Promiseでインスタンスを作成したときの初期状態

まず基本的な使用手順としてはコンストラクタであるPromisenewして、
new Promise()の形でインスタンスを作ります。

初期化された時点でのPromiseオブジェクトの状態はまだ未確定(pending)の状態です。
ですが、とりあえず仮の状態でPromiseオブジェクトは返されます。

そのあともし特定の処理が成功したら先に返されたPromiseオブジェクトの状態は成功(fullfilled)の状態として変化し、そのあとthenメソッドに登録した任意の関数処理を実行することができます。

反対に、もし失敗したらPromiseオブジェクトの状態は失敗(rejected)の状態に変化し、そのあとcatchメソッドで登録した任意の関数処理を実行することができます。

上記の説明を実際のコードで書くと次のようになります。

let myFirstPromise = new Promise((resolve, reject) => {
  const result = 1;
①条件処理
  if (result === 1) {
    setTimeout(() => resolve(result), 1000); //成功時
  } else {
    setTimeout(() => resolve(reject), 1000); //エラー時
  }
});

myFirstPromise //Promiseオブジェクトの初期化 → この時点で未確定のPromiseオブジェクトが返される
//①の処理が成功したら
  .then((result) => {
    console.log("done" + result);
    return result + 1;
  })
//①の処理が失敗したら
  .catch((err) => {
    console.error(err);
  });

。。。はい、難しいですよね(笑)
正直初見だとマジで意味がわからないと思うので、日常のイメージで噛み砕いてみようと思います。

Aさんがはとあるゲーム機が欲しかったので、それを購入しようとしています。
ですがそのゲーム機は今人気がありすぎて入手困難です。在庫が品薄のため、店舗では「抽選販売」の形式を取っており、当選した人だけがゲーム機を購入することができます。

という背景を前提として話を進めたいと思います。

ここからがPromiseオブジェクトと照らし合わせた部分になるのですが、

・Aさんが店舗で抽選エントリーを申し込みに行った。そしてお店の人からは抽選引換券を受け取った。
 →Promiseオブジェクトの初期化。未確定のPromiseオブジェクト(こちらが抽選引換券にあたるもの)が返される。

【成功したとき(当選)】
・Aさん、当選。ゲーム機を購入する権利を獲得。
→Promiseオブジェクトの状態がFulfilled(成功)に変化。resolve関数を呼ぶ。
 ・そのあと、店舗に足を運びゲーム機を購入して家に持って帰る。
 →thenメソッドで登録した関数処理の実行

【失敗例(落選)】
・Aさん、落選。購入権を獲得できず。
→Promiseオブジェクトの状態がrejected(失敗)に変化。reject関数を呼ぶ。
 ・そのあと、無念の気持ちをtwitterにつぶやく。
  →catchメソッドで登録した関数処理の実行

みたいなイメージです!笑
伝わりますでしょうか😅

また、Promiseを使うと先に挙げていたコールバック地獄が解消されます。

//resolve関数を読んだ場合の
myFirstPromise
  .then((result) => {
    console.log("done"+result);
    return result + 1;
  })
 .then((result) => {
    console.log("done"+result);
    return result + 2;
  })
 .then((result) => {
    console.log("done"+result);
    return result + 3;
  })

こんな感じに、then()を繋げることができて、
こうすることで同期的処理のように後続の処理を次々と登録することができます。

まとめ

・同期処理とは、処理を待って走る処理実行のこと(一般的な処理の流れ)

・非同期処理とは前の処理を待たずに走る処理のこと(せっかちなやつです)

・Promiseは非同期処理を明示的に書きやすくした方法の一つにすぎない。
 →コールバック地獄を回避することができる。

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

かなりハードな内容だったと思うのですが、自分のアウトプットとしても知識を整理してみました。

まだまだ至らない点もあるかとは思いますが、これからも知識を深めれていけたらなと思います。

もりけん塾に入っております!

もりた先生のブログ: kenjimorita.jp (武骨日記)
もりた先生のアカウント: twitter.com/terrace_tech

参考

あわせて読みたい
No Image
非同期処理:Promise/Async Function · JavaScript Primer #jsprimerJavaScriptにおける非同期処理についてを紹介します。同期処理と非同期処理の違いやなぜ非同期処理が重要になるかを紹介します。非同期処理を扱うPromise、Async Function...

↑こちらのシリーズがわかりやすかったので、おすすめです。

この記事を書いた人

コメント

コメントする

CAPTCHA


目次
閉じる