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
つまり、JavaScriptはXMLHttpRequestを使って外部と通信している間に、次の自分の仕事である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関数内でないと使えない.