3
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【完全理解】JavaScriptの非同期処理 —「待てない人」のための処理術

Posted at

🤔 はじめに:非同期って何?コーヒーショップで考えよう

JavaScriptのような「シングルスレッド」の言語は、一度に一つのことしかできません。想像してみてください:

あなたは一人でコーヒーショップを経営しています。お客さんAが注文したラテを作っている間、他のお客さんは全員待たなければなりません。営業は大失敗。破産確定です...😱

非同期処理はこの問題を解決します!あなたは魔法の「後でやる」能力を手に入れました。

お客さんAが注文:「ラテください」
あなた:「かしこまりました!(コーヒーマシンをセット)できあがるまで少々お待ちください」
その間に、お客さんBの注文も受けられる!
コーヒーマシンから「ピンポーン🔔」と音がしたら、ラテを渡す

つまり非同期処理とは、「完了を待たずに次の作業に進み、完了したらその結果を使う」テクニックなのです。

👴 第一世代:コールバック関数(2010年頃)

—「終わったら電話してね」方式

// 昔ながらのコールバック方式
function コーヒーを注文する(コーヒーの種類, コールバック) {
  console.log(`${コーヒーの種類}を準備中...⏳`);
  
  // コーヒーが完成する時間をシミュレート
  setTimeout(() => {
    console.log(`${コーヒーの種類}ができました!☕`);
    コールバック(`熱々の${コーヒーの種類}`);
  }, 2000); // 2秒かかる
}

// 使ってみよう!
console.log("🏪 コーヒーショップへようこそ!");

コーヒーを注文する("カフェラテ", (完成したコーヒー) => {
  console.log(`お客さん:${完成したコーヒー}をいただきました!😋`);
});

console.log("レジ係:次のお客さんどうぞ~"); // ここが先に実行される!

実行結果:

🏪 コーヒーショップへようこそ!
カフェラテを準備中...⏳
レジ係:次のお客さんどうぞ~
カフェラテができました!☕
お客さん:熱々のカフェラテをいただきました!😋

🤮 コールバック地獄 —「ロシアンネスティングドール」問題

朝食を作る(() => {
  歯を磨く(() => {
    着替える(() => {
      通勤する(() => {
        仕事する(() => {
          昼食を食べる(() => {
            // もう見るのも嫌...😵
          });
        });
      });
    });
  });
});

これでは「インデントおじさん」になってしまいます。そこで...

🤵 第二世代:Promise(2015年〜)

—「予約券」方式

// モダンなPromise方式
function コーヒーを注文する(コーヒーの種類) {
  return new Promise((resolve, reject) => {
    console.log(`${コーヒーの種類}を準備中...⏳`);
    
    if (コーヒーの種類 === "存在しないコーヒー") {
      reject(new Error("そんなコーヒーはメニューにありません!"));
      return;
    }
    
    // コーヒーが完成する時間をシミュレート
    setTimeout(() => {
      console.log(`${コーヒーの種類}ができました!☕`);
      resolve(`熱々の${コーヒーの種類}`);
    }, 2000); // 2秒かかる
  });
}

// 使ってみよう!
console.log("🏪 コーヒーショップへようこそ!");

コーヒーを注文する("エスプレッソ")
  .then((完成したコーヒー) => {
    console.log(`お客さん:${完成したコーヒー}をいただきました!😋`);
    return コーヒーを注文する("カプチーノ"); // 次のコーヒーを注文
  })
  .then((次のコーヒー) => {
    console.log(`お客さん:${次のコーヒー}も美味しい!🥰`);
  })
  .catch((エラー) => {
    console.error(`問題発生!:${エラー.message} 😱`);
  })
  .finally(() => {
    console.log("お会計はレジでお願いします 💰");
  });

console.log("バリスタ:次のご注文どうぞ!"); // これが先に実行される!

🚀 第三世代:async/await(2017年〜)

—「魔法の待機」方式

// 最新のasync/await方式(裏はPromise)
async function 朝のルーティン() {
  try {
    console.log("🌞 おはようございます!朝の準備を始めます");
    
    const 朝食 = await 朝食を作る();  // 完了するまで待機
    console.log(`${朝食}を食べました!`);
    
    const  = await 歯を磨く();  // 完了するまで待機
    console.log(`${}がピカピカになりました!`);
    
    console.log("さあ、出勤しましょう!🚶‍♂️");
  } catch (エラー) {
    console.error(`問題発生!:${エラー.message} 😱`);
  }
}

