Promise と async/await で非同期処理をしてみよう

目次

概要

プログラムからファイルを読み書きしたり、 Web API を実行したりといった処理をするには時間がかかります。
この「時間がかかる処理」を JavaScript で扱うには、JavaScript がコードを実行する順番を知っておく必要があります。
この記事では、コードが実行される順番を知る上で重要な、同期処理や非同期処理、Promise を学んでいきましょう。

kintone カスタマイズでも、「他のアプリのレコードを取得する」「別のアプリにレコードを登録する」といった時間がかかる処理をすることはよくあります。
この記事で扱う内容を理解していないと、他のアプリのレコードを取得してフィールドへセットするような場合に、「うまく値がセットされない」といったことが起こりえます。
同期処理や非同期処理、Promise は、kintone カスタマイズをする上で必須な知識です。

同期処理

JavaScript は、一度に複数の処理を実行できない言語です。
コードの上から順番に処理をして、ひとつの処理が終わるまで次の処理には進みません。
プログラミングの世界では、複数の処理をひとつずつ順番に実行することを、同期処理と呼びます。

JavaScript では、基本的に同期処理で処理が実行されます。
ブラウザーの開発者ツールのコンソールに次のコードをコピー&ペーストして、実行してみましょう。

1
2
console.log('1. 処理を開始します');
console.log('2. 処理を終了します');

すると、「1.〜」「2.〜」の順でコンソールに出力されます。

1
2
// 1. 処理を開始します
// 2. 処理を終了します

同期処理なため、上から順番に処理されました。

直感的でわかりやすいですが、同期処理が問題となるケースがあります。
ブラウザーの開発者ツールのコンソールに次のコードをコピー&ペーストして、実行してみましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 指定時間(ms)が経過するまで、無限ループする関数
function sleep(delay) {
  const startTime = Date.now();
  // 指定ミリ秒間だけループさせる
  while (true) {
    const diffTime = Date.now() - startTime;
    if (diffTime >= delay) {
      return; // 指定した時間が経過したら、関数を終了させる
    }
  }
}

console.log('1. 処理を開始します');
sleep(5000); // 5,000ミリ秒(5秒間)停止する
console.log('2. 処理を終了します');

先ほどと同じように「1.〜」「2.〜」の順でコンソールに出力されます。
ただし、「1.〜」と「2.〜」が出力されるには、5 秒の間があったはずです。
途中の sleep(5000) という関数で 5 秒停止する処理を実行しているからです。

これが 10 秒、1 分、1 時間と待つ場合はどうでしょう?
次の処理の console.log('2. 処理を終了します');sleep() が終わるまで待たされるので、「2.〜」は出力されません。

時間のかかる処理があった場合に、同期処理では、その処理が終わるまで次に進めず、処理がブロックされます。
Web ブラウザーの場合は、画面がフリーズしてしまいクリックしても反応しないといった状態になってしまいます。

非同期処理

処理が終わるまでブロックされてしまう現象を解決するには、非同期処理をします。
上から順番にコードが実行されることは同期処理と同じですが、非同期処理では、ひとつの処理が終わるのを待たずに次の処理を実行します。

setTimeout() という関数は、JavaScript で非同期処理ができる関数の代表例です。
setTimeout() は、delay ミリ秒後に、引数へ渡した関数を呼び出すようにタイマーへ登録する非同期関数です。

1
2
setTimeout(コールバック関数, delay);
// 関数の引数に渡す関数のことをコールバック関数と呼びます。

では、setTimeout を使って、3,000 ミリ秒(3 秒)待ってみましょう。 ブラウザーの開発者ツールのコンソールに次のコードをコピー&ペーストして、実行してみましょう。

1
2
3
console.log('1. 処理を開始します');
setTimeout(() => console.log('3,000 ミリ秒後に呼び出されました'), 3000);
console.log('2. 処理を終了します');

実行すると、コンソールには次のような結果が出力されます。

1
2
3
1. 処理を開始します
2. 処理を終了します
3,000 ミリ秒後に呼び出されました

setTimeout() に指定したコールバック関数(2 行目)は非同期的に実行されるため、コードの並び順とは違う順番で実行されます。

ただし非同期処理では、複数の非同期処理があると、処理される順番が担保されないことに注意しましょう。
ブラウザーの開発者ツールのコンソールに次のコードをコピー&ペーストして、実行してみましょう。

1
2
3
4
console.log('1. 処理を開始します');
setTimeout(() => console.log('3,000 ミリ秒後に呼び出されました'), 3000);
setTimeout(() => console.log('2,000 ミリ秒後に呼び出されました'), 2000);
console.log('2. 処理を終了します');

実行すると、コンソールには次のような結果が出力されます。

1
2
3
4
1. 処理を開始します
2. 処理を終了します
2,000 ミリ秒後に呼び出されました
3,000 ミリ秒後に呼び出されました

非同期処理では処理が終わった順となるため、コードの並び順とは一致しません。
3,000 ミリ秒後に実行する 2 行目の console.log() よりも早く、2,000 ミリ秒後に実行する 3 行目の console.log() が実行されています。

例:コンビニでお弁当を温める

