Promiseとは
Promise は、JavaScript や Node.js において、非同期処理のコールバック関数をエレガントに記述するための仕組みです。
英語の promise は、「制約」、「保障」などの意味を持ちます。IE11 ではサポートされていませんので es6-promise といったPolyfillが必要です。
コールバック地獄とは?
JavaScript では、ブロックする(処理が終わるまで待ち合わせる)関数よりも、非同期関数※の方が多様されます。ここで、例えば、膨大な演算(実は単に元の数を2倍するだけ)を行う非同期関数 aFunc1() があるとします。下記は、100の2倍を求める非同期関数の使用例です。
※ 処理の完了を待たず、処理が完了した時点でコールバック関数が呼び出される。
JavaScript
// 引数を2倍にする非同期関数 function aFunc1(data, callback) { setTimeout(function() { callback(data * 2); }, Math.random() * 1000); }
JavaScript
function sample_callback() { // 非同期関数を用いて100の2倍を求める aFunc1(100, function(value) { console.log(value); // => 200 }); }
単純に非同期関数を1回だけ呼び出すのであれば、上記で問題ありませんが、1回目で得られた値を用いて、aFunc1() を2度、3度呼び出そうとすると、下記の様な実装になります。
JavaScript
function sample_callback_hell() { aFunc1(100, function(data) { console.log(data); // => 200 aFunc1(data, function(data) { console.log(data); // => 400 aFunc1(data, function(data) { console.log(data); // => 800 }); }); }); }
呼び出す回数に比例してコールバックのネストが深くなります。これを、「コールバック地獄」と呼びます。
タイミング問題
非同期関数はまた、処理の順序を制御できないという問題も含みます。下記の例では、100の2倍、200の2倍、400の2倍を求めようとしたにも関わらず、処理結果は 200, 400, 800 だったり、800, 200, 400 など、結果処理が順不同となるという問題があります。
JavaScript
function sample_timing_problem() { aFunc1(100, function(data) { console.log(data); // => 200 }); aFunc1(200, function(data) { console.log(data); // => 400 }); aFunc1(400, function(data) { console.log(data); // => 800 }); }
これは、jQueryにおける addClass や attr といったよく利用するメソッドも同じです。
$("#hoge").addClass("is-open") で is-open というclassを追加した直後に、$(".is-open") でDOMを取得しようとしても、タイミングによっては取得できない場合があります。
$(function(){ $("#hoge").addClass("is-open"); console.log($(".is-open")) // 取得できない })
これは、jQuery内部で非同期関数として実装されているからです。
Promiseによる解決
これらの問題を解決するために考案されたのが Promise です。Promise は、約束、誓約、保証などの意味を持ちます。Promise は、待機(pending)、成功(fulfilled)、失敗(rejected)の3値を持つオブジェクトです。前述の非同期関数 aFunc1() を Promise を用いて書き直すと下記の様になります。処理を行う関数を引数とした Promise オブジェクトを返却します。
JavaScript
function aFunc2(data) { return new Promise(function(resolve) { setTimeout(function() { resolve(data * 2); }, Math.random() * 1000); }); }
Promise オブジェクトは then(resolve, reject) というメソッドを持ちます。then() は、Promise が成功または失敗になるまで処理を受け流し、成功時に resolve を、失敗時に reject をコールバック関数として呼び出します。
JavaScript
function sample_promise() { aFunc2(100).then(function(data) { console.log(data); // => 200 }); }
アロー関数を用いると、次のようにも記述できます。
JavaScript
function sample_promise2() { aFunc2(100).then((data) => { console.log(data); // => 200 }); }
さらに処理を継続するには、下記の様にします。
JavaScript
function sample_promise3() { aFunc2(100).then((data) => { console.log(data); // => 200 return aFunc2(data); }) .then((data) => { console.log(data); // => 400 return aFunc2(data); }) .then((data) => { console.log(data); // => 800 }); }
エラー処理
下記は、約 30% の確率でエラーとなる Promise 非同期関数です。
JavaScript
function aFunc3(data) { return new Promise(function(resolve, reject) { setTimeout(function() { if (Math.random() < 0.30) { reject(new Error('ERROR!')); } else { resolve(data * 2); } }, Math.random() * 1000); }); }
.then() は第一引数に成功時のコールバック関数、第二引数に失敗時のコールバック関数を指定します。エラーを考慮した呼び出し元は下記の様になります。
JavaScript
function sample_reject() { aFunc3(100).then( (data) => { console.log(data); }, // 成功時の処理 (e) => { console.log(e); } // 失敗時の処理 ); }
上記は、下記の様に記述することもできます。.catch(reject) は、.then(undefined, reject) と同じ意味を持ちます。Promise は一度エラーが発生すると、最初に reject 関数が指定されるまで、then 処理をスキップします。
JavaScript
function sample_catch() { aFunc3(100).then((data) => { console.log(data); return aFunc3(data); }) .then((data) => { console.log(data); return aFunc3(data); }) .then((data) => { console.log(data); }) .catch((e) => { console.log(e); }); }
throwを伴うエラー処理
.catch() はまた、処理中に発生した throw をキャッチすることもできます。下記の例では、aFunc3() 内部で発生したエラーや、2番目の処理で発生した例外を .catch() が受け止めます。
JavaScript
function sample_catch_with_throw() { aFunc3(100).then((data) => { console.log(data); return aFunc3(data); }) .then((data) => { console.log(data); throw new Error('ERROR!!!'); }) .then((data) => { console.log(data); }) .catch((e) => { console.log(e); }); }
Finally
.catch() の後ろに .then() を加えることで、成功時にも、失敗時にも常に実行される Finally のような処理を追加することができます。
JavaScript
function sample_finally() { aFunc3(100).then((data) => { console.log(data); return aFunc3(data); }) .then((data) => { console.log(data); return aFunc3(data); }) .then((data) => { console.log(data); }) .catch((e) => { console.log(e); }) .then(() => { console.log('*** Finally ***'); }); }
ES2018(ES9) では、.finally() がサポートされました。
function sample_finally2() { aFunc3(100).then((data) => { console.log(data); return aFunc3(data); }) .then((data) => { console.log(data); return aFunc3(data); }) .then((data) => { console.log(data); }) .catch((e) => { console.log("catch"); console.log(e); }) .finally(() => { console.log('*** Finally ***'); }); }
すべてのタスクが完了したら(Promise.all())
Promise.all() は配列で指定されたすべての Promise タスクを待ち合わせ、すべてのタスクが完了した時点で .then() のコールバック関数を呼び出します。
JavaScript
function taskA() { return new Promise((callback) => { console.log("taskA start."); setTimeout(function() { console.log("taskA end."); callback(); }, Math.random() * 3000); }); } function taskB() { return new Promise((callback) => { console.log("taskB start."); setTimeout(function() { console.log("taskB end."); callback(); }, Math.random() * 3000); }); } function sample_all() { p1 = taskA(); p2 = taskB(); Promise.all([p1, p2]).then(() => { console.log("taskA and taskB are finished."); }); }
いずれかのタスクが完了したら(Promise.race())
Promise.race() は配列で指定された Promise タスクを待ち合わせ、いずれかひとつのタスクが完了した時点で、.then() のコールバック関数を呼び出します。
JavaScript
function sample_race() { p1 = taskA(); p2 = taskB(); Promise.race([p1, p2]).then(() => { console.log("taskA or task B is finished."); }); }
非同期関数を同期関数っぽく呼び出す(async/await)
ES2017 では、Promise に加え、async/await がサポートされました。こちらも、Internet Explorer を除く大半のモダンブラウザで利用可能です。async と await を用いることで、Promise に対応した非同期関数を、同期関数の様に呼び出すことが可能となります。同期関数の様に呼び出したい非同期関数を呼び出す際に await をつけます。await を呼び出す関数に async をつけます。
JavaScript
async function sample_async_await() { var val = 100; val = await aFunc2(val); console.log(val); // 200 val = await aFunc2(val); console.log(val); // 400 val = await aFunc2(val); console.log(val); // 800 }
エラー処理に対応するコードは下記の様になります。
JavaScript
async function sample_async_await_with_catch() { var val = 100; try { val = await aFunc3(val); console.log(val); val = await aFunc3(val); console.log(val); val = await aFunc3(val); console.log(val); } catch (e) { console.log(e); } }
Udemyを実際に体験した方の感想記事もぜひご覧ください♪