// これすごく読みやすい!👍
// シンプルな同期コードみたいに書けるけど、裏では非同期で動いている

🔍 実例:「推しの画像検索アプリ」を作ってみよう

ボタンをクリックすると、Unsplash APIから「猫」の画像をランダムで表示するアプリを考えてみましょう:

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>推し猫検索アプリ🐱</title>
  <style>
    body { 
      font-family: "Hiragino Kaku Gothic ProN", sans-serif; 
      text-align: center;
      background-color: #f5f5f5;
      padding: 20px;
    }
    .container {
      max-width: 600px;
      margin: 0 auto;
      background: white;
      padding: 20px;
      border-radius: 10px;
      box-shadow: 0 0 10px rgba(0,0,0,0.1);
    }
    .cat-img {
      max-width: 100%;
      border-radius: 5px;
      margin-top: 20px;
      box-shadow: 0 0 5px rgba(0,0,0,0.2);
    }
    button {
      background: #ff6b6b;
      color: white;
      border: none;
      padding: 10px 20px;
      font-size: 16px;
      border-radius: 5px;
      cursor: pointer;
      transition: 0.3s;
    }
    button:hover {
      background: #ff5252;
      transform: translateY(-2px);
    }
    .loader {
      border: 5px solid #f3f3f3;
      border-top: 5px solid #ff6b6b;
      border-radius: 50%;
      width: 50px;
      height: 50px;
      animation: spin 1s linear infinite;
      margin: 20px auto;
      display: none;
    }
    @keyframes spin {
      0% { transform: rotate(0deg); }
      100% { transform: rotate(360deg); }
    }
    .error {
      color: #ff5252;
      font-weight: bold;
    }
  </style>
</head>
<body>
  <div class="container">
    <h1>🐱 推し猫検索アプリ 🐱</h1>
    <p>ボタンを押して、かわいい猫ちゃんを召喚しよう!</p>
    <button id="cat-button">にゃんこ召喚!</button>
    <div id="loader" class="loader"></div>
    <div id="error" class="error"></div>
    <div id="cat-container"></div>
  </div>

  <script>
    document.addEventListener('DOMContentLoaded', () => {
      const button = document.getElementById('cat-button');
      const loader = document.getElementById('loader');
      const errorDiv = document.getElementById('error');
      const catContainer = document.getElementById('cat-container');
      
      button.addEventListener('click', async () => {
        try {
          // リセット
          catContainer.innerHTML = '';
          errorDiv.textContent = '';
          loader.style.display = 'block';
          button.disabled = true;
          
          // 🔍 猫の画像を取得(非同期処理)
          const catData = await fetchRandomCat();
          
          // 画像表示
          const img = document.createElement('img');
          img.src = catData.urls.regular;
          img.alt = catData.alt_description || '猫の画像';
          img.className = 'cat-img';
          
          // ちょっとした演出(猫を待たせすぎないように!)
          setTimeout(() => {
            loader.style.display = 'none';
            catContainer.appendChild(img);
            button.disabled = false;
            
            // 猫の鳴き声を追加(ちょっとしたおまけ)
            const meows = ['にゃー!', 'みゃお〜', 'にゃん♪', 'ふにゃ〜'];
            const randomMeow = meows[Math.floor(Math.random() * meows.length)];
            const meowEl = document.createElement('p');
            meowEl.textContent = randomMeow;
            catContainer.appendChild(meowEl);
          }, 500);
          
        } catch (error) {
          loader.style.display = 'none';
          errorDiv.textContent = `😿 にゃんこ召喚に失敗: ${error.message}`;
          button.disabled = false;
        }
      });
      
      // 猫の画像を取得する関数
      async function fetchRandomCat() {
        // 注意: 実際のアプリではUnsplashのAPI keyが必要です
        const response = await fetch('https://api.unsplash.com/photos/random?query=cat&client_id=YOUR_API_KEY');
        
        if (!response.ok) {
          throw new Error(`HTTPエラー: ${response.status}`);
        }
        
        return await response.json();
      }
    });
  </script>
</body>
</html>

