JavaScriptには同期処理と非同期処理の2つがあり、プログラムを期待通りに動作させるにはPromiseという組み込みオブジェクトについて理解する必要があります。学習中に私がつまづいた部分についても記載します。
同期処理と非同期処理
まずはJavaScriptの同期処理と非同期処理の違いについて確認します。
同期処理
ある実行文の処理が終わるまで次の処理を行わないのが同期処理です。コードを1行ずつ順番に処理して行くため、コードの流れ通りに処理が行われます。
console.log("処理開始");
console.log("処理完了");
console.log("プログラム終了");
実行結果
処理開始
処理完了
プログラム終了
非同期処理
処理が終わる前に次の処理を許可するのが非同期処理です。例えば、Webページにアクセスした時に全てのデータが表示されていなくても画面スクロールができます。もし、データを全て受け取って表示まで完了していないと操作ができないとフリーズしたような状態になってしまいます。このように、非同期処理は後の処理の実行を遅延させないためのもので、Webブラウザ表示速度や操作性を向上させるのに必要な処理です。
非同期処理で有名なものはsetTimeout()メソッドやAjaxがあります。ここではsetTimeout()メソッドを使って非同期処理の挙動を確認します。
次のようなプログラムを考えます。
1. "処理開始"と表示
2. その3秒後に"処理完了"と表示
3. その1秒後に"プログラム終了"と表示
このフローチャートを下記のように順序通り処理されることを想定して記述してみます。
console.log("処理開始");
setTimeout(function () {
console.log("処理完了");
}, 3000);
setTimeout(function () {
console.log("プログラム終了");
}, 1000);
実行結果
処理開始
プログラム終了
処理完了
setTimeout()メソッドで表示したい"処理完了"よりも先に"プログラム終了"と表示されました。setTimeout()メソッドは非同期処理なので、setTimeout()メソッドの処理が完了する前に次の処理が実行されたためです。そのため、setTimeout()メソッドだけを使って期待通りの動作にするには次のように記述します。
console.log("処理開始");
setTimeout(function () {
console.log("処理完了");
setTimeout(function () {
console.log("プログラム終了");
}, 1000);
}, 3000);
実行結果
処理開始
処理完了
プログラム終了
setTimeout()メソッドの中にさらにsetTimeoutメソッドを記述することで意図した通りに動作します。関数の中にさらに関数を内包することをネストと言います。しかし、ネストさせるとプログラムの挙動を追うのが難しくなります。さらに、1秒ごとに"⚪︎秒経過"と出力する機能を追加してみます。
console.log("処理開始");
setTimeout(function () {
console.log("1秒経過");
setTimeout(function () {
console.log("2秒経過");
setTimeout(function () {
console.log("3秒経過");
console.log("処理完了");
setTimeout(function () {
console.log("プログラム終了");
}, 0);
}, 1000);
}, 1000);
}, 1000);
実行結果
処理開始
1秒経過
2秒経過
3秒経過
処理完了
プログラム終了
期待通りに動作していますが、ネストさせるとプログラムの可読性が下がりバグの原因にもなりえます。
これを避けるためにPromiseオブジェクトというものが存在します。
Promiseオブジェクト
Promiseオブジェクトの概要
Promiseは、ネスト化せずに非同期処理の後に実行したい処理を実行できるようにした組み込みオブジェクトです。組み込みオブジェクトなので、下記のようにコンストラクタを使って生成します。
const promise = new Promise(function(resolve, reject) {処理内容})
処理内容に非同期処理の内容を記載します。Promiseインスタンスには実行内容が正常に処理されたかどうかを表すStateと、Stateの状態を変更するresolve()関数とreject()関数があります。そのほかにもFateというものもありますが、知らなくても特に影響はないので省略します。
State
Stateは3つの状態のどれかを取ります。
- Pending(待機状態)
- Fulfilled(履行状態)
- Rejected(拒否状態)
Promiseインスタンスを生成した段階では、StateはPendingとなります。Promiseインスタンスの処理内容が適切に処理された後にresolve()関数を実行するとStateはFulfilledに変更され、エラー終了した際にreject()関数を実行するとStateはRejectedに変更されます。StateはPendingから状態が変更されると、そこから状態を変化させることはできなくなります。
Promiseインスタンスの状態がFulfilledに変更されるとthenメソッドが実行されます。Rejectedになった場合はcatchメソッドが実行されます。つまり、StateがFulfilledまたはRejectedに変更されない間は次の処理が実行されないということです。この仕組みを使うことで非同期処理をネストを使わずに順序通り処理することができます。
resolve()関数とthen()メソッド
Promiseインスタンスの処理内容でresolve()関数を実行するとStateはFulfillになり、その後、thenメソッドが実行されます。resolve()関数は、非同期処理が問題なく終了した後に実行するように記述します。
const promise = new Promise(function (resolve, reject) {
setTimeout(function() {
if (条件式) {// 処理が正常に終了した時
resolve();
}
}
});
promise.then(function () {
// Promiseインスタンスの処理が正常に終了した後に実行する処理
});
resolve()関数を実行時に()の中に値を入れると、thenメソッドで定義した関数の仮引数にその値を入れることができます。
const promise = new Promise(function (resolve, reject) {
setTimeout(function () {
if (true) {
console.log("処理完了");
resolve("成功しました");
}
}, 3000);
});
promise.then(function (message) {
console.log(message);
});
実行結果
処理完了
成功しました
resolve()関数を実行することでStateはFulfilledになり、resolve()の引数である"成功しました"が、thenメソッドで定義しているfunctionの引数messageに代入されます。その結果、thenメソッドが実行されてconsole.log(message)で
成功しました
と出力されます。
reject()関数とcatch()メソッド
Promiseインスタンスの処理内容でreject()関数を実行するとStateはRejectedになり、その後、catchメソッドが実行されます。resolve()関数とは逆に、reject()関数は処理にエラーが発生した場合に実行するように記述します。
const promise = new Promise(function (resolve, reject) {
setTimeout(function() {
if (条件式) {
resolve(); // 処理が正常に終了した時
} else {
reject(); // 処理が異常が発生した時など
}
});
promise
.then(function () {
// Promiseインスタンスの処理が正常に終了した後に実行する処理
});
.catch(function () {
// Promiseインスタンスの処理が異常終了した後に実行する処理
});
resolve()関数と同様、引数をcatchメソッドで定義した関数の引数に代入します。
const promise = new Promise(function (resolve, reject) {
setTimeout(function () {
if (false) {
console.log("処理完了");
resolve("成功しました");
} else {
reject("失敗しました");
}
}, 3000);
});
promise
.then(function (message) {
console.log(message);
})
.catch(function (message) {
console.log(message);
});
実行結果
"失敗しました"
動作はresolve()関数とthenメソッドと同様です。reject()関数の引数である"失敗しました"という文字列をcatchメソッドの関数の引数に代入しています。
上記のようにthenメソッドとcatchメソッドを記入しておくと、処理が正常に完了した場合と異常終了した場合で条件分けして実行することができます。
finallyメソッド
finallyメソッドはStateがFulfilledとRejectedのどちらでも必ず実行されるメソッドです。非同期処理後に共通の処理を実行する場合に使用します。
const promise = new Promise(function (resolve, reject) {
setTimeout(function () {
if (true) {
console.log("処理完了");
resolve("成功しました");
} else {
reject("失敗しました");
}
}, 3000);
});
promise
.then(function (message) {
console.log(message);
})
.catch(function (message) {
console.log(message);
})
.finally(function () {
console.log("プログラム終了");
});
実行結果
処理完了
成功しました
プログラム終了
つまづいたところ
はじめは「Promiseは非同期処理の後の処理を順序通りに実行させたい時に使うもの」とだけ認識していました。Promiseの中に非同期処理を入れさえすれば順序通り処理してくれると誤解していました。
例えば、"処理開始"と表示してから3秒後に処理を実行して成功か失敗かを出力し、最後に"プログラム終了"と表示させる場合、下記のような書き方をしていました。
const promise = new Promise(function (resolve, reject) {
setTimeout(function () {
if (true) {
console.log("処理完了");
resolve("成功しました");
} else {
reject("失敗しました")
}
}, 3000);
});
console.log("処理開始");
promise
.then(function (message) {
console.log(message);
})
.catch(function (message) {
console.log(message);
});
console.log("プログラム終了");
実行結果
処理開始
プログラム終了
処理完了
成功しました
Promiseインスタンスも非同期処理の一つで、thenメソッドかcatchメソッドが実行される前に次の処理が行われてしまうというところに気づいていませんでした。Ajaxを実装する際に非常につまづきました。
上の例の実行文は、次のように記述すれば正しく動作します。
console.log("処理開始");
promise
.then(function (message) {
console.log(message);
})
.catch(function (message) {
console.log(message);
})
.finally(function () {
console.log("プログラム終了");
});
処理開始
処理完了
成功しました
プログラム終了