Skip to content

shouyamamoto/js-study

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

58 Commits
 
 
 
 

Repository files navigation

もりけん塾勉強会資料

モンハンで理解する非同期処理の書き方

この記事の対象者

  • 非同期処理って聞くけど何?
  • Promise を調べてみたけど、あまり理解できていない
  • 簡単なコードの例ばかりで、実際の使い方がわからない

この記事を読むにあたって身につけて置いた方がよい知識

  • 引数、コールバック関数の書き方
  • アロー関数の書き方
  • 簡単な Promise の知識(みたことあるレベルで可。なくても良いですが、あるとスムーズ)

この記事を呼んで理解できること

  • 同期処理とは、非同期処理とは
  • Promise 実行時のイメージ
  • Promise チェーンや Promise を使った並列処理

同期処理とは

まず、非同期処理を理解するために、同期処理を説明します。
同期処理とはつまり、順序を守って処理されるということです。一つの処理が完了するまで次の処理には進みません。
処理の順番は決して変わることはなく、上から順に実行されます。

ということは、もし上に重い処理があったときには、次の処理が行われないということです。
例えば、重い処理とは外部から値を取得する処理などです。

重たい同期処理

非同期処理とは

上記のように、次の処理が行われないことを防ぐために非同期処理が存在します。
非同期処理の書き方は 3 種類あり、
コールバック関数Promiseasync, awaitです。

まずは非同期処理の書き方をコードでみていきます。

  • コールバックを使った非同期処理の書き方
setTimeout(() => {
  console.log(5);
  setTimeout(() => {
    console.log(4);
    setTimeout(() => {
      console.log(3);
      setTimeout(() => {
        console.log(2);
        setTimeout(() => {
          console.log(1);
          setTimeout(() => {
            console.log(0);
          }, 1000);
        }, 1000);
      }, 1000);
    }, 1000);
  }, 1000);
}, 1000);

// 出力結果 => 5, 4, 3, 2, 1, 0

コールバックを使って非同期処理を順次実行しようとすると、ネストが深くなってしまい、よく見る「コールバック地獄」というコードが出来上がります。

そこで、ES6(2015 年)から Promise が使えるようになりました。Promise を使って書くことでネストが深くならず、見通しの良いコードがかけるようになります。

  • Promise を使った書き方
const promiseFactory = (num) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(num);
      resolve();
    }, 1000);
  });
};

promiseFactory(5)
  .then(() => promiseFactory(4))
  .then(() => promiseFactory(3))
  .then(() => promiseFactory(2))
  .then(() => promiseFactory(1))
  .then(() => promiseFactory(0));

// 出力結果 => 5, 4, 3, 2, 1, 0

さらに Promise よりもさらに直感的にかけるように async await も ES8(2017)から使えるようになりました。
then()と囲う必要がないことでさらにコードがすっきりします。

  • async await を使った書き方
const promiseFactory = (num) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(num);
      resolve();
    }, 1000);
  });
};

const putsNum = async () => {
  await promiseFactory(5);
  await promiseFactory(4);
  await promiseFactory(3);
  await promiseFactory(2);
  await promiseFactory(1);
  await promiseFactory(0);
};
putsNum();

// 出力結果 => 5, 4, 3, 2, 1, 0

Promise や async await を使うとネストが深くならず、可読性があがるというメリットがあります。
非同期処理を書く際には Promise または async await を使うことになると思います。
今回は Promise の書き方にフォーカスして解説していきます。
Promiseを理解することで、async awaitの理解や、fetchを使うときにも理解が早くなると思います。

ざっくり書き方はわかったけど、中身の処理が理解できない...

という方も多いと思います。ここからは絵も取り入れながら、まずはイメージを掴んでもらおうと思います。
今回は「モンハン」を題材にして、解説します。

モンハンでは依頼者からクエストを受け、モンスターを倒したり、薬草を採取したりなどしてストーリーを進めていきます。
今回は、「回復薬」を作ることをクエスト達成の条件としたいと思います。
イメージはこんな感じです。

クエスト依頼

回復薬を作るためには、「薬草」と「アオキノコ」が必要で、「密林」にいく必要があります。

クエスト開始!!

JavaScript はシングルスレッドです。シングルスレッドは直訳で「一本の糸」という意味で、一度に一つの処理しかできません。
言い換えると、異なる処理を同時に行うことができません。