📋 非同期処理を使うときの7つのベストプラクティス

  1. エラーハンドリングは必ず書こう
    「エラーなんて起きないよ」と思った瞬間に限って、必ず何かが壊れます。

    try {
      const data = await 危険な関数();
    } catch (error) {
      console.error('お詫び:想定外のニャンコ発生', error);
      // ユーザーにも優しく伝える
    }
    
  2. Loading状態を表示しよう
    ユーザーは「何が起きているのか」を知りたがっています。「処理中...」の表示があるだけで、イライラ度は激減します。

  3. タイムアウトを設定しよう
    永遠に待ち続けるのはやめましょう。

    // 10秒でタイムアウトする関数
    function withTimeout(promiseFunc, timeoutMs = 10000) {
      return Promise.race([
        promiseFunc(),
        new Promise((_, reject) => 
          setTimeout(() => reject(new Error('タイムアウトしました😵')), timeoutMs)
        )
      ]);
    }
    
  4. Promise.all()で並列処理
    独立した複数の処理は、同時に実行しましょう。

    // 朝の準備を並列でやっちゃう!(実際はできないけど...)
    async function 効率的な朝() {
      const [コーヒー, ニュース, 天気] = await Promise.all([
        コーヒーを入れる(),
        ニュースを読む(),
        天気を確認する()
      ]);
      
      console.log(`${天気}の朝に${コーヒー}を飲みながら${ニュース}を読んでいます`);
    }
    
  5. キャンセル可能な非同期処理
    最新のブラウザなら、AbortControllerが使えます。

    const controller = new AbortController();
    const signal = controller.signal;
    
    // キャンセルボタンを押したとき
    cancelButton.addEventListener('click', () => {
      controller.abort(); // 処理をキャンセル!
    });
    
    try {
      const response = await fetch('https://api.example.com/data', { signal });
      const data = await response.json();
    } catch (error) {
      if (error.name === 'AbortError') {
        console.log('ユーザーによってキャンセルされました');
      } else {
        console.error('エラー発生:', error);
      }
    }
    
  6. デバウンス/スロットリングを使う
    検索入力欄などでは、ユーザーのタイピングごとにAPIリクエストを飛ばすのは非効率です。

    // 検索入力の200ms後にのみAPIを呼び出す
    function debounce(func, wait = 200) {
      let timeout;
      return function(...args) {
        clearTimeout(timeout);
        timeout = setTimeout(() => func.apply(this, args), wait);
      };
    }
    
    const debouncedSearch = debounce((query) => {
      searchAPI(query);
    });
    
    // 入力欄のイベントリスナー
    searchInput.addEventListener('input', (e) => {
      debouncedSearch(e.target.value);
    });
    
  7. 状態管理はきちんと設計
    大規模なアプリでは、非同期処理の状態管理が重要です。

    // シンプルな状態管理の例
    const state = {
      data: null,
      loading: false,
      error: null
    };
    
    function updateState(newState) {
      Object.assign(state, newState);
      renderUI(); // UIの再描画
    }
    
    async function fetchData() {
      updateState({ loading: true, error: null });
      
      try {
        const data = await apiCall();
        updateState({ data, loading: false });
      } catch (error) {
        updateState({ error, loading: false });
      }
    }
    

🧠 非同期マスターへの道:思考法

非同期処理をマスターするには、「料理人思考」が役立ちます。料理人は複数の料理を同時に進行させながら、それぞれの完成時間を考慮して効率よく仕事をします。

シェフの秘訣:
「肉を焼いている間にソースを作り、ソースを煮詰めている間に皿を温め...」

同様に、JavaScriptでも「この処理が完了するまでの間に他の何ができるか」と考えることで、効率的なコードが書けるようになります。

🎯 まとめ:非同期処理の進化

JavaScriptの非同期処理は、「コールバック → Promise → async/await」と進化してきました。進化の目的は常に「人間にとって読みやすく、書きやすいコード」です。

コールバック:「終わったら電話するね」
Promise:「ここに予約券があるよ。完成したらこれと交換してね」
async/await:「ちょっと待っててね(魔法の杖を振る)はい、できたよ!」

今日から非同期処理の考え方を意識して、スムーズに動作するWebアプリケーションを作ってみましょう!そして何より、awaitを使うときは、コードを書いているあなた自身も「せっかちにならず」に待つことを忘れないでくださいね😉


🐾 おまけ:非同期処理あるある

  • 「あれ?データが空...」: awaitを忘れたせいでPromiseオブジェクトをそのまま使おうとしてる
  • 「なんでエラーが出ないの...?」: .catch()を書き忘れたので、エラーが闇に消えている
  • 「この変数、なんでundefinedなの...」: 非同期処理の外で結果を使おうとしている
  • 「何回もAPIが呼ばれるんだけど...」: デバウンス処理を忘れている

JavaScriptの非同期処理はときに不思議な動きをしますが、基本を理解すれば怖くありません。むしろ、「裏で勝手に働いてくれる小人たち」と思えば、とても便利な機能です!

3
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?