feature image

2020年12月17日 | ブログ記事

JavaScriptの非同期処理についてのメモ【AdC2020 33日目】

この記事はアドベントカレンダー2020 33日目の記事です。

こんにちは。20Bのreyuです。普段はググってコピペを繰り返しながらプログラムを書いています。

メモなので内容ガバってても許してください

同期処理と非同期処理

同期処理とは、コードを順番に実行していき、ひとつの処理が終わるまで次の処理は実行しないような処理です。まあ普通の処理です。
非同期処理とは、そうではないような処理です。簡単ですね。

非同期処理のお気持ち

そもそも、どうして非同期処理なんてものは必要なのでしょうか。
非同期処理は、例えば以下のような場面で使われます。

他に止めたくない処理がある場合

例えばブラウザでは、メインスレッドでJavaScriptの処理だけでなくUIに関する処理も行われています。1秒かかる処理を裏で実行したらUIの更新が1秒止まるみたいなのは避けたいですね。

外部で処理をする場合

例えばサーバーにリクエストを投げて、それが1秒後に返ってくるとします。同期処理でこれを行うと1秒間なにもしない時間が発生しそうでなんか嫌ですね。

非同期処理を使う上で

1秒待つ関数 myTimeout() があるとします。ここで、コンソールに ab を1秒ずらして出力したくて以下のようなコードを書いたとします。

console.log('a');
myTimeout(myFunc);
console.log('b');

もしも myTimeout() が同期処理だったとしたら、このコードは想定通りの動作をします。しかし、myTimeout() が非同期処理である場合、ab はほぼ同時にコンソールに出力されます。

今度は、あるテキストを読み込む関数 myGetText() があるとします。同期処理なら以下のように書けば大丈夫そうですね。

try {
  const text = myGetText();
  console.log(text);
} catch (error) {
  //例外処理
}

ただ、非同期処理だとこういう書き方はできません。

まず、2行目のように非同期処理の返り値に処理の結果をもたせることはできません。text に値が代入される時点で myGetText() の処理は終わっている保証がないからです。
また、try...catch 文は非同期処理内の例外をキャッチできません。今回だと console.log() などでの例外はキャッチされますが、myGetText() 中の例外はキャッチされません。

このように、非同期処理では同期処理と同じようには書けない場面があります。これに対応するための方法はいくつかあります。

コールバック関数を使う

コールバック関数とは、ひとことで言うと「引数として渡される関数」のことです。非同期関数に引数として関数を渡すことで、上での問題を解決することができます。

上の例については、myTimeout()myGetText() がどちらもコールバック関数を用いるとすれば、実装によりますがだいたい以下のような書き方で実現できます。

console.log('a');
myTimeout(() => {
  console.log('b');
});
myGetText((error, text) => {
  if (error) {
    //例外処理
  } else {
    console.log(text);
  }
});

これで、非同期処理でだいたいのことができるようになりました。ただ、困ることもたまにあります。

例えば、上の myTimeout() を用いて abcd をそれぞれ1秒ずつ空けてコンソールに出力したいとします。するとこうなります。

console.log('a');
myTimeout(() => {
  console.log('b');
  myTimeout(() => {
    console.log('c');
    myTimeout(() => {
      console.log('d');
    });
  });
});

やばいですね。いわゆるコールバック地獄と言われる状態です。これに例外処理がついたりするともっとやばくなります。

Promise を使う

Promiseは、ES2015で導入された非同期処理の結果を表現するビルドインオブジェクトです。
まず、以下のようにしてPromise インスタンスを返すような関数を作ることができます。

function myPromiseTimeout() {
  return new Promise((resolve) => {
    myTimeout(() => {
      resolve();
    });
  });
}
function myPromiseGetText() {
  return new Promise((resolve, reject) => {
    myGetText((error, text) => {
      if (error) {
        reject(error);
      } else {
        resolve(text);
      }
    });
  });
}

これらの関数を使うことで、コールバックと同じような処理を実現できます。

myPromiseTimeout().then(() => {
  console.log('a');
});
myPromiseGetText().then((text) => {
  console.log(text);
}, (error) => {
  //例外処理
});

