Promiseに関して
どうもフロントエンドエンジニアのoreoです。今回は、Promise
について再整理します。
1 同期処理と非同期処理
ブラウザにおいて、JavaScriptは主にメインスレッド上で実行されます。処理はコールスタックに積まれ、後入れ先だし(LIFO)で、実行されています。同期処理では、一つの処理が完了するまでは、次の処理に移行しません。
一方、非同期処理は、処理がコールスタックから一時的に切り離され、コールバックキュー(※)に処理が格納されます。コールスタックに処理が積まれている状態では、コールバックキューに格納された処理は待ちの状態になります。コールスタックの処理が空になれば、コールバックキューに格納されている処理が、イベントループによって、先入れ先出し(FIFO)でコールスタックに積まれ処理されます。
※コールバックキューについて
コールバックキューにはマクロタスク、マイクロタスクの2種類のキューがあります。
- マクロタスク
- タスクキューとも呼ばれる。
-
setTimeout
など。 - イベントループで順番が来ると、ひとつずつタスクを実行する。
- マイクロタスク
- ジョブキューとも呼ばれる。
-
Promise
など。 - イベントループで順番が来ると、全てのジョブを実行する
コールバックキュー、コールスタック、イベントループ等の詳細は下記ご参照ください。
2 コールバック地獄について
2-1 コールバック関数を用いて非同期処理の順番を制御する
同期処理のみの場合は、ex1のようにグローバルコンテキストにそって処理が実行され、メッセージが①→②→③の順で出力されます。
【ex1】
/**
* 以下の順番で出力
* ①りんごは何個?
* ②りんごは1個
* ③わかりました!
*/
function question(){
console.log("①りんごは何個?");
}
function apple(num) {
console.log(`②りんごは${num}個`);
}
function ok(){
console.log("③わかりました!");
}
question()
apple(1);
ok()
しかし、ex2のように、非同期処理のsetTimeout
を用いると、第二引数に渡す遅延時間を0としても、メッセージの出力順が①→③→②と、ex1と比べて②と③の順番が入れ替わります。
これは、setTimeout
の第一引数の処理がマクロタスクに積まれ、コールスタックの処理(ここでのquestion()
とok()
)が終了した後に、呼ばれるためです。
【ex2】
/**
* 以下の順番で出力
* ①りんごは何個?
* ③わかりました! ※②と③の出力が入れ替わった!
* ②りんごは1個
*/
function question(){
console.log("①りんごは何個?");
}
function apple(num) {
setTimeout(function () { //非同期処理のsetTimeoutを使う
console.log(`②りんごは${num}個`);
}, 0); //setTimeoutの第二引数には遅延時間を0として渡す
}
function ok(){
console.log("③わかりました!");
}
question()
apple(1);
ok()
ここで、コールバック関数を使用すると、非同期処理setTimeout
を使用しても、ex1のような出力が可能です。具体的には、ex3のように、apple
関数の第一引数にコールバック関数を設定し、コールバック関数にok
関数を渡して実行すると、ex1と同じくメッセージが①→②→③の順番で出力されます。
このように、コールバック関数を用いると、非同期処理が絡んだ実装において、処理順を制御すること可能です。
【ex3】
/**
* 以下の順番で出力
* ①りんごは何個?
* ②りんごは1個
* ③わかりました!
*/
function question() {
console.log("りんごは何個?");
}
function apple(callback, num) { //appleの第一引数にコールバック関数を設定
setTimeout(function () {
console.log(`りんごは${num}個`);
callback(); //appleの第一引数に渡したコールバック関数をここで実行
}, 0);
}
function ok() {
console.log("わかりました!");
}
question();
apple(ok, 1); //okをappleの第一引数に渡して実行
2-2 コールバック関数を用いて、複数の非同期処理の順番を制御する
次に、コールバック関数を用いて、複数の非同期処理を順番に実行する場合を考えます。
ex4のように、apple
関数を宣言したとします。apple
関数では、第一引数にコールバック関数、第二引数にりんごの個数num
をとります。apple(function (num) {},1);
を実行すると、1秒後に「りんごは1個」と出力されます
【ex4】
/**
* 以下が出力
* りんごは1個
*/
function apple(callback, num) {
setTimeout(function () {
console.log(`りんごは${num}個`);
num += 1;
callback(num);
}, 1000);
}
apple(function (num) {}, 1);
「りんごは2個」までと出力させたい場合は、ex5のようにapple
関数の第一引数に、apple(function (num) {},num);
を与えると、1秒後に「りんごは1個」2秒後に「りんごは2個」と出力されます。この程度なら読み解くことが可能です。
【ex5】
/**
* 1秒毎に以下の順番で出力
* りんごは1個
* りんごは2個
*/
//「りんごは2個」と出力させる
apple(function (num) {
apple(function (num) {}, num);
}, 1);
しかし、「りんごは10個」まで出力させたい場合は、ex6のように冗長なコードとなり、パッと見で読み解くことが難しくなります
このようにコールバック関数を用いて非同期処理のチェーンを実装した場合、ネストが深くなり可読性が下がります。この現象がコールバック地獄と呼ばれるものです。
【ex6】
/**
* 1秒毎に以下の順番で出力。「りんごは10個」まで出力される
* りんごは1個
* ・
* ・
* りんごは10個
*/
apple(function (num) {
apple(function (num) {
apple(function (num) {
apple(function (num) {
apple(function (num) {
apple(function (num) {
apple(function (num) {
apple(function (num) {
apple(function (num) {
apple(function (num) {}, num);
}, num);
}, num);
}, num);
}, num);
}, num);
}, num);
}, num);
}, num);
}, 1);
3 Promiseオブジェクトで非同期処理を制御する
Promise
を使用して非同期処理を実装すると、コールバック関数よりも、簡単でわかりやすく記載することができます。
3-1 Promiseの基本構文
new Promise(function(resolve,reject){})
のように、二つの引数resolve
、reject
を持ったコールバック関数function(resolve,reject){}
をnew Promise()
に渡して、Promise
オブジェクトをインスタンス化します。
Promise
オブジェクトでは、主に.then
メソッド、.catch
メソッド、.finally
メソッドを使います(ex7~ex10)。
.then
メソッド
3-1-1 ex7のようにnew Promise
のコールバック関数の中で、resolve
が実行されると、.then
メソッドに渡したコールバック関数が実行されます。ここで.then
メソッドに渡したコールバック関数では、resolve
に渡した引数(ここでは「resolveです!」)を取得することができます。
【ex7】
/**
* 以下が出力
* resolveです!
* 処理は終了です!
*/
new Promise(function (resolve, reject) {
resolve("resolveです!");
})
.then(function (data) {
console.log(data);
})
.catch()
.finally(function () {
console.log("処理は終了です!");
});
.then
メソッドのチェーンを実装したい場合、一つ目の.then
メソッド内で二つ目の.then
メソッドに渡したいデータをreturn
すると、二つ目の.then
メソッドでそれを受け取ることができます(ex8)。
【ex8】
/**
* この場合以下が出力される。
* resolveです!
* resolveです!
* 処理は終了です!
*/
new Promise(function (resolve, reject) {
resolve("resolveです!");
})
.then(function (data) {
console.log(data);
return data //ここでデータをreturnする。
})
.then(function (data) {
console.log(data); //データを取得できる
})
.catch()
.finally(function () {
console.log("処理は終了です!");
});
.catch
メソッド
3-1-2 また、ex9のようにnew Promise()
のコールバック関数の中で、reject
が実行されると、.catch
メソッドに渡したコールバック関数が実行されます。.catch
メソッドに渡したコールバック関数では、reject
に渡した引数(ここでは「rejectです!」)を取得することができます。reject
は、なんらかのエラーが発生したときに、それをPromise
に通知します。
【ex9】
/**
* 以下が出力
* rejectです!
* 処理は終了です!
*/
new Promise(function (resolve, reject) {
reject("rejectです!");
})
.then()
.catch(function (data) {
console.log(data);
})
.finally(function () {
console.log("処理は終了です!");
});
.then
メソッドの中で、エラーを検知し、.catch
メソッドに処理を移行したい場合は、ex10のように.then
メソッドの中で、throw new Error
を実行します。そうすると.catch
メソッドに処理を移行できます。
【ex10】
/**
* この場合以下が出力される。
* resolveです!
* throw new Errorでエラーを検知!
* 処理は終了です!
*/
new Promise(function (resolve, reject) {
resolve("resolveです!");
})
.then(function (data) {
console.log(data);
throw new Error();
})
.catch(function (val) {
console.log("throw new Errorでエラーを検知!");
})
.finally(function () {
console.log("処理は終了です!");
});
.finally
メソッド
3-1-3 .finally
メソッドのコールバック関数に記載した処理は、.then
メソッド、.catch
メソッドのどちらが呼ばれたかに関わらず実施されるので、共通の終了処理を記載します。.finally
メソッドのコールバック関数には引数を渡すことができないので注意が必要です。
尚、new Promise()
のコールバック関数は同期的に処理を行い、.then()
メソッド、.catch()
メソッド、.finally()
メソッドは、非同期的に処理が行われます。
3-2 Promiseチェーン
それではex4~ex6でコールバック関数を用いて実装した非同期処理のチェーンをPromise
を用いて置き換えてみます。
ex11のように、apple
関数内で、Promise
のインスタンスをreturn
します。new Promise
に渡すコールバック関数内では、setTimeout
の処理を書き、setTimeout
内で、resolve
します。
【ex11】
function apple(num) {
return new Promise(function (resolve) {
setTimeout(function () {
console.log(`りんごは${num}個`);
num += 1;
resolve(num);
}, 1000);
});
}
ex11で宣言したapple
関数に対して、ex12のように.then
メソッドを繋げ、.then
メソッド内でPromise
オブジェクトをreturn
すると、非同期処理のチェーンを実装できます。
ex12の出力結果は、ex6と同じですが、Promise
を用いた方が、直感的でわかりやすくなったかと思います。
【ex12】
/**
* 1秒毎に以下の順番で出力。「りんごは10個」まで出力される
* りんごは1個
* ・
* ・
* りんごは10個
*/
apple(1)
.then(function (num) {
return apple(num);
})
.then(function (num) {
return apple(num);
})
.then(function (num) {
return apple(num);
})
.then(function (num) {
return apple(num);
})
.then(function (num) {
return apple(num);
})
.then(function (num) {
return apple(num);
})
.then(function (num) {
return apple(num);
})
.then(function (num) {
return apple(num);
})
.then(function (num) {
return apple(num);
})
また、ex13のように、無名関数を簡略化できるアロー関数を用いて、ex12を置き換えるとさらに簡略化できます。
【ex13】
/**
* アロー関数を使用するとよりわかりやすく「りんごは10個」まで出力できる
* 以下の実装は、ex12と同じ出力結果になる
*/
apple(1)
.then((num) => apple(num))
.then((num) => apple(num))
.then((num) => apple(num))
.then((num) => apple(num))
.then((num) => apple(num))
.then((num) => apple(num))
.then((num) => apple(num))
.then((num) => apple(num))
.then((num) => apple(num))
3-3 その他、Promiseで使うメソッド
.all
メソッド
3-3-1 .all
メソッドを使うと、非同期処置を並列で実装することが可能です。
ex14のように、.all
メソッドの引数にPromise
インスタンスを含んだ反復可能オブジェクトを渡します(この場合は、配列を渡しています)。配列の中にあるapple(1)
、apple(2)
、apple(3)
の処理が全て終了すると、.then
のメソッドが実行されます。因みに、all
メソッドの引数に渡したPromise
インスタンスのどれか一つがreject
された場合は、.catch
の処理が実行されます。
尚、.then
のコールバック関数の引数には、.all
メソッドの反復可能オブジェクト内に渡したPromise
インスタンスのresolve
の実引数が配列として渡されます。
【ex14】
/**
* 以下のように出力される
* りんごは1個
* りんごは2個
* りんごは3個
* 渡ってきたvalは [1, 2, 3]
* 並列処理終わり
*/
function apple(num) {
return new Promise(function (resolve) {
setTimeout(function () {
console.log(`りんごは${num}個`);
resolve(num);
}, 1000 * num);
});
}
Promise.all([apple(1), apple(2), apple(3)])
.then((val) => {
console.log("渡ってきたvalは", val); //このvalは[1, 2, 3]になる
console.log("並列処理終わり");
});
.race
メソッド
3-3-2 .race
メソッドでは、.all
メソッドと同様に、引数にPromise
インスタンスを含んだ反復可能オブジェクトを渡します。しかし、.all
メソッドとは異なり、引数に渡したPromise
インスタンスのどれか一つの処理が終了すれば、次の.then
メソッドに処理が移行します。
ex15では、.race
メソッドに、apple(1)
、apple(2)
、apple(3)
を含む配列を渡します。この場合、apple(1)
の処理が一番早く終了するので、処理が終了した段階で、次の.then
メソッドに移行します。因みに、一番早く終了した処理がreject
された場合は、.catch
の処理が実行されます。
尚、.then
のコールバック関数の引数には、.race
メソッドの反復可能オブジェクト内に渡したPromise
インスタンスで一番早く処理が終わったresolve
の実引数が渡されます。
【ex15】
/**
* 以下のように出力される
* りんごは1個
* 渡ってきたvalは1
* 並列処理終わり
* りんごは2個
* りんごは3個
*/
function apple(num) {
return new Promise(function (resolve) {
setTimeout(function () {
console.log(`りんごは${num}個`);
resolve(num);
}, 1000 * num);
});
}
Promise.race([apple(1), apple(2), apple(3)])
.then((val) => {
console.log("渡ってきたvalは", val);
console.log("並列処理終わり");
});
3-3-3 その他メソッド
この他に、.allSettled
メソッド、any
メソッドがありますが、詳細は👇をご覧ください。
4 最後に
あまり馴染みが無かったコールバックキュー、イベントループなどに関して調べることができ有意義でした。次回は、Promise
をさらに直感的に記述できるasync
、await
や例外処理について記載します。
Discussion