用語を説明するだけでは難しいかもしれないので、同期処理と非同期処理をコンビニの接客でたとえてみましょう。

あなたは、友人と一緒にコンビニへやってきました。
あなたと友人はそれぞれ弁当を購入し、弁当を電子レンジで温めてもらいます。 同期的な接客をするコンビニと非同期的な接客をするコンビニ、それぞれどんな違いがあるでしょうか。

同期的な接客をするコンビニ

同期的な接客をするコンビニでは、同期処理で接客をしているので、店員は一人ずつ接客をします。
あなたの弁当を温め終わるまで、友人が購入した弁当の会計はされません。

非同期的な接客をするコンビニ

非同期的な接客をするコンビニでは、非同期処理で接客をします。
あなたの弁当を温めている途中に、友人の購入した弁当を会計します。
ただし、順番は担保されないので、先に温まった弁当から渡されます。

同期的な接客をするコンビニに比べて待たされることなく、弁当を買うことができました!

Promise

Promise は、ES2015 で導入された非同期処理を行うためのしくみです。
非同期処理が成功したときは、then() のコールバック関数 *1 内で処理結果を受け取ります。
失敗したときは、同じく catch() のコールバック関数内でエラーの結果を受け取ります。

*1 関数の引数に渡す関数のことをコールバック関数と呼びます。 ^

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 非同期処理をする関数 wait
const wait = (ms) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(`${ms} ミリ秒後に呼び出されました`);
      resolve();
    }, ms);
  });
};

// メイン関数
console.log('1. 処理を開始します');
wait(3000)
  .then((resp) => {
    // 非同期処理が成功したときの処理
    console.log('成功しました');
  })
  .catch((err) => {
    // 非同期処理が失敗したときの処理
    console.error('失敗しました');
  });
console.log('2. 処理を終了します');

実行すると、 非同期処理 と同じ順で出力されました。

1
2
3
4
1. 処理を開始します
2. 処理を終了します
3000 ミリ秒後に呼び出されました
成功しました

この例では非同期処理が必ず成功しエラーの結果を受け取ることはないので、catch のコールバック関数内にある 20 行目の「失敗しました」は出力されません。

非同期関数 では、複数の非同期処理があると、処理される順番が担保されないと説明しました。
Promise を使うと、複数の非同期処理があっても順番に処理できます。

3,000 ミリ秒後に「2. 成功しました」を出力し、さらにその 2,000 ミリ秒後に「'3. 成功しました」を出力してみましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
const wait = (ms) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(`${ms} ミリ秒後に呼び出されました`);
      resolve();
    }, ms);
  });
};

// メイン関数
console.log('1. 処理を開始します');
wait(3000).then((resp) => {
  // 非同期処理が成功した時の処理
  console.log('2. 成功しました');
  return wait(2000);
}).then((resp) => {
  // 非同期処理が成功した時の処理
  console.log('3. 成功しました');
});

実行結果は、次のようになります。

1
2
3
4
5
1. 処理を開始します
3000 ミリ秒後に呼び出されました
2. 成功しました
2000 ミリ秒後に呼び出されました
3. 成功しました

async/await

ES2017 で導入された async / await という構文を使うと、よりシンプルに順番を担保しながら非同期処理を書くことができます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const wait = (ms) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(`${ms} ミリ秒後に呼び出されました`);
      resolve();
    }, ms);
  });
};

// メイン関数
(async () => {
  console.log('1. 処理を開始します');
  await wait(3000);
  console.log('2. 成功しました');
  await wait(2000);
  console.log('3. 成功しました');
})();

実行結果は、次のようになります。

1
2
3
4
5
1. 処理を開始します
3000 ミリ秒後に呼び出されました
2. 成功しました
2000 ミリ秒後に呼び出されました
3. 成功しました

async / await という構文では、await 非同期処理をする関数 のように非同期処理をする関数の頭に await をつけます。
すると、成功したときの処理を then() のコールバック関数の中ではなく、その次の行に書くことができます。
そのため「その行が終わったら次の行の処理へ進む」といったように、同期処理のように書くことができます。

ただし await をつける構文は、async を付けた関数の中でしか利用できないことに注意しましょう。
この例の場合は、 11 行目のように、即時関数に async をつけて実行します。

ちなみに、Promise で catch() メソッドに書いていたエラー処理は、try...catch 構文の catch(err){ ... } の括弧の中に書きます。
これは「例外処理」と呼ばれる、例外(エラー)が発生したときの処理方法です。気になった人は「JavaScript 例外処理」などで調べてみてください。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
const wait = (ms) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(`${ms} ミリ秒後に呼び出されました`);
      resolve();
    }, ms);
  });
};

// メイン関数
(async () => {
  try {
    console.log('1. 処理を開始します');
    await wait(3000);
    console.log('2. 成功しました');
    await wait(2000);
    console.log('3. 成功しました');
  } catch (err) {
    console.error('失敗しました');
  }
})();

まとめ

Promise は JavaScript の最難関知識と言っても過言ではありません。
一度にすべてを理解しようとせず、ひとつずつ理解を深めましょう。
次回は Web API について学習しましょう。