このように、then() の第一引数に成功したときの処理、第二引数に失敗したときの処理を渡します。

これを見て、「あれ、別にコールバック地獄解決してなくね?」と思った方もいると思います。確かに、普通に書くとこうなります。

console.log('a');
myPromiseTimeout().then(() => {
  console.log('b');
  myPromiseTimeout().then(() => {
    console.log('c');
    myPromiseTimeout().then(() => {
      console.log('a');
    });
  });
});

ただ、 Promise の場合は解決法があります。

Promiseの then() は新しいPromiseを返します。つまり、then() にさらに then() を繋げるような使い方ができます。

console.log('a');
myPromiseTimeout().then(() => {
  console.log('b');
  return myPromiseTimeout();
}).then(() => {
  console.log('c');
  return myPromiseTimeout();
}).then(() => {
  console.log('d');
});

こう書くとわかりやすいかもしれません。

console.log('a');

const promise1 = myPromiseTimeout().then(() => {
  console.log('b');
  return myPromiseTimeout();
});

const promise2 = promise1.then(() => {
  console.log('c');
  return myPromiseTimeout();
});

promise2.then(() => {
  console.log('d');
});

then() 内で値を返すことで、次の処理に値を渡すこともできます。

myPromiseGetText().then((text) => {
  const newtext = text + 'hoge';
  return newtext;
}).then((text) => {
  console.log(text); //元のtext+hogeが出力される
}).catch((error) => {
  //例外処理
  //then()で発生したエラーはどちらもここでキャッチされる
});

ここで、catch(onRejected)then(undefinded, onRejected) と同じ意味で、失敗時の処理のみを設定することができます。

まあ他にもいろいろあるんで気になった方は調べてください。

async, awaitを使う

async await はES2017で導入されました。これらを用いることで、非同期処理を簡潔に書くことができます。

まず、async function と定義された関数は必ず Promise を返します。例えば、以下の2つは同じ意味となります。

function asyncFunction() {
    return new Promise((resolve) => {
        resolve("hoge");
    });
}
async function asyncFunction() {
    return "hoge"
}

例外として、async function 内で Promise が返された場合、その Promise がそのまま返り値となります。

async function 内では、await 式を使うことができます。簡単に言うと 右辺の Promise の結果が確定するまで次の処理に移らないようにするものです。

ここで、await は非同期処理を同期処理のように扱うため、同期処理のような書き方をすることができます。

具体的には、await は右辺の Promise が成功したとき、Promise ではなくresolveに渡された値を返します。また、失敗した場合は例外を throw します。

つまり、返り値で結果を返すことや、 try...catch で例外をキャッチすることなどができます。

async function asyncFunction() {
  try {
    const text = await myPromiseGetText();
    return text;
  } catch (error) {
    //例外処理
  }
}
async function asyncFunction() {
  console.log('a');
  await myPromiseTimeout();
  console.log('b');
  await myPromiseTimeout();
  console.log('c');
  await myPromiseTimeout();
  console.log('d');
}

わかりやすいですね。

おわり

明日 今日の担当者は @Komichi さんです。お楽しみに!

reyu icon
この記事を書いた人
reyu

いろいろやったりやらなかったりしてます。

この記事をシェア

このエントリーをはてなブックマークに追加
共有

関連する記事

ERC20トークンを用いた宝探しゲーム(真)の提案【アドベントカレンダー2018 10日目】 feature image
2018年11月3日
ERC20トークンを用いた宝探しゲーム(真)の提案【アドベントカレンダー2018 10日目】
Azon icon Azon
2021年5月19日
CPCTF2021を実現させたスコアサーバー
xxpoxx icon xxpoxx
2021年12月8日
C++ with JUCEでステレオパンを作ってみた【AdC2021 26日目】
liquid1224 icon liquid1224
2019年4月22日
アセンブリを読んでみよう【新歓ブログリレー2019 45日目】
eiya icon eiya
2018年4月17日
春休みにゲームを作りました
uynet icon uynet
2017年11月17日
そばやのワク☆ワク流体シミュレーション~MPS編~
sobaya007 icon sobaya007
記事一覧 タグ一覧 Google アナリティクスについて