イベントループ — 1本のスレッドで待たずに捌く
JavaScript が1スレッドなのに、なぜ複数の処理を同時に進められるのか。そして、なぜ「重い処理」が画面ごと止めるのか。
この記事の進み方
What — 1本のスレッドが何を順番に処理しているか
JavaScript の実行環境は、原則としてスレッドが1本です。その1本が、こういう順番で仕事を回しています。
マイクロタスクは「今のタスクの続き」として全部処理される。マクロタスクは1つずつ。この非対称さが、Promise と setTimeout の順序の違いを生む。
マイクロタスクとマクロタスクの違い
今の同期処理が終わった直後、溜まっているものを全部実行する。描画より先。
Promise.then / catch / finally
queueMicrotask
async 関数の await 以降
MutationObserver- 取り出し方
- 空になるまで全部
- 描画との順序
- 描画より前
- 無限に積むと
- 画面が完全に固まる
- –実行中に新しいマイクロタスクを追加すると、同じ回で処理される。だから再帰的に追加すると抜けられない。
- –「次のタスクより前に、確実に実行される」ことが保証される。状態の整合性を保ちたい処理に向く。
優先度の違いではなく「取り出し方」の違い。マイクロタスクは全部、マクロタスクは1つずつ。この差が実行順序を決める。
順序を実際に追うと、こうなります。
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// 出力: 1 → 4 → 3 → 2
同期コード(1, 4)が最後まで走り、スタックが空になってからマイクロタスク(3)、そしてマクロタスク(2)。setTimeout(…, 0) が最後に来るのは、優先度が低いからではなく、取り出される順番がそこだからです。
10万件の配列を回す同期処理に async を付けると、画面の固まりは解消しますか?
Why — なぜ1スレッドという設計を選んだのか
ブラウザの JavaScript が単一スレッドなのは、DOM を複数スレッドから触らせないためです。
複数のスレッドが同時に DOM を書き換えられるとしたら、ロックが必要になります。「この要素を触る間、他のスレッドは待て」という排他制御を、Web ページを書くすべての人が正しく扱わなければなりません。デッドロックも競合状態も日常的に起きるでしょう。
単一スレッドなら、この問題が定義上発生しません。ある処理が動いている間、他の処理は絶対に割り込まない。だから element.textContent を読んで書き換えるまでの間に、誰かが横から変えることはありえない。
この単純さと引き換えに受け入れたのが、「1つの重い処理が全部を止める」という性質です。
非同期は「並列」ではない
ここが最も誤解されるところです。fetch が非同期なのは、別スレッドで JavaScript が動いているからではありません。
ネットワーク通信そのものはブラウザ(C++ で書かれた部分)が担当し、JavaScript のスレッドは関与しません。通信が終わると、ブラウザが「完了しました」というタスクをキューに積む。JavaScript のスレッドは、手が空いたときにそれを取り出して then を実行する。
つまり JavaScript は待っていないのではなく、待つ仕事を他人に渡しているだけです。渡せる相手がいる仕事(ネットワーク・タイマー・ファイル読み込み)は非同期にできますが、計算そのものは渡す相手がいないので非同期にできません。
setTimeout(fn, 0) と Promise.resolve().then(fn)。先に実行されるのはどちらですか?
演習 — まず自分で判断する
解説を読む前に、まず自分で判断してみてください。ここで一度詰まっておくと、 次の節の判断軸が「なるほど」ではなく「そう来たか」に変わります。
この処理を、体感を損なわずに実装するにはどうしますか?
- 管理画面で「レポート生成」ボタンを押すと、以下を実行する
- ① API から売上データ 5 万件を取得する(約 1.2 秒)
- ② API からユーザーマスタ 8 千件を取得する(約 0.4 秒)
- ③ ①と②を突き合わせて集計する(純粋な JS 計算で約 1.5 秒)
- ④ 集計結果を表にして DOM に描画する
- ①と②に依存関係はない
- 現在の実装は、await で①→②→③→④を順に実行している
- · どの処理がメインスレッドを占有し、どれが占有しないか
- · 同時に始められるものを直列にしていないか
- · 占有を避けられない処理をどう扱うか
「スピナーを出したのに回らない」と言う同僚に、何が起きているかを説明してください
- 相手は重い集計処理の前にスピナーの DOM を追加している
- 相手は async/await を付ければ非同期になると考えている
- 処理は 10万件のループで 800ms かかる
読み終わりましたか?
読了にすると、これを前提とする記事がロードマップで開放されます。