ObjectとPromiseの理解はJavaScriptで最重要
まず最初に、お前らなどと言ってしまい、誠に申し訳ございませんでした。
備忘録がわりに、未来の複数の地点での自分に向けて書こうと思ったら、こういった不適切な表現になってしまいました。
さて、以前の記事にもPromise、Async、Awaitについての基本 を書いたけれど、お前らに、ここにもう少し踏み込んだ説明をしてやろうと思います。
JavaScriptの理解には、Objectの理解と、Promise関数の理解はめちゃ重要で、ここを理解できればJavaScriptの半分は理解出来たと思ってよろしいと思います。
最近はもっぱらReacterなのですが、使う関数の半分以上は非同期関数です。サーバー関係の関数はほぼ非同期です。
そして、自分も学習を始めた初期は、かなり理解に苦しみました。
何となくコードは書けてるけど、本当に理解していない気持ちの悪い感じ。あまり知らない他人と同棲している様な微妙な気持ちを抱えておりました。
なぜ非同期関数は重要か
なぜPromiseがそんなに重要かと言えば、ご飯を炊いてる間に餃子の準備をし、さらに餃子を蒸してる間に、ビールなんかを飲みながら、おつまみの仕込みも出来るからである。
過去のJavaScriptは、上から下にコードを順番に実行していく”同期関数”だったので、ご飯が炊けるまでは他の作業は出来ないといった有様で、非常に効率が悪かった。
特にサーバーやデータベースに問い合わせてデータをフェッチするといった作業は時間がかかり、それが完了するまではボケっと阿保みたいに台所に立っておかないといけない。
で、Async 非同期処理である。
他の作業とは関係無く、非同期で同時に作業が出来るようになったわけだ。
つまりマルチタスクである。もう飯が炊けるまで阿呆みたいに炊飯器の前で待っておく必要がなくなったのだ。
炊飯器をセットしておけば、飯が長けた時にはアラームが鳴り(fulfilled)、炊けた飯(return value) が戻ってくる。もし途中で何らかのエラーが発生した場合は、”炊飯器爆発”(rejected)といったエラーメッセージが返ってくる。
つまり、作業が完了しましたよという合図とともに、飯かエラーが返ってくるのだ。
また、非同期の問題として、いつその作業が完了するかわからないので、このデータが返ってきた後で、ここに表示するみたいな順序立てたフローをどうすれば処理できるのかという問題が発生する。
つまり、飯が炊けたらチャーハンを作る みたいな事をするために、.then とか await とかがあるのだ。これが過去多くのプログラマーを死に追いやってきたコールバックヘルの問題を解決した。
自分もJavaScriptを勉強し出した頃は、Promiseって何やねんと思って調べても、PromiseはPromise が返ってくるんや!みたいな説明がほとんどで、危うくモニターを破壊しかけたことが何度もある。
で、Promiseって、ただのObjectです。
つまり、Promiseが返ってくるっていうのは、オブジェクトが返ってくるって事。
このオブジェクトは少しだけ特殊で、pending/fulfilled/rejected の3つのステータスと、成果物のデータかエラーメッセージを保持している。
具体的にPromiseオブジェクトを生成するコードを書いてみると、
new Promise((resolve, reject) => {
const steamedRice = cookRice(); //なんか時間のかかる処理をする
if(steamedRice){
resolve(steamedRice)
}else{
reject("Rice is exploded")
}
}
何か処理をした結果
1, 値があれば、resolve()コールバックにその値を入れる。
2, もしエラーが発生すれば、そのエラーをreject()コールバックに渡す。
つまり、それだけである。で、Promiseオブジェクトが生成される。
で、Promise関数っていうのは、 単純にこのPromiseオブジェクトをリターンする関数の事である。
const cookDinner = () => {
const steamedRice = cookRice(); //なんか処理をする
const misoSoup = cookMisosoup(); //なんか処理をする
return new Promise((resolve, reject) => {
if(steamedRice && misoSoup){
resolve([steamedRice, misoSoup])
} else {
reject("飯食うな")
}
}
}
お分かりであろうか、関数内でPromiseオブジェクトを生成し、それをreturnしているだけである。
実際のコードの具体例である。
const readFileAsync = (filePath) => {
return new Promise((resolve, reject) => {
fs.readFile(filePath, 'utf8', (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
};
ファイルの読み込みが完了したら、そのファイルデータを、エラーが発生したらそのエラーを返すPromiseオブジェクトをreturnしている。
で、このreadFileAsync関数は非同期で仕事をしてくれる。
Promiseから値をゲットするには、いわゆる .then .catch を使うか、より新しい async awaitキーワード を使えばいい。
const getFile = async () => {
try {
const data = await readFileAsync('example.txt');
console.log(data);
} catch (error) {
console.error('Error reading file:', error);
}
};
asyncFunction();
とまあ、ここで疑問が浮かんでくる。
Promiseオブジェクトを生成してリターンするのは分かるんやけど、普段コードを書いてて、いちいち new Promise とか書いてへんけどな と思うはずである。
なぜなら、非同期関数を作る時は上記の例の様に、大抵 async () => {} で終わっているからである。
何故なのか?
実は、関数にasyncキーワードをつけると、自動的にPromiseが生成され、リターンするようになるからである。
const myAsyncFunction = async () => {
return "How are you?";
};
は、
const myFunction = () => {
return Promise.resolve("How are you?");
};
と同じなのだ。勝手に、resolve("Hello") としてPromiseオブジェクトとして返してくれる。
つまり、async をつけた関数は自動的にPromiseを返す非同期関数となる。
もし、値を返さない処理の場合は、自動的に fulfilled のステータスで undefined を返すようになっている。
まとめると、 非同期関数っていうのは、上から下のフローから自由になってコードを実行してくれ、Promiseというpending/fulfilled/rejectedという3つのステータスと、実行結果を値として保持したオブジェクトをリターンする関数である。
で、.then .catchメソッド か、async/await というキーワードをその返ってくるPromiseオブジェクトに対して使うと、fulfilled となったと同時にその返ってきた値をゲット出来るというわけである。
上の方で書いた、晩飯を作るフローのコードをもう少しリアルなコードとして書いておく。
// 飯を炊く関数 1000ミリセカンドの時間がかかるとする
const cookRice = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Steamed Rice');
}, 1000);
});
};
// 味噌汁を作る関数 500ミリセカンドの時間がかかるとする
const cookMisosoup = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Miso Soup is Ready');
}, 500);
});
};
// 定義した関数を使って、晩飯の支度をする
const cookDinner = () => {
return new Promise((resolve, reject) => {
Promise.all([cookRice(), cookMisosoup()])
.then((results) => {
const [steamedRice, misoSoup] = results;
if (steamedRice && misoSoup) {
resolve([steamedRice, misoSoup]);
} else {
reject("飯が不足");
}
})
.catch((error) => {
reject("飯食うな");
});
});
};
cookDinner()
.then((dinner) => {
console.log('Dinner is ready:', dinner);
})
.catch((error) => {
console.error('Error:', error);
});
これは、勿論 async/await を使って、こうも書ける
const cookRice = async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
return 'Steamed Rice';
};
const cookMisosoup = async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
return 'Miso Soup is ready';
};
const cookDinner = async () => {
try {
const steamedRice = await cookRice(); // Wait for rice to cook
const misoSoup = await cookMisosoup(); // Wait for miso soup to cook
return [steamedRice, misoSoup];
} catch (error) {
throw new Error("飯食うな");
}
};
cookDinner()
.then((dinner) => {
console.log('Dinner is ready:', dinner);
})
.catch((error) => {
console.error('Error:', error.message);
});
というわけで、備忘録。