Promise 内で resolve() を呼ぶタイミングによって、then() の中の関数が実行されるタイミングも変わるんじゃね?っていう勘違いをずっとしてました
Promise を返す関数があったとしても、非同期的な処理を一切挟まずに、すぐに resolve したら、then() の中がすぐ実行される、と思って試してみたら勘違いだった事が分かったので、備忘録として実行タイミングをテストするのに使ったコードを載せておきたいのです。
簡単にテストするために、nodejs でテキストファイルを非同期で読み込ませてその内容を変数に保持しておいて、2回目は保持しておいた変数の内容を返すようなのを書いてみました。
const fs = require("fs");
const {performance} = require("perf_hooks");
let textFileCache = "";
/** @return {Promise<string>|string} */
const getTextFileContent = ()=>
{
return new Promise(resolve =>
{
if(textFileCache)
{
console.log("テキストファイルの内容 :", textFileCache);
resolve(textFileCache);
}
else
{
fs.readFile("test.txt", {encoding: "utf8"}, (error, text)=>
{
textFileCache = text;
console.log("テキストファイルの内容 :", textFileCache);
resolve(text);
});
}
});
}
const test = ()=>
{
console.log("//")
const start = performance.now();
const doSomething = ()=>
{
console.log("なんか処理するよ", performance.now() - start, "ms");
}
getTextFileContent().then(text=>
{
doSomething();
});
console.log("a", performance.now() - start, "ms");
}
test();
setTimeout(test, 1000);
結果
//
a 1.7953009977936745 ms
テキストファイルの内容 : text file
なんか処理するよ 6.83050100132823 ms
//
テキストファイルの内容 : text file
a 0.6821989975869656 ms
なんか処理するよ 1.5800999999046326 ms
1回目の処理は fs.readFile() が終わった後に resolve しているので、「a」っていうメッセージが一番最初に表示されてから、「テキストファイルの内容」とか「なんか処理するよ」っていうメッセージが出るんだろうな、っていうのは知ってました。
2回目のテキストファイルの内容をキャッシュしてる時は、fs.readFile とかの非同期処理を挟まずに、すぐに resolve してるから then() の中が即座に実行されて、「テキストファイルの内容」→「なんか処理するよ」→「a」っていう順番で出るんだと思ってましたけど、「a」っていうメッセージの方が先に来ているので、ここでthen() って後で実行される物なんだという事に気づき始めました。
マイクロタスクとかコールスタックとかタスクキューとかっていうのを、もっとしっかり理解出来るようになれば、こういう処理の流れにもビックリしなくなるんだろうなぁ、とは思うのですが、アレすっごい難しいです。
1回目の処理でテキストファイルの内容を変数に保持してたら、Promise を返すんじゃなくて、直接テキストファイルの内容を返しちゃうような形って実現できるんだろうか、と思って書いてみたのですが、関数の返り値が Promise なのか、テキストファイルの内容(string)なのかをイチイチ判定する書き方はちょっとめんどくさかったです。
const fs = require("fs");
const {performance} = require("perf_hooks");
let textFileCache = "";
/** @return {Promise<string>|string} */
const getTextFileContent = ()=>
{
if(textFileCache)
{
console.log("テキストファイルの内容 :", textFileCache);
return textFileCache;
}
else
{
return new Promise(resolve =>
{
fs.readFile("test.txt", {encoding: "utf8"}, (error, text)=>
{
textFileCache = text;
console.log("テキストファイルの内容 :", textFileCache);
resolve(text);
});
});
}
}
const test = ()=>
{
console.log("//")
const start = performance.now();
const doSomething = (start)=>
{
console.log("なんか処理するよ", performance.now() - start, "ms");
}
let text = getTextFileContent();
if(text instanceof Promise)
{
text.then(result=>
{
text = result;
doSomething();
})
}
else
{
doSomething();
}
console.log("a", performance.now() - start, "ms");
}
test();
setTimeout(test, 1000);
結果
//
a 2.584501001983881 ms
テキストファイルの内容 : text file
なんか処理するよ 8.310101002454758 ms
//
テキストファイルの内容 : text file
なんか処理するよ 0.5498999990522861 ms
a 1.1728999987244606 ms
処理の順番的には2回目は await で書いたみたいな実行順にはなったけど、正直パフォーマンス的にも then() を実行しない分ちょっとくらい影響出るのかなと思ったら、誤差の範囲で全然変わらないんですね。これなら Promise だけ返してた方が楽ですね。
最後に、ずっと resolve() を呼んだ時点で then の中が実行されるわけではない事にハッキリと気づいた時の勉強用のコードです。Promise 難しいです。はふん。
const now = performance.now();
const test = ()=>
{
return new Promise(resolve =>
{
console.log("a", performance.now() - now);
resolve();
console.log("b", performance.now() - now);
});
}
test().then(()=>
{
console.log("c", performance.now() - now);
});
console.log("d", performance.now() - now);
何回か実行してみた結果:
a 0
b 0.10000000149011612
d 0.5
c 0.8000000007450581
a 0
b 0.10000000149011612
d 0.10000000149011612
c 0.19999999925494194
a 0.09999999776482582
b 0.09999999776482582
d 0.30000000074505806
c 0.30000000074505806
今回の勉強を始める前までは fetch とか fs.readFile みたいな非同期処理を挟まなければ a → c → b → d の順番で実行される物なんだと思ってました。
Promise 内のどこで resolve を呼ぼうが、then() の中が実行されるタイミングが変わらない事に気づくまでに Promise を勉強し始めてから凄い長い事時間がかかりました。
でもおかげで合わせて勘違いしていた、async/await は return new Promise()より細かくタイミングを調整出来ない代物、っていう考えも改められそうです。 resolve() を書く場所で、いつメインの処理に渡せるか調整出来る!とか思ってた頃の自分が恥ずかしいです。
resolve() で結果をメインの処理に渡した後も、オマケで処理を書けるもんだと思って色々コード書いてましたが、resolve() 書いた後の処理もちゃんと終わってから、メインの処理に移ってたんですね。
const now = performance.now();
const test = ()=>
{
return new Promise(resolve =>
{
console.log("a", performance.now() - now);
resolve();
- console.log("b", performance.now() - now);
+ queueMicrotask(()=>
+ {
+ console.log("b", performance.now() - now);
+ });
});
}
test().then(()=>
{
console.log("c", performance.now() - now);
});
console.log("d", performance.now() - now);
a 0.10000000149011612
d 0.19999999925494194
c 0.3999999985098839
b 1.1000000014901161
resolve() でメインの処理に結果を投げて、メインの処理がある程度落ち着いた頃に、続けてローカル変数とかを利用してなんか実行したいコードとかを書く時は、queueMicrotask() みたいなのを使わないといけないみたいですね。
ただ、こんな書き方を覚えたからと言って多用しだしたら、マルチスレッドプログラミングと同じくらい難しくなりそうなので、処理の実行順が右往左往するようなコードだとIQ3の私には追えなくなってしまいそう。
実行速度上げるための書き方を学び続けるのってマジで大変です。はひん。
せっかく備忘録として書いたんだから、未来の自分に忘れないでいてほしいです。もう頭の中がパンパンマンです。