Javascriptの非同期処理を本質から理解する エラーファーストコールバック・Promise
非同期処理
参考にさせていただいたサイト↓
非同期処理とは
非同期処理はコードがz軸に沿って浮いた状態のイメージとなる。例えば
console.log("1. setTimeoutに登録した関数を1秒後に実行します"); setTimeout(() => { console.log("3. 非同期処理が実行されています"); }, 1000); console.log("2. 非同期処理より先に実行されます");
この場合、setTimeout
に登録した関数は非同期処理となり、プログラムは上から順に実行されるという僕の思い込みは打ち砕かれます。僕のイメージでは、setTimeoutに登録(引数として渡した)アロー関数は、1000msの間浮いた状態となります。
非同期処理と例外処理
非同期処理をtry...catch
で例外キャッチしようとしてもできません。
try { setTimeout(() => { throw new Error("非同期的なエラー"); }, 10); } catch (error) { // 非同期エラーはキャッチできないため、この行は実行されません }
try...catch
の中に非同期処理を記述してはいますが、try...catch
が実行される時、非同期処理は浮いているため、例外をキャッチできません。次のようにすれば例外をキャッチできます
setTimeout(() => { try { throw new Error("エラー"); } catch (error) { console.log("エラーをキャッチできる"); } }
ですが、これでは非同期処理の外側に例外が発生したことを伝えることができません。なぜ外側に例外処理を伝える必要があるのかを考えていきます
例えば、サーバからデータを取ってくるという非同期な処理と、データを加工するという処理をするとします。もしこれを非同期処理の中で全部実行するなら例外を外側へ伝える必要はありあませんが、データを加工する処理を分離すると、そのデータを加工する処理に、「データ取ってくるの失敗しちゃった😭」と伝える必要があるのです。
そしたらデータを加工する処理は条件分岐で、成功したらconst title = response.title
、失敗したらリダイレクト
とすることがきます。
エラーファーストコールバック
具体例をみてみます
function dummyFetch(path, callback) { setTimeout(() => { // /success からはじまるパスにはリソースがあるという設定 if (path.startsWith("/success")) { callback(null, { body: `Response body of ${path}` }); } else { callback(new Error("NOT FOUND")); } }, 1000 * Math.random()); } // /success/data にリソースが存在するので、`response`にはデータが入る dummyFetch("/success/data", (error, response) => { if (error) { // この行は実行されません } else { console.log(response); // => { body: "Response body of /success/data" } } }); // /failure/data にリソースは存在しないので、`error`にはエラーオブジェクトが入る dummyFetch("/failure/data", (error, response) => { if (error) { console.log(error.message); // => "NOT FOUND" } else { // この行は実行されません } });
分離しなきゃいいじゃん!って思うかもしれませんが、データ加工処理はページ毎などに変わる可能性が高いので、その度にdummyFetch関数
を記述しなければいけなくなり、やばいほどに冗長です。
ちなみに、上のコードように、データ加工処理をするコールバック関数の第一引数にエラーオブジェクトを入れて、その中で条件分岐する方法をエラーファーストコールバックと呼ぶそうです
今このような記述をすることはほとんどないと思いますが、過去のコードでガチャガチャするときに必要になると思います。
Promise
エラーファーストコールバックとは異なり、非同期処理(asyncPromiseTask関数)はPromiseインスタンスを返しています。 その返されたPromiseインスタンスに対して、成功と失敗時の処理をそれぞれコールバック関数として渡すという形になります。
// asyncPromiseTask関数はPromiseインスタンスを返す asyncPromiseTask().then(()=> { // 非同期処理が成功したときの処理 }).catch(() => { // 非同期処理が失敗したときの処理 });
Promiseインスタンスの生成
イメージとしては、非同期処理を行なう関数をpromiseインスタンス
に格納する。
const executor = (resolve, reject) => { // 非同期の処理が成功したときはresolveを呼ぶ // 非同期の処理が失敗したときはrejectを呼ぶ }; const promise = new Promise(executor);
条件分岐
thenメソッド
で、成功時と失敗時の関数を登録します。
// `Promise`インスタンスを作成 const promise = new Promise((resolve, reject) => { if(true) { resolve(); } else { reject(); } }); const onFulfilled = () => { console.log("resolveされたときに呼ばれる"); }; const onRejected = () => { console.log("rejectされたときに呼ばれる"); }; // `then`メソッドで成功時と失敗時に呼ばれるコールバック関数を登録 promise.then(onFulfilled, onRejected);
ただ、これだとpromiseインスタンスが一つしかないため、promiseインスタンス工場を作成して、何度呼び出しても、新鮮なPromiseインスタンスを使用することができます。そしてこの例は上の例よりもよく使う形に近い表現で書かれています
function dummyFetch(path) { return new Promise((resolve, reject) => { setTimeout(() => { if (path.startsWith("/success")) { resolve({ body: `Response body of ${path}` }); } else { reject(new Error("NOT FOUND")); } }, 1000 * Math.random()); }); } // `then`メソッドで成功時と失敗時に呼ばれるコールバック関数を登録 // /success/data のリソースは存在するので成功しonFulfilledが呼ばれる dummyFetch("/success/data").then(function onFulfilled(response) { console.log(response); // => { body: "Response body of /success/data" } }, function onRejected(error) { // この行は実行されません });
補足ですが、成功時だけ登録したい場合、第二引数を省略すれば良いですが、失敗時だけ登録したい場合は、第一引数にundefined
を書く必要があり、冗長で不恰好なため次のようにcatch
で登録することができます
dummyFetch("/failure/data").catch(error => { console.log(error.message); });
ここまで理解できれば多分あとはスラスラいけるのでここからはjs-primerさんに任せます。
続き↓
このあと読むと良い記事(ここでもjs-primer)