テクノロジー

2024年11月21日

同期処理と非同期処理[Promiseオブジェクト]

同期処理ではコードを記述順に処理し、一つの処理が終わるまで次の処理を行いません。
それに対して、非同期処理は、コードを記述順に処理していくのは同期処理と同様ですが、一つの非同期処理の終了を待たずに次の処理を行います。

著者プロフィール

IT分野における教育の先駆者として、多くのエンジニアを育成するプログラミングスクールの運営、Web開発やAI研修を行なっています。幅広いレベルの受講生に対して実践的なスキルを提供。生徒の成長を第一に考え、効果的で魅力的な教育プログラムの設計に情熱を注いでいます。

ゴール

  • 同期処理と非同期処理のちがいを理解する
  • Promiseを使い、非同期処理における遅延処理ができるようになる

同期処理と非同期処理の違い

JavaScriptのコードの処理には2種類あります。同期処理非同期処理 です。
同期処理ではコードを記述順に処理し、一つの処理が終わるまで次の処理を行いません。
それに対して、非同期処理は、コードを記述順に処理していくのは同期処理と同様ですが、一つの非同期処理の終了を待たずに次の処理を行います。

同期処理の例

以下は、一つの処理が終わるまで次の処理を行わない同期処理のコードの例です。

function sleep() {
  console.log("sleep開始");
  const startTime = new Date(); // -- 1.実行時点の時刻を生成
  while (true) {
    // 2. 1で生成した時間から3000ミリ秒経過しないとwhileを抜け出さない
    if (new Date() - startTime > 3000) {
      console.log("sleep終了");
      return;
    }
  }
}

console.log("処理を開始");
sleep();
console.log("処理を終了");

sleep()関数は3,000ミリ秒(3秒)間処理を止める関数です。
sleep()実行中であることがわかるように、sleep()関数の中で、開始と終了に”sleep開始”と”sleep終了”を出力させています。

sleep()関数の中のnew Date()では、組み込みコンストラクタである Dateコンストラクタ を使用して、実行時点の日時の情報が格納されたオブジェクトを生成しています。上記の例のように、生成されたDateオブジェクト同士は、算術演算子で差分のミリ秒を算出したり、比較演算子で比較できます。

(例)

console.log(new Date);

実行結果

Thu Sep 17 2020 09:51:06 GMT+0900 (日本標準時)

同期処理のコードを実行して、Console画面で確認してみましょう。
以下のように、”処理を開始”,”sleep開始”と出力されてから3秒後に”sleep終了”,”処理を終了”と出力されます。

Image from Gyazo

これは、sleep()関数実行中は、次のconsole.log("処理を終了");が実行されないためです。

非同期処理の例

では、この3秒待つ処理を「非同期処理」で書いてみましょう。
非同期処理の代表的なものが、setTimeout()関数による処理です。

setTimeout()は、第一引数にコールバック関数(実行する関数)、第二引数に 何ミリ秒後にその関数を実行するか を指定します。
つまりsetTimeout()は、指定されたミリ秒後にコールバック関数を実行する非同期処理を行う関数といえます。

setTimeout(実行する関数, 何ミリ秒にその関数を実行するか)

setTimeout()関数のコールバック関数として、Consoleに”3秒経ちました”と表示する関数を設定します。

console.log("処理を開始");
setTimeout(function() {
  console.log("3秒経ちました");
}, 3000);
console.log("処理を終了");

実行結果

Image from Gyazo

このように、”処理を開始”,”処理を終了”と出力されてから、”3秒経ちました”と出力されました。
これは以下のような流れになっています。

  1. setTimeout()関数が、3,000ミリ秒後にコールバック関数(ここではconsole.log("3秒経ちました");)の実行タイマー登録だけを行う。
  2. その処理を待たずに、次のconsole.log("処理を終了");が実行されている。

上記のsetTimeout()関数を使った処理を、以下の順番で処理をしたい場合を考えましょう。

  1. “処理を開始”
  2. “3秒経ちました”
  3. “処理を終了”

その場合、setTimeout()関数のコールバック関数に、さらにsetTimeout()関数を記述する必要があります。このようにsetTimeout()関数の中でsetTimeout()関数を呼ぶような処理を ネスト といいます。
ではネストで上記の順番で処理をしてみましょう。

console.log("処理を開始");  // --A
setTimeout(function() {  // --B
  setTimeout(function() { // --C
    console.log("処理を終了"); // --D
  }, 0);
  console.log("3秒経ちました"); // --E
}, 3000);