クエストを受注したハンターは防具をつけ、武器を選び、モンスターと闘うことも想定していろいろな道具を持っていくなど、クエストにいくまでにはたくさんの準備が必要なのですが、
ハンターは不器用なのでそれぞれの準備を同時に行うことはできません。

ハンター一人でクエストに挑む

そして、村を出たハンターは「密林」に移動し、「薬草」「アオキノコ」を採取、そして 2 つを調合して「回復薬」を作り、無事クエスト依頼者に納品しました。

新しい仲間を手に入れる

回復薬を作るだけでもかなり大変だと感じたハンターは、どうにかして楽をできないかを考えました。そこで、ネコを雇うことに。
アイルー

モンハンではアイルーというオトモネコを仲間にすることができます。(一緒にクエストいくと可愛い。🐱)
このアイルー(猫)は働き者で、かならず約束を守るイイやつだったのでハンターはこのアイルーに「プロミス」という名前をつけました。
(今後、JS での説明では Promise、モンハンのアイルーはプロミスと分けて記載しています。)

プロミスはクエストが完了すると、クエスト成功したか失敗したかを教えてくれます。また、成功した時・失敗した時に次の手順を決めてくれます。

プロミスはきちんと報告してくれる

プロミスには今回、薬草とアオキノコを採取してきてもらうクエストを任せました。
ハンターは準備やクエストに向かう必要がなくなったので、その時間で回復薬を調合する準備をすることができます。さらに空いた時間で釣りをすることもできました。

そしてプロミスはクエストを終え次第、ハンターに「クエストの結果」を知らせ、もしクエストを達成した場合には「薬草」と「アオキノコ」を渡します。
そして、プロミスからクエストの結果と素材を受け取ったハンターは、プロミスのいう通りに回復薬を調合します。
ハンターは調合の準備ができているので、すぐに回復薬を作り依頼者に納品します。

プロミスにクエストを頼む

こうしてハンターはうまく時間を使えるようになりました。
ここで重要なのは、プロミス(アイルー)がクエストに向かっているときにもハンターの行動が制限されることがないというところです。
JavaScript に話を戻すと、重い処理を実行していてもクリックやスクロールができるので、ユーザ体験がよくなるということです。

コードとモンハンを混ぜて解説

まずは基本的な Promise の書き方についてみていきましょう。

new Promise((resolve, reject) => {
  // 同期処理 ここでresolveかrejectを実行する
})
  .then() // resolveが実行されたら実行される処理を書く
  .catch(); // rejectが実行されたら実行される処理を書く

まず、Promise を使うときにはnew Promiseとして、Promise から Promise オブジェクトを生成します。
続けて.then().catch()と書くことができます。これらは Promise の処理結果によって実行される処理が変わります。

ここからは実際のコードと先ほどのモンハンの解説を混ぜて説明します。

const restorativeItem = ["薬草", "アオキノコ"];

const getRestorativeItem = (questResult) => {
  return new Promise((resolve, reject) => {
    if (questResult) {
      resolve(restorativeItem);
    } else {
      reject();
    }
  });
};

const createRestorative = (item) => {
  const yakusou = item[0];
  const aokinoko = item[1];
  console.log(`${yakusou} + ${aokinoko} を調合して回復薬を作った!`);
};

getRestorativeItem(true)
  .then((item) => createRestorative(item))
  .catch(() => console.error("クエストに失敗しました。"));

まず、1 行目

const restorativeItem = ["薬草", "アオキノコ"];

は、回復薬を作るために必要なアイテムを配列に格納しています。

const getRestorativeItem = (questResult) => {
  return new Promise((resolve, reject) => {
    if (questResult) {
      resolve(restorativeItem);
    } else {
      reject();
    }
  });
};

次にnew Promise()としてプロミスを呼び出しています。これからこのプロミスに何をして欲しいかを指示します。(プロミス(アイルー)を呼んでハンターが何をして欲しいかのメモを渡すイメージ)
ここではquestResultの結果によって、resolverejectを実行するように伝えています。
そして、resolveを実行したときには、restorativeItemを渡すようにしました。

次に進みます。

getRestorativeItem(true)
  .then((item) => createRestorative(item))
  .catch(() => console.error("クエストに失敗しました。"));

先ほど、.then().catch()は Promise の処理結果によって実行される処理が変わるとお伝えしました。
then()の第一引数に与えた関数にitemが渡ってきます。これはどこから渡ってくるのでしょうか?

