ファイルを分けた途端、変数が初期化前だと怒られた。循環importの断ち切り方
片方だけ直せば、それで十分だった。
日本進化シミュレーション第4回 / 全16回目次
ファイル分割を始めて数日、コンソールに見慣れないエラーが出た。
ReferenceError: Cannot access 'scene' before initialization
意味は分かる。sceneという変数を、まだ用意できていないうちに使おうとしている。
でも、コードを目で追う限り、ちゃんと順番通りに書いてあるように見えた。
分からないまま、その日は終わった。
犯人は、お互いを参照しあう2つのファイル
翌日、落ち着いて僕が追ってみて分かった。
植生を扱うvegetation.tsが、シーンを管理するscene.tsからsceneという変数を読み込んで、そこへ木をscene.add(...)で足していた。
ところがscene.tsのほうも、vegetation.tsの値を使っていた。
AがBを読み込み、BもAを読み込んでいる。
ES modulesはこの循環自体は許してくれる。
ただし片方がまだ自分の本体を実行し終える前に、もう片方がそのconstをトップレベルで即座に参照すると、今回のエラーになる。
単一ファイルだった頃は、この問題はそもそも存在しなかった。
上から下へ順番に書かれているだけだったから。
ファイルを分けたことで、初めて「どっちが先に実行されるのか」という問題が生まれた。
両方直す必要はなかった
最初は「循環を断ち切るために、どちらのimportも消さないと」と身構えていた。
でも実際にやってみると、片方だけ直せば十分だった。
値を直接importするのをやめて、setter関数越しの間接参照に変える。
// 提供する側(scene.ts)let _leafMeshRound: any = null;export function setLeafMeshRoundRef(m: any) { _leafMeshRound = m; }// 使うときは _leafMeshRound.xxx// 供給する側(vegetation.ts)setLeafMeshRoundRef(leafMeshRound);function宣言は、モジュール本体がまだ実行し終わっていなくても参照できる。constのような一時的な参照不可の期間(TDZ)に入らないからだ。
だから、setter関数を介した呼び出しなら、循環の途中でも安全に呼べる。
片方だけ直せば足りる、という判断
この手のバグに出会うと、つい「循環そのものが悪」という気になって、依存関係を全部洗い出して整理し直したくなる。
実際にはそこまでしなくても直った。
片方を間接参照に変えるだけで、実行順の問題は解消する。
もう片方はそのままの直接importで構わない。
機械的に分割した約28ファイルの間には、似たような循環がまだいくつか残っているはずだ。
その全部を今すぐ洗い出すより、エラーが出たところから一つずつ直していく方針にした。
切り分けで一番効いたのは、新しいタブ
この調査の途中、直したはずなのに直らないように見える瞬間が何度かあった。
原因はコードではなく、開発サーバーのホットリロードが壊れた状態のまま残っていたタブを見ていたことだった。
新しいタブを開いてコンソールを見直すと、さっきまでのエラーが跡形もなく消えている。
以来、挙動が怪しいと感じたら、まず疑うのはコードより先にタブの鮮度になった。
※運営者が個人で書いているnoteです。