実行結果

Image from Gyazo

以下のような流れで処理が実行されています。

  1. console.log("処理を開始");を実行(A)。
  2. 3,000ミリ秒後に中のsetTimeout()関数を実行するタイマー登録を行う(B)。
  3. 3,000ミリ秒経過。
  4. (B)でタイマー登録された処理が実行され、0ミリ秒後にconsole.log("処理を終了")を実行するタイマー登録(C)と、console.log("3秒経ちました");(E)が実行される。
    setTimeout()はタイマー登録をするだけで、その後に処理が続いている場合は、以降の処理の実行に移る。ここでは0ミリ秒後に指定していますが、その後に(E)の処理が続いています。そのため、その処理の終了を待って0ミリ秒後以降のできるだけ早くに実行される。
  5. (C)でタイマー登録されたconsole.log("処理を終了")(D)が実行される。

ネストが深くなるにつれて、非常にわかりづらいコードになってしまいました。
以下の例は、1秒ごとに”○秒経ちました”と表示させるコードの例です。

console.log("処理を開始");
setTimeout(function() {
  setTimeout(function() {
    setTimeout(function() {
      setTimeout(function() {
            console.log("処理を終了");
          }, 1000);
        console.log("3秒経ちました");
      }, 1000);
    console.log("2秒経ちました");
  }, 1000);
  console.log("1秒経ちました");
}, 1000);

実行結果

Image from Gyazo

このように、コールバック関数を多重のネストにすると、「コールバック地獄」といわれ、非常にわかりづらいコードになってしまいます。

Promiseを使った非同期処理の遅延

ここまで学んだように、setTimeout()関数のコールバック関数や、クライアントサイドJavaScriptで使用するaddEventListener()に渡したコールバック関数など(基礎文法シリーズでは扱っていません)、JavaScriptには非同期的に行われる処理があります。
そのような処理の中で、上記のようなコールバック地獄に陥らないように使用するのが、非同期処理を扱うPromiseと呼ばれる組み込みオブジェクトです。
Promiseは非同期処理を実行し、その処理が終了するまで、次の処理を遅延させるために使用されます。

Promiseは組み込みオブジェクトであるため、以下のようにコンストラクタを使って生成します。

const promise = new Promise(function(resolve, reject) { 処理内容 })

new Promise()の引数には、実行する処理を記載した関数を指定します。
指定された関数には、 resolvereject という引数が渡されます。引数として渡されますが、この二つはコールバック関数です。

resolve()
指定された関数が正常に処理された時に呼ばれ、Promiseを正常終了させるコールバック関数。
resolve()で終了したPromiseは、then()メソッドを使って次の処理に繋ぐことができる。
then()メソッドの引数には、続けて実行したい関数を指定する。

reject()
指定された関数が失敗した時に呼ばれ、Promiseをエラー終了させるコールバック関数。
reject()で終了したPromiseは、catch()メソッドを使ってエラー処理に繋ぐことができる。
catch()メソッドの引数には、続けて実行したい関数を指定する。

Promiseが正常に終了した場合[resolve()関数とthen()メソッド]

では、処理が成功したときの例を確認しながら、resolve()関数とthen()メソッドの使い方をみていきましょう。

実際の動きを確認するため、同期処理の例で使用したsleep()関数を、setTimeout()関数の中で使用してみます。
sleep()関数を、0ミリ秒後に実行するように指定しています。

// 3000ミリ秒間処理を止める関数
function sleep() {
  console.log("sleep開始");
  const startTime = new Date();
  while (true) {
    if (new Date() - startTime > 3000){
      console.log("sleep終了");
      return;
    }
  }
}

console.log("処理を開始");
setTimeout(function() {
  sleep();
}, 0);
console.log("処理を終了");

実行結果

Image from Gyazo

このように、”処理を開始”,”処理を終了”と出力されてから、”sleep開始”“sleep終了”と出力されました。
これはsetTimeout()関数がsleep()関数の実行タイマー登録だけを行い、実際にsleep()関数処理を待たずに、次のconsole.log("処理を終了");が実行されているからです(非同期処理)。

この処理を、Promiseを使用して同期的に処理するようにしてみましょう。

function sleep() {
  console.log("sleep開始");
  const startTime = new Date();
  while (true) {
    if (new Date() - startTime > 3000){
      console.log("sleep終了");
      return;
    }
  }
}

