はじめに
JavaScriptの非同期処理とコールバック、Promiseについてあまり理解できていなかったので、自分なりに調べてまとめてみました。
間違いがあればコメント頂けると幸いです。
目次
- そもそも非同期処理とは
- 非同期処理のサンプル
- コールバック(Callback)とは
- コールバック(Callback)の問題点
- Promiseとは
- Promiseの書き方
- 参考
そもそも非同期処理とは
まず、Promiseを知る上で、必要な概念として、非同期処理があります。
非同期処理とは、「ある処理が実行されてから終わるまで待たずに、次に控えている別の処理を行うこと」です。
ちなみに同期処理とは、「ある処理が実行されてから終わるまで、次に控えている処理は待つこと」です。
具体的に非同期処理が必要とされるものとしては、以下の3種類があります。
- ネットワーク経由のリクエスト(例えばAjax呼び出し)
- ファイルシステム関連の操作(ファイルの読み書きなの)
- 意図的に遅延された操作(例えばアラーム)
上記を例えば同期的に処理すると、
- 読み込みに10分かかるファイルの読み込みを行う。
- 読み込みを行っている間、JavaScriptアプリケーションは他の処理を行えなくなります。
もし非同期的に処理すると、
- 読み込みに10分かかるファイルの読み込みを行う。
- 読み込みを行っている間、JavaScriptアプリケーションは他の処理を行うことができます。
JavaScriptのアプリケーションは「シングルスレッド」で実行されるので、1度にひとつのことを実行していきます。言い換えると、複数の処理を並列で実行することができません。ただ、最近のコンピューターはマルチコアが一般的であり、マルチタスキングが可能であるため、JavaScriptのアプリケーションでも非同期の処理を実現するために、非同期プログラミングの機能が追加されました。
非同期処理のサンプル
単純な例を見ていきます。組み込み関数setTimeoutは指定の時間だけ実行を遅延するものです。実際に実行してみるとコードの記述順とコードの実行順が異なることがわかります。
console.log('setTimoutの前:' + new Date());
function f() {
console.log('これは関数fの中:' + new Date());
}
setTimeout(f, 10000);/* 10秒後に関数fを実行。コールバック*/
console.log('setTimeoutの後')
console.log('これもsetTimeoutの後')
/* 実行結果
setTimoutの前:Sat Oct 10 2020 18:40:51 GMT+0900 (GMT+09:00)
setTimeoutの後
これもsetTimeoutの後
これは関数fの中:Sat Oct 10 2020 18:41:01 GMT+0900 (GMT+09:00
*/
コールバック(Callback)とは
先ほどのsetTimeout(f,10000);
は、10秒後に関数fを呼び出す、というものでしたが、この10秒後に呼び出される関数fをコールバック関数といいます。つまり、***「将来のある時点で呼び出される関数」***をコールバック関数といいます。
コールバックを利用することで、次のことができるようになります。
- 非同期処理の中で、決まった順序で処理を実行したい
- 関数を渡す形にすることで、非同期処理を実行した後の処理の内容を自由に決めたい
コールバック(Callback)の問題点
コールバックを利用することで、非同期の実行の管理ができますが、複数のコールバックを待つ必要があると管理が難しくなる問題点があります。これをコールバック地獄といいます。
/* Nodeで実行 */
const fs = require('fs');
fs.readFile('a.txt', function (err, dataA) {
if (err) console.error(err);
fs.readFile('b.txt', function (err, dataB) {
if (err) console.error(err);
fs.readFile('c.txt', function (err, dataC) {
if (err) console.error(err);
fs.writeFile('d.txt', dataA + dataB + dataC, function (err) {
if (err) console.error(err);
});
});
});
});
これは3個のテキストファイルを読み込み、それらを1つのファイルに書き出すという処理ですが、深い入れ子のブロックができあがってしまいます。これでは保守性が悪いため、より安全で保守しやすいコードを記述するために、Promiseが考案されました。
Promiseとは
Promiseとは、非同期処理に対するオブジェクトとルールを仕様化することで、複雑な非同期処理をパターンに沿って記述、実行できるようになった仕組みのことをいいます。
先ほどの3個のテキストファイルを読み込み、それらを1つのファイルに書き出すという処理
をPromiseに置き換えてみます。
function readFile(file){
return new Promise((resolve,reject)=>{
fs.readFile(file,(err,data)=>{
if(err) {reject(err); return;}
resolve(data)
})
})
}
function writeFile(file, contents){
return new Promise( (resolve,reject) =>{
fs.writeFile(file, contents, (err) => {
if(err) {reject(err); return;}
resolve("書き込み成功");
})
})
}
let totalData;
readFile('a.txt')
.then((data)=>{
totalData = data;
return readFile('b.txt')
})
.then((data)=>{
totalData += data;
return readFile('c.txt')
})
.then((data)=>{
totalData += data;
return writeFile('d.txt',totalData)
})
先ほどよりコードが長くなってしまいましたが、Promiseにした方が見やすくはなったのではないでしょうか。
Promiseの書き方
1. Promiseオブジェクトの作成
Promiseを利用するには、Promiseのオブジェクトを生成していきます。オブジェクトの生成は以下の通りです。
- new Promise(fn) の返り値がpromiseオブジェクト
- fn には非同期等の何らかの処理を書く
- 処理結果が正常なら、resolve(結果の値) を呼ぶ
- 処理結果がエラーなら、reject(Errorオブジェクト) を呼ぶ
つまり、下記のようなコードになります。
new Promise((resolve, reject) => {
/* 非同期の処理を記述 */
setTimeout(() => {
resolve('Hello World');
}, 10000);
})
/* 上記をconsole.log()で確認してみた結果は、以下の通りで、promiseオブジェクトが返ってきている。
Promise { <pending> }
*/
2. Promiseオブジェクトへの処理を記述
先ほど作成したPromiseオブジェクトを返す関数を実装してみます。
function asyncFunction() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Hello World');
}, 10000);
})
}
asyncFunction()
/* asyncFunction()をconsole.log()で確認してみた結果は、以下の通り。
Promise { <pending> }
*/
ただし、このままですと、戻されるPromiseを何も利用できていないので、Promiseに用意されているthenメソッドを利用していきます。
function asyncFunction() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Hello World');
}, 10000);
})
}
asyncFunction().then(
/* 非同期処理成功時に行う処理を記述 */
function onFulfilled(value){
console.log(value)
}).catch(
/* 非同期処理失敗時に行う処理を記述 */
function onRejected(err){
console.log(err)
})
/* 実行結果
10秒後にHello Worldが出力される。
*/
これをアロー関数を使ってみると、以下のかたちで書けます。
function asyncFunction() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Hello World');
}, 10000);
})
}
asyncFunction().then((value) => {
console.log(value)
}).catch((err) => {
console.log(err)
})
/* 実行結果
10秒後にHello Worldが出力される。
*/
参考
- 初めてのJavaScript ES2015以降の最新ウェブ開発
- JavaScript Promiseの本