JavaScriptの同期処理・非同期処理


※この記事の内容はJavaScriptの同期、非同期、コールバック、プロミス辺りを整理してみるを参考に、自分用にまとめただけなので、こちらのリンクを辿って参考にした方が良いです.


JavaScriptの同期処理・非同期処理

Javascriptはシングルスレッド

  • Javascriptはシングルスレッドで動いている.
  • 同期であろうと非同期であろうと2つ以上の処理を同時に行なうことはできない.
  • JavaScriptでは、キューに登録された関数が順番にひとつずつ実行されていく.
  • でもキューに登録される順番が同期であったり非同期であったりする.

まとめると - JavaScriptは並列処理できない:1度に1つの仕事しかできない。 - JavaScriptは非同期処理できる:誰か(DB等)に仕事を任せている間に自分の他の仕事を進めることができる

同期処理・非同期処理

  • 同期処理
    • 書いた順番に実行されていく.
    • 重たい処理が間にあると、そこで大きな待ち時間が生まれる.

同期処理の例

console.log(1);
console.log(2);
console.log(3);

実行結果

1
2
3
  • 非同期処理
    • あるタスクが実行をしている際に、他のタスクが別の処理を実行できる方式.
    • 例えばサーバーと通信を行った際に、リクエストが返ってくるまでに数秒以上もかかる場合、レスポンスが返るまでに一旦関数から抜けて別の処理を進めて、レスポンスを受け取り次第、呼び出し元に値を返す.

非同期処理の例

console.log(1);
setTimeout(function(){console.log(2)}, 1000);
console.log(3);

キューの登録順

console.log(1);
setTimeout(function(){console.log(2)}, 1000); //★
console.log(3);
// 1000ミリ秒後に★で登録された新たなキュー↓
console.log(2)

実行結果

1
3
2

関数setTimeoutに無名関数function(){console.log(2)}が渡ったように、ある関数Aの引数に別の関数Bを渡し、AからBを呼び出すことをコールバックという。

非同期処理の例2

var xhr = new XMLHttpRequest();
xhr.open('GET','何かのAPI');
xhr.send();
xhr.addEventListener('load', function(result){
  console.log(1);
});
console.log(2);

キューの登録順

xhr.open('GET','何かのAPI');
xhr.send();
xhr.addEventListener('load', function(result){
  console.log(1); // ★
});
console.log(2);
console.log(1); // ★

addEventListener()でloadイベントが発生した時点でlog(1)がキューに登録される. これはその前の一連の関数とは関係ないタイミングでキューに登録されるので非同期.

実行結果

2
1

つまり、JavaScriptXMLHttpRequestを使って外部と通信している間に、次の自分の仕事であるconsole.log(2)を進めている事になる. これにより、JavaScriptのスレッドでやらなくても良い仕事を外部にやらせてる間にJavaScriptのスレッドが自分の仕事を進められる.

同期処理と非同期処理の比較

複数行の処理を行う上で、同期処理は簡潔だが、非同期処理は複雑になる。

複数行の同期処理

関数の例

var syncBuyApple = function(payment){
  if(payment >= 150){
    return {change:payment-150, error:null};
  }else{
    return {change:null, error:150-payment + '円足りません。'};
  }
}

複数行の同期処理

var result1 = syncBuyApple(500);
if(result1.change !== null){
  console.log('1つ目のおつりは' + result1.change + '円です。');
}
if(result1.error !== null){
  console.log('1つ目でエラーが発生しました:' + result1.error);
}
var result2 = syncBuyApple(result1.change);
if(result2.change !== null){
  console.log('2つ目のおつりは' + result2.change + '円です。');
}
if(result2.error !== null){
  console.log('2つ目でエラーが発生しました:' + result2.error);
}
var result3 = syncBuyApple(result2.change);
if(result3.change !== null){
  console.log('3つ目のおつりは' + result3.change + '円です。');
}
if(result3.error !== null){
  console.log('3つ目でエラーが発生しました:' + result3.error);
}
var result4 = syncBuyApple(result3.change);
if(result4.change !== null){
  console.log('4つ目のおつりは' + result4.change + '円です。');
}
if(result4.error !== null){
  console.log('4つ目でエラーが発生しました:' + result4.error);
}