console.log("処理を開始");

const promise = new Promise(function(resolve, reject) {
  setTimeout(function() {
    sleep();
    resolve(); // --1 
  }, 0);
})

promise.then(function() { // --2
  console.log("成功しました");
})

実行結果

Image from Gyazo

1の部分でresolve()が設定されています。これはsleep()が正常に実行された場合に実行されます。resolve()が実行されると、2のthen()メソッドで指定された関数が呼ばれます。

もしPromiseを使用しなければ、setTimeout()関数によってコールバック関数が非同期的に処理され、先にconsole.log("成功しました")という処理が実行されていたはずです。
今回のコードではPromiseを使用したため、sleep()関数の実行を待って、console.log("成功しました")という処理が実行されたことがわかります。

resolve()関数に引数を渡す

resolve()関数には引数を渡すことができ、その引数はthen()メソッドで指定された関数に渡されます。
以下の例では、resolve()関数に”3秒経過”という文字列を引数として登録し、それがthen()メソッドの関数の中で使われている例です。

function sleep() {
  console.log("sleep開始");
  const startTime = new Date();
  while (true) {
    if (new Date() - startTime > 3000){
      console.log("sleep終了");
      return;
    }
  }
}

console.log("処理を開始");

const promise = new Promise(function(resolve, reject) {
  setTimeout(() => {
    sleep();
    resolve("3秒経過"); // --1 "3秒経過"という文字列を引数に登録
  }, 0);
})

promise.then(function(str){ // --2 resolve()の引数をstrで受け取る
  console.log(`${str}成功しました`); // --3 引数strを関数の中で使用
})

実行結果

Image from Gyazo

Promiseがエラー終了した場合[reject()関数とcatch()メソッド]

今度は処理が失敗したときの例を確認しながら、resolve()関数とcatch()メソッドの使い方をみていきましょう。
以下の例では、promiseの内容を「sleep()関数の結果がtrueの場合は正常終了、そうでなければエラー」という条件分岐にしています。
今回はreject()の動きを確認するため、sleep()関数の結果がfalseになるよう、sleep()関数の内容を変更しています。

function sleep() {
  console.log("sleep開始");
  const startTime = new Date();
  while (true) {
    if (new Date() - startTime > 3000){
      console.log("sleep終了");
      return false; // --1 falseを返却するよう変更
    }
  }
}

console.log("処理を開始");

const promise = new Promise(function(resolve, reject) {
  setTimeout(function() {
    if (sleep()) { // --2 sleep()実行によりfalseが返却される
      resolve(); // sleep()がtrueの場合は正常終了としてresolve()実行
    } else {
      reject(); // sleep()がtrueでない場合はエラーとしてreject()実行
    }
  }, 0);
})

promise
.then(function(){ // --3 resolve()実行時に呼ばれる
  console.log("成功しました")
})
.catch(function() { //--4 reject()実行時に呼ばれる
  console.log("失敗しました")
})

実行結果

Image from Gyazo

setTimeout()の中で実行されたsleep()関数の結果がfalseであったため、reject()が実行されました。
reject()が実行されると、catch()メソッドが呼ばれます。
例の3と4のように、then()メソッドとcatch()メソッドはpromiseに繋げて記述できます。このように、処理が正常に終了したかエラー終了したかによって、その後の処理を複数用意しておくことが一般的です。

上記の例で、sleep()関数の戻り値をtrueに変更すると、resolve()関数が呼ばれます。自身の環境で試してみてください。

reject()関数に引数を渡す

reject()関数とcatch()メソッドにも、引数を設定できます。
使い方は、resolve()関数とthen()メソッドと同様です。

function sleep() {
  console.log("sleep開始");
  const startTime = new Date();
  while (true) {
    if (new Date() - startTime > 3000){
      console.log("sleep終了");
      return false;
    }
  }
}

console.log("処理を開始");

const promise = new Promise(function(resolve, reject) {
  setTimeout(function() {
    if (sleep()) {
      resolve("trueです");
    } else {
      reject("falseです");
    }
  }, 0);
})

promise
.then(function(str){
  console.log(`${str} 成功しました`);
})
.catch(function(str) {
  console.log(`${str} 失敗しました`);
})

実行結果

Image from Gyazo

Promiseを関数に組み入れる

ここまではnew Promise()で生成したpromiseオブジェクトを使用した例をみてきましたが、promiseは関数でないため引数が取れません。
引数をとる関数として活用する例を確認していきましょう。

以下の例では、inputNumber()関数を定義し、その戻り値としてPromiseオブジェクトを生成して返却しています。
このようにすることによって、inputNumber()関数実行時に引数をとることができるようになります。

function inputNumber(data) {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      if (typeof(data) == "number") {
        resolve(data + 1);
      } else {
        reject("数字ではありません");
      }
    }, 3000);
  })
}

console.log("処理を開始");

inputNumber(1)
.then(function(data){
  console.log(`${data}です`);
})
.catch(function(error) {
  console.log(error);
})

実行結果

Image from Gyazo

inputNumber()関数の引数に数字が渡されると、正常終了としてその数字に1を足した数字が表示されます。
数字以外が入力された場合はエラー終了となり、”数字ではありません”というエラーメッセージが出力されます。

Promiseチェーンで処理を繋げる

then()メソッドを繋げて、処理を順番に行うこともできます(Promiseチェーン)。
下記の例ではinputNumber()関数を3回続けて実行しています。

function inputNumber(data) {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      if (typeof(data) == "number") {
        resolve(data + 1);
      } else {
        reject("数字ではありません");
      }
    }, 3000);
  })
}

console.log("処理を開始");
inputNumber(1)
.then(function(data){
  console.log(data);
  return inputNumber(data);
})
.then(function(data){
  console.log(data);
  return inputNumber(data)
})
.then(function(data){
  console.log(data);
})

実行結果

Image from Gyazo

then()メソッドの中にreturn inputNumber(data)を記述することで、inputNumber()関数を呼び出しているため、その結果を再度then()メソッドの中で使用できています。

複数の非同期処理を並列処理[Promise.all()]

Promise.all()メソッドは、複数の非同期処理を並列して処理し、全ての処理が正常終了した場合にのみthen()メソッドにつなげることができます。
以下のように、引数にpromiseオブジェクトの配列を渡して実行します。

Promise.all([promiseオブジェクト, promiseオブジェクト, ・・])

Promise.all()メソッドを実行すると、引数に指定したすべてのpromiseオブジェクトが実行されます。全て実行し、全てが正常終了したときにthen()メソッドが実行されます。時間がかかる処理を並列に実行することにより、時間を節約するときなどに使用します。

先ほどのinputNumber()関数の例で確認してみましょう。

(全て成功する例)

function inputNumber(data) {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      if (typeof(data) == "number") {
        console.log(data)
        resolve(data);
      } else {
        console.log("数字ではありません")
        reject("失敗しました");
      }
    }, 3000);
  })
}

console.log("処理を開始");

Promise.all([
  inputNumber(2),
  inputNumber(6),
  inputNumber(5),
])
.then(function(){
  console.log("全部成功しました");
})
.catch(function(error) {
  console.log(error);
})

実行結果

Image from Gyazo

Promise.all()関数の中の処理が全て成功したため、then()メソッドが呼ばれ、”全部成功しました”と出力されました。

一つでも失敗すると、その時点でcatch()メソッドが呼ばれます。

(失敗する例)

function inputNumber(data) {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      if (typeof(data) == "number") {
        console.log(data)
        resolve(data);
      } else {
        console.log("数字ではありません")
        reject("失敗しました");
      }
    },3000);
  })
}

console.log("処理を開始");

Promise.all([
  inputNumber(2),
  inputNumber("hello"), //数字以外を引数に渡す
  inputNumber(5),
])
.then(function(){
  console.log("全部成功しました");
})
.catch(function(error) {
  console.log(error);
})

実行結果

Image from Gyazo

inputNumber("hello")で失敗したため、catch()メソッドが呼ばれました。

まとめ

  • JavaScriptにはコードを記述順に処理していき、一つの処理が終わるまで次の処理を行わない同期処理と、処理の終了を待たずに次の処理を行う非同期処理がある
  • Promiseオブジェクトは非同期処理を実行し、その処理が終了するまで次の処理を遅延できる
  • resolve()関数はPromiseを正常終了させるコールバック関数であり、then()メソッドを使って次の処理に繋ぐことができる
  • reject()関数はPromiseをエラー終了させるコールバック関数であり、catch()メソッドを使ってエラー処理に繋ぐことができる

公式ドキュメント・参考情報

1. setTimeout()
2. Promise
3. resolve()
4. then()
5. reject()
6. catch()
7. Promise.all()

ダイビックのことをもっと知ってみませんか?