RequireJSで循環参照の回避

Backbone.jsの各ファイルをRequireJSでまとめて使ってるます。

今回生じた問題は、
「viewからrouterをRequireJSを使って読もうとしたとき、なぜか読み込まれない」現象

  • views/index.js
define(['router', 'backbone'], function(router, Backbone) {
  var IndexView = Backbone.View.extend({
    events: {
      'click .hoge': 'enterRoom'
    },
    enterRoom: function() {
      // ここでrouterよんでる。クリックされたら他のページに飛ぶ的な
      router.navigate('/room', {trigger: true});
    },
  });
  return IndexView;
});
  • router.js
define(['views/index', 'backbone'], function(IndexView, Backbone) {
  var MainRouter = Backbone.Router.extend({
    routes: {
      '': 'index',
      'room/:roomName': 'roomShow'
    },
    index: function() {
      // ここでIndexViewよんでる。
      var indexView = new IndexView();
      // 表示する処理。略
    }
    // roomShow 略
  });
  var router = new MainRouter();
  return router;
});

こんなかんじにした時、routerからIndexViewが読めなくなっている(ログをはくとundefined)現象がおきていた。

調べてみると、routerがIndexViewを参照し、IndexViewがrouterを参照しているため、循環参照が起きていて、
そのせいでうまくいかなくなっているらしい。

試してみた結果、次のようにすることで割と楽に回避できた。
(RequireJS本家のドキュメント通りだと、無駄がある気がして、次のようにしても問題なく動きました)

  1. routerは変えない。
  2. IndexViewの中で、routerを使うときに、'require'を利用する。
  • views/index (訂正版)

// routerを呼ぶかわりにrequireを呼ぶ

define(['require', 'backbone'], function(require, Backbone) {
  var IndexView = Backbone.View.extend({
    events: {
      'click .hoge': 'enterRoom'
    },
    enterRoom: function() {
      // routerを使う時に、requireする
      var router = require('router');
      router.navigate('/room', {trigger: true});
    },
  });
  return IndexView;
});

ここでポイントとなるのは、defineの中でrouterを呼ぶのではなく、
使う箇所でrequire('router')として呼んでいること。
requireを使うことで、IndexViewを最初から呼ぶのではなく、使う時に呼ぶようにしていることで、きちんと読み込まれるようにしている。

だから、requireを使うのは(defineのさらに1つ下階層の)関数の中でなければならない。requireが関数の中じゃないと、最初にdefine関数が実行されるときにrequireで読みにいって、やっぱりundefinedになってしまう。
(上の例では使う場所がenterRoom関数の中なので、問題無し。)

まとめると、RequireJSで循環参照になってしまうときは多分、

  • 主呼び出しと思われる方はそのままに、
  • 副呼び出しのと思われる方で、(関数内で)requireを使うことで動的に呼び出す

ことで回避できる。

なお本家ドキュメントでは両方からrequire使って、さらにdefineでも定義しているので、もしかしたら本当はこうしないといけない理由があるのかもしれない。

さらに今回の話で言えば、使ってるのがBackbone.jsですし、
Viewがクリックしたらイベントを発火して、それをrouterが検知してルーティングを行うのが正解かもしれない。

毎度ながら日本語がへたくそでまとまりがない。。とりあえず参考url
RequireJS API
backbone.js - Require.js module not seeing Backbone Router.js - Stack Overflow