実行結果

1つ目のおつりは350円です。
2つ目のおつりは200円です。
3つ目のおつりは50円です。
4つ目でエラーが発生しました:100円足りません。

複数行の非同期処理

関数の例 今度はreturnではなく1秒後にコールバックでおつりを受け取る

//150円のりんごを1つ買う関数
//第一引数に支払い金額
//第二引数にコールバック関数
//おつりを計算してコールバック関数に渡す
var asyncBuyApple = function(payment, callback){
  setTimeout(function(){
    if(payment >= 150){
      callback(payment-150, null);
    }else{
      callback(null, '金額が足りません。');
    }
  }, 1000);
}

複数行の非同期処理

//りんごをたくさん買う場合(コールバック地獄)
asyncBuyApple(500, function(change, error){
  if(change !== null){
    console.log('1回目のおつりは' + change + '円です。');
    asyncBuyApple(change, function(change, error){
      if(change !== null){
        console.log('2回目のおつりは' + change + '円です。');

        asyncBuyApple(change, function(change, error){
          if(change !== null){
            console.log('3回目のおつりは' + change + '円です。');
          }
          if(error !== null){
            console.log('3回目でエラーが発生しました:' + error);
          }
        });
      }
      if(error !== null){
        console.log('2回目でエラーが発生しました:' + error);
      }
    });
  }
  if(error !== null){
    console.log('1回目でエラーが発生しました:' + error);
  }
});

実行結果

1つ目のおつりは350円です。
2つ目のおつりは200円です。
3つ目のおつりは50円です。

コールバックを続けて行なうとネストがかなり深くなる. これがコールバック地獄と呼ばれている.

Promise

コールバック地獄を回避する方法として登場したのがPromise.

var promiseBuyApple = function(payment){
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            if (payment >= 150) {
                resolve(payment - 150);
            } else {
                reject('金額が足りません。');
            }
        }, 1000);
  });
}

この関数はPromiseオブジェクトを返す. Promiseオブジェクトのコンストラクタに渡す関数には、成功した場合に実行する関数と、失敗した場合に実行する関数を渡す. この例の場合は、成功した場合にはresolve、失敗した場合にはrejectが実行される.

Promise.prototype.then() メソッドと Promise.prototype.catch() メソッドもまた Promise を返すので、これらをチェーン (連鎖) させることができる.

Promiseを使い簡潔になった例

promiseBuyApple(500).then(function(change){
    console.log('おつりは' + change + '円です');
    // 成功なら次のthenへ
    // 失敗ならcatchへ
    return promiseBuyApple(change);
}).then(function(change){
    console.log('おつりは' + change + '円です');
    // 成功なら次のthenへ
    // 失敗ならcatchへ
    return promiseBuyApple(change);
}).then(function(change){
    console.log('おつりは' + change + '円です');
    // 成功なら次のthenへ
    // 失敗ならcatchへ
    return promiseBuyApple(change);
}).catch(function(error){
  console.log('エラーが発生しました:' + error);
})

このように同期処理のように記述できる.

async / await

Promiseよりさらに非同期処理を簡潔に書ける方法.

async / awaitで書き換えた例

async function asyncCall() {
  try {
    var change = await promiseBuyApple(500);
    console.log('おつりは' + await promiseBuyApple(500) + '円です');
    change = await promiseBuyApple(change);
    console.log('おつりは' + change + '円です');
    change = await promiseBuyApple(change);
    console.log('おつりは' + change + '円です');
    change = await promiseBuyApple(change);
    console.log('おつりは' + change + '円です'); // error: catchへ
  } catch (error) {
    console.log(error)
  }
}

asyncで定義した関数は、promise(Promiseのインスタンス)を返す. awaitをPromiseに付けると、Promiseがresolve()するのを待ってその値を取得する (ように見える) ようになる.awaitはasync関数内でないと使えない.

参考