答えは、Promise の中にあるresolve(restorativeItem)から渡ってきています。
このように、resolve().then()と、reject().catch()とペアになっていることがわかります。
(ここではわかりやすいようにペアと説明していますが、厳密には違います。細かい挙動に関しては promises-bookなどをご参照ください)
一連の流れを図解します。

プロミスを図解

プロミスを図解

Promise チェーン

ここまでで、基本的なプロミスの書き方についてはある理解できたかと思います。次は Promise チェーンを解説していきます。
Promise チェーンとは、Promise を使って非同期処理を順次実行していくことです。
またモンハンで例えながら説明していきます。

新しいクエスト

さきほどの回復薬から一つグレードアップした回復薬グレートが欲しい様子です。素材は「薬草」「アオキノコ」「ハチミツ」が必要です。
今回は「薬草」「アオキノコ」は密林のエリア 1 に、「ハチミツ」は密林のエリア 2 にあることを想定します。
なので、まずは密林についたら「薬草」「アオキノコ」を先に採取して、次に「ハチミツ」を採取することにします。

クエストの達成イメージ

では、コードを書いてみましょう。

const pocket = [];
const restorativeItem = ["薬草", "アオキノコ"]; // 例えば密林のエリア1にある
const greatRestorativeItem = ["ハチミツ"]; // 例えば密林のエリア2にある
const getRestorativeItem = (targetItem, questResult) => {
  return new Promise((resolve, reject) => {
    if (questResult) {
      setTimeout(() => {
        insertPocket(targetItem);
        resolve(pocket);
      }, 2000);
    } else {
      reject();
    }
  });
};

getRestorativeItem(restorativeItem, true) // getRestorativeItem(採取しにいく素材, 採取できたかどうか)
  .then((pocketItem) => {
    return getRestorativeItem(greatRestorativeItem, true); // getRestorativeItem(採取しにいく素材, 採取できたかどうか)
  })
  .then((pocketItem) => {
    createRestorativeItem(pocketItem); // 調合する処理
  })
  .catch(() => console.log("クエスト失敗..."))
  .finally(() => console.log("クエスト終了"));

const insertPocket = (items) => {
  items.forEach((item) => {
    pocket.push(item);
  });
};

const createRestorativeItem = (pocketItem) => {
  if (pocketItem.length === 3) {
    const yakusou = pocketItem[0];
    const aokinoko = pocketItem[1];
    const hachimitu = pocketItem[2];
    console.log(
      `${yakusou}${aokinoko}${hachimitu}を調合して回復薬グレートを作った!!`
    );
  } else {
    console.log(`素材が不足している...調合できずにクエスト失敗`);
  }
};

まずはじめにgetRestorativeItemを実行し、「薬草」「アオキノコ」を採取しにいきます。
その後に.then()と繋いで再度getRestorativeItemを実行して「ハチミツ」を採取しにいきます。

returnとしている理由は、Promise チェーンを繋ぐ場合には then メソッドのコールバック関数のreturnに Promise のインスタンスを設定する必要があるからです。
こうしないと Promise のチェーンが切れてしまい意図した挙動になりません。

例えば以下のようにreturnを書かない場合には、2 回目のgetRestorativeItemの処理を待たずに次のcreateRestorativeItemが実行されてしまい、createRestorativeItemでの処理が失敗します。(ハチミツが不足している状態)

getRestorativeItem(restorativeItem, true)
  .then((pocketItem) => {
    getRestorativeItem(greatRestorativeItem, true); // return がない場合
  })
  .then((pocketItem) => {
    createRestorativeItem(pocketItem); // ハチミツが足りない状態で回復薬グレートを作り始めてしまう。 -> 素材が不足している...調合できずにクエスト失敗が出力される
  })
  .catch(() => console.log("クエスト失敗..."))
  .finally(() => console.log("クエスト終了"));
return がない場合の出力結果;
// ["薬草", "アオキノコ"]
// 何かが足りないようだ...調合失敗
// クエスト終了
// ["ハチミツ"]

このように、Promise チェーンを順次実行する際には、「コールバック関数のreturnにPromiseのインスタンスを設定する」ことを覚えておきましょう。

Promise を並列処理する方法

Promise は並列処理を行うこともできます。並列処理を行うためにはPromise.allを使います。(他にもPromise.racePromise.allSettledなどありますが、説明は割愛します。) では先ほどのコードをPromise.allを用いて書き直してみます。

const pocket = [];
const restorativeItem = ["薬草", "アオキノコ"]; // 例えば密林のエリア1にある
const greatRestorativeItem = ["ハチミツ"]; // 例えば密林のエリア2にある

const getRestorativeItem = (targetItem, questResult) => {
  return new Promise((resolve, reject) => {
    if (questResult) {
      setTimeout(() => {
        insertPocket(targetItem);
        resolve(pocket);
      }, 2000);
    } else {
      reject();
    }
  });
};

const insertPocket = (items) => {
  items.forEach((item) => {
    pocket.push(item);
  });
};

const createRestorativeItem = (pocketItem) => {
  if (pocketItem.length === 3) {
    const yakusou = pocketItem[0];
    const aokinoko = pocketItem[1];
    const hachimitu = pocketItem[2];
    console.log(
      `${yakusou}${aokinoko}${hachimitu}を調合して回復薬グレートを作った!!`
    );
  } else {
    console.log(`素材が不足している...調合できずにクエスト失敗`);
  }
};

Promise.all([
  getRestorativeItem(restorativeItem, true),
  getRestorativeItem(greatRestorativeItem, true),
])
  .then(() => {
    createRestorativeItem(pocket);
  })
  .catch(() => {
    console.log("クエスト失敗...");
  })
  .finally(() => {
    console.log("クエスト終了");
  });

Promise.allの中に反復可能オブジェクトを設定し、その中で Promise のインスタンスを格納します。

Promise.all([
  getRestorativeItem(restorativeItem, true),
  getRestorativeItem(greatRestorativeItem, true),
]);

今回は反復可能オブジェクトとして配列を使用し、配列に Promise のインスタンスを返すgetRestorativeItemを 2 つ格納しています。
Promise.allを使うと、反復可能オブジェクトに格納した Promise のインスタンスの状態が全てfulfilledになるまで次の.then()が実行されません。
Promise のインスタンスの状態がfulfilledになるということは、全ての処理でresolve()が実行されるまでという考え方もできます。

このPromise.all()を使うことで Promise オブジェクトの配列を並列に実行することができるため、同じ処理を直列で行うよりも処理が早くなります。

例えば、先ほどであれば直列に.thenを実行していたので、2 秒、2 秒で合計 4 秒かかっていましたが、
Promise.allを使うことで 2 秒で処理を終わらせることができます。

イメージとしてはアイルーを 2 匹雇って、別々のステージに行かせてクエストを早く終わらせるといった感じです。

 Promise.allのイメージ

最後に

ここまでの内容が理解できれば、ある程度 Promise を自分で書くことができるようになっていると思います。
もっと非同期処理について知りたい!という方のために、次は何を学ぶべきかを残しておきます。

  • 【JS】ガチで学びたい人のための JavaScript メカニズム(Udemy)
    今回の資料を作成する際にとても参考にさせていただきました。非同期処理だけでなく、JS の基本的な知識から深い部分まで丁寧に解説されているので、これから JS を学習される方にもおすすめの動画教材です。

  • Promise-book
    Promise についてまとまっています。Promise を知りつくしたい!という方にはこちらをおすすめします。

  • async await について学ぶ
    Promise を使うよりも非同期処理をわかりやすく記述できる方法です。こちらもPromise-book【JS】ガチで学びたい人のための JavaScript メカニズム(Udemy)をおすすめしておきます。

  • イベントループ、タスクキュー(マイクロタスク、ミクロタスクについて)
    【JS】ガチで学びたい人のための JavaScript メカニズム(Udemy) こちらも紹介した Udemy の教材がとてもわかりやすかったです。

  • JavaScript の非同期処理をできる限り正確に理解する
    こちらは Qiita の記事ですが、非同期 API である Promise が裏側でどのように動いているかを丁寧に解説されている記事です。
    今回の資料を作成するにあたって、僕自身もわかっていなかったのですが、
    JS はシングルスレッドなのに、なぜ非同期処理を行うと処理が同時に実行できるんだ?Promise の並列処理はどうやって動いているんだ?
    という疑問が解決する記事です。

  • fetch を使ってみる
    Promise や async await の書き方がわかったら、実際に外部から値を取得してみる練習をすると良いです。
    fetch は Promise の理解をしていればそう難しくはないと思います。

いろいろご紹介しましたが、読むだけ・見るだけでは知識は定着しません。
記事を呼んだら、写経でも良いのでまずはコードを書いてみて、各行でconsole.log()を実行してみて
実際に何の値が入っているのか?なぜそう動いているのか?と自分で疑問を持ちながら学習してみてください。
あと、もりけん先生の JS 課題に取り組んでみるのも良いと思います。

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published