ファイルを分けた途端、変数が初期化前だと怒られた。循環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も書いています AI活用やサイト運営で気づいたことを、もう少し個人の視点で掘り下げています。

※運営者が個人で書いているnoteです。

技術ブログ一覧へ戻る