はじめに
皆さん、こんにちは!Webシステム開発エンジニアの蘭です!
今日はJavascriptの同期/非同期関数:【Promise、Async、Await】について語りたいと思います。
・そもそも同期と非同期処理って何?
結論から言いますと、
・同期処理:あるタスクが実行している間、他のタスクの処理は中断される方式です。
・非同期処理:あるタスクが実行をしている際に、他のタスクが別の処理を実行できる方式になります。
想像してみましょう
仮にあなたは今SNSのユーザーだとします、普段は忙しいパパでも、休みを取り、家族との記念で海外旅行に行きました。
写真撮影はいいカメラを使い、高画質な画像やビデオばかり。
そしてそれをSNSに投稿した後、ネットのせいか30分ぐらい処理待ちの状態、画面も真っ白になり、不安しか積もるばかりで、投稿処理が終わらないと何もできない...
何故ユーザーがずっと処理待つことになるのか
上記の例で見てみよう
■今までのフロントエンド処理
1. 投稿画面表示:ブラウザーからサーバー画面要求(GET)やデータ送信(POST)し、サーバーからHTMLが返却され、画面を表示。
2. SNSに投稿:ブラウザーからサーバー画面要求(GET)やデータ送信(POST)します。
3. 写真や投稿内容の保存:サーバーでデータの新規追加処理が行われます。
4. 投稿内容をSNSに表示:サーバーで処理が完了後、HTMLをブラウザーに返却し、ブラウザーにHTMLを描画して投稿内容を画面に表示。
マルチページアプリケーションの通信パターンはこのワンセットの繰り返しです。
これではサーバーの実行が完了しないと、画面の更新はされない状態になり、ユーザーは画面が更新されるまで、ずっと待つ状態となります。
■近年はSPAで非同期処理
Webサイトで近年SPA(シングルページアプリケーション)の導入が多くなりました。何故なら非同期に複数の処理ができるからです。
1. 投稿画面表示:ブラウザは最初に画面を要求し、サーバは HTML を返却し、画面表示。
2. バックで投稿処理をし、同時に画面更新:ページ遷移せずに、Ajax等でサーバーにデーター要求や送信をし、同時にJavascriptにより画面の更新部分だけを書き換えて、ユーザーは更新後の画面が見られます。
3. 後はAjaxでサーバーのやり取り結果を待つ事になりますが、画面が真っ白等、画面更新処理が中断されることは有りません。
以下の利点があります。
・更新部分だけ書き換えるので、画面更新処理向上
・複数処理を同時に行うため、処理速度が速くなる
・ユーザーの使用感も改善
もちろん、同期と非同期処理にはメリットとデメリットがあります。
こちらは開発内容に応じて調整しましょう。
同期処理と非同期処理
Javascriptの同期/非同期
以下の内容をこちらを引用。
簡単な同期処理の例を見てみましょう。
console.log(1);
console.log(2);
console.log(3);
ログを3つ表示するだけのシンプルな処理です。
3つの関数上からはlog(1)、log(2)、log(3)の順番に処理されます。
1
2
3
上から順に処理していくのは同期処理です。
非同期処理
console.log(1);
setTimeout(function(){console.log(2)}, 1000);
//console.log(2)の処理を1秒後実行する
console.log(3);
1
3
2
・先ず、こちらsetTimeout()はコールバック関数と呼びます。
Javascriptではコールバック関数は非同期処理になります。(※コールバック関数は後ほど説明します。)
・setTimeout()が呼び出された後、内部の関数(console.log(2))は別世界、別次元で動いてると思ってください。ですのでsetTimeout()と別に関数内部は非同期に処理されます。setTimeout()は呼び出すだけで、表は実行完了となります。
・setTimeout()は内部の処理は別として実行完了となり、次のconsole.log(3)の処理を開始します。
・最後にsetTimeout()内部で、一秒後にconsole.log(2)が実行された為、実行完了時間がconsole.log(3)より遅く完了しました。
・console.log(2)実行完了後に2という結果が表示されました。
⇛console.log(3)を処理していると同時にconsole.log(2)を処理する、これが非同期処理です。
コールバック関数って何?
・別の関数に呼び出してもらうための関数
・簡単に言うと関数の引数に関数が指定されてる。
function Callback(B){
...
}
//関数Bが関数Aの引数に指定されてる
function B(){
...
}
ここで、何?関数を引数として指定ってどういう意味?
実はJavascriptでは数字、ブーリン、文字、配列、関数全て値です。
以下の内容はこちらを引用。
const numValue = 100;
const strValue = "私は値です!";
const boolValue = true;
const arrayValue = [1, 2, 3];
const objValue = { key: 'value' };
const addFunc = function(a, b){
return a + b;
}
console.log(addFunc(2, 3));
//5が表示される。
//addFuncも値で、引数として指定できます。
ここで注意、関数を値として扱う時は括弧()は付けません
// 自分で定義する関数
function add(a, b) {
return a + b;
}
// 定義した関数を変数に入れる
const addFunc = add; // カッコはつけない!
// JavaScriptに標準でついてる関数でもできる
const myMax = Math.max; // max関数をmyMaxという変数に入れる
// 呼び出してみる
console.log(add(1, 2), addFunc(1, 2)); // どっちも3になる
console.log(Math.max(1, 2), myMax(1, 2)); // どっちも2
⇛関数に括弧があれば処理の呼び出しになり、括弧がなければ値として扱われます。
では、本題に戻ります。
コールバック関数とは高階関数に渡すための関数です。
// 関数を2回実行する関数!!
function doTwice(func) {
func(); // 1回目!
func(); // 2回目!
}
// あいさつを2回実行する
doTwice(sayHello);
//あいさつをする
function sayHello() {
console.log('Hello!');
}
上記を見ますとdoTwiceに関数sayHello()が引数として指定されてますね、このdoTwiceがコールバック関数です。
これで何故setTimeout()がコールバック関数なのか理解できましたね。
非同期でコールバック関数地獄
以下の内容はこちらを引用。
先ず非同期のコールバック関数を作ります
今て手元に100ポイントがあります。
りんご一つ買うと40ポイント減ります。
40ポイント以下になるとりんごが買えなくなり、エラーが表示されます。
var asyncBuyApple = function(restPoint, callback){
setTimeout(function(){
if(restPoint >= 40){
callback_pointCalculate(restPoint-40, null);
}else{
callback_pointCalculate(null, '金額が足りません。');
}
}, 1000);
}
コールバック関数を複数非同期処理
asyncBuyApple(100, function(restPoint, error){
if(restPoint !== null){
console.log('1回目の残りのポイントは' + restPoint + '円です。');
asyncBuyApple(change, function(restPoint, error){
if(restPoint !== null){
console.log('2回目の残りのポイントは' + restPoint + '円です。');
asyncBuyApple(change, function(restPoint, error){
if(restPoint !== null){
console.log('3回目の残りのポイントは' + restPoint + '円です。');
}
if(error !== null){
console.log('3回目でエラーが発生しました:' + error);
}
});
}
if(error !== null){
console.log('2回目でエラーが発生しました:' + error);
}
});
}
if(error !== null){
console.log('1回目でエラーが発生しました:' + error);
}
});
上記の例を見ますと、ネストが深く、これが仮に100回の処理がある場合、まさに大変なことになり、バグも見つかりにくなります。
コールバック関数地獄から抜け出そう:Promise
コールバック関数の問題を解決するために、Javascriptでは【Promise】という仕様が登場しました。
※PromiseはES6対応ですので、ES5の場合はbluebird等のライブラリーが必要になります。
Promiseの関数を見てみましょう!
var promiseBuyApple = function(restPoint){
return new Promise(function(resolve, reject){
if(restPoint >= 40){
resolve(restPoint-40);
//実行成功、resolveをPromiseコンストラクタに渡す
//return restPoint-40 と同じ意味です。
}else{
reject('ポイントが足りません。');
//実行失敗の場合、rejectをPromiseコンストラクタに渡す
//return 'ポイントが足りません。' と同じ意味です。
}
});
}
・Promise関数はPromiseオブジェクトを返します。
・成功の場合は「resolve」、失敗の場合は「reject」関数をPromiseコンストラクタに渡します。
Promise関数複数処理
Promiseではresolveやrejectをthenメソッドに渡します。
promiseBuyApple(100).then(function(restPoint){
console.log('残りのポイントは' + restPoint + 'です');
return promiseBuyApple(restPoint);
}).then(function(restPoint){
console.log('残りのポイントは' + restPoint + 'です');
return promiseBuyApple(restPoint);
}).then(function(restPoint){
console.log('残りのポイントは' + restPoint + 'です');
}).catch(function(error){
console.log('エラーが発生しました:' + error);
});
・Promiseを使うことで、コードが綺麗に整いました。
・最初の関数が実行した後、thenメソッドのコールバック中でreturnされた結果が次のthenメソッドのコールバックに引数として渡されます。
・then(function(restPoint)
のrestPoint
に成功すればresolveが渡されて、失敗の場合はrejectが渡されます。
・エラーの場合はcatchメソッドが実行されます。
簡単に言うといかの感じになります。
以下の内容はこちらを引用。
A(function(){
B(function(){
C(function(){
console.log('Done!');
});
});
});
A().then(B).then(C).then(function(){
console.log('Done!');
});
更に、async、awaitはPromiseを簡単に扱える仕組み
・内容は以下を引用しました。
Promiseが分かれば簡単!async, await
【JavaScript入門】5分で理解!async / awaitの使い方と非同期処理の書き方
Promiseだけでも同じ処理ができますが、async、awaitを使うとよりスッキリしたコードになります。
基本的な構文について
まずは、基本の構文を見てみましょう!
「async」は「function」の前に記述するだけで非同期処理を実行できる関数を定義できます。
async function() { }
このようにasyncを記述しておくと、この関数はPromiseを返すようになります。また、「await」はPromise処理の結果が返ってくるまで一時停止してくれる演算子となります。
await Promise処理
・ここで注意ですが、「await」は「async」で定義された関数の中でしか使えません。今では「await」と「async」は一緒に使われてることが多いですね!
実際の例を見てみましょう。
以下がPromiseの処理(非同期)です。
function myPromise(num) {
return new Promise(function(resolve) {
setTimeout(function() { resolve(num * num) }, 3000)
})
}
非同期処理で3秒間かかる処理を記述し、引数numで受け取った値を2乗した結果を返す単純な処理です。
以下は「then」を使わずに、「await/async」を利用します。
async function myAsync() {
const result = await myPromise(10);
console.log(result);
}
myAsync();
この例では、asyncを付与することで非同期処理の関数を作成していますね。その関数内でPromise処理を記述している「myPromise()」の前に「await」を付与しているのが分かります。
これにより、3秒後に結果が返ってくるPromise処理を一時的に待つことになり、結果を取得した瞬間に関数内の処理が続行されるのです。実行結果には引数に与えた「10」が2乗された値「100」が取得できていますね。
「then」を使わずに非同期処理を複数実行する方法
myPromise(10).then(function(data) {
console.log(data);
return myPromise(100)
}).then(function(data) {
console.log(data);
return myPromise(1000)
}).then(function(data) {
console.log(data);
})
同じ処理をasync、awaitで書くと
async function myAsyncAll() {
console.log(await myPromise(10));
console.log(await myPromise(100));
console.log(await myPromise(1000));
}
myAsyncAll();
「Promise.all」と「async/await」の並列処理
「async/await」を使い、Promise処理が終わり、resolve(reject)が返された後にまた次のPromise処理を実行するのもいいですが、例えばPromise処理がAPIでデータを取得する処理としよう、仮に取得するAPIが合計個あるとします、API処理で1分かかり、全てのAPIを取得するだけで20分かかります。
これは非同期で一括取得したほうが速いですよね!
ここで「Promise.all」と「async/await」の並列処理を紹介します。
まずは「Promise.all」を使った一括処理
Promise.all([
myPromise(10),
myPromise(100),
myPromise(1000)
]).then(function(data) {
console.log(data);
})
上記を見てみますと、一括処理はできたが、個別の処理結果を見たい時は少し不便ですよね。
ここで「async/await」を使った一括処理
nc function myAsyncAll() {
const r1 = myPromise(10);
const r2 = myPromise(100);
const r3 = myPromise(1000);
console.log(await r1, await r2, await r3);
}
myAsyncAll();
上記を見ますと、Promise処理を全て起動させて変数に格納し、結果を取得できます。
おまけに、以下はSPAで一括API取得処理の例
async function xxx() {
let res1;
let res2;
let res3;
try {
[res1, res2, res3] = await Promise.all([
axios.get('http://.../get1').catch(e => { throw 'get1 error '+e.message}),
axios.get('http://.../get2').catch(e => { throw 'get2 error '+e.message}),
axios.get('http://.../get3').catch(e => { throw 'get3 error '+e.message}),
]);
} catch(err) {
console.log(err);
return; // 1つでもエラーになったら、関数を抜ける
}
// 3つ全てが正常データを取得できたとき、以下を実行
console.log(res1);
console.log(res2);
console.log(res3);
}
まとめ
いかがでしょうか。
今回はJavascriptの同期/非同期、コールバック関数、Promise関数、Async/Await、非同期一括処理についての簡単な紹介をしました。
また今後も現場で活用していただければ嬉しいです!