久しぶりに今更なネタですが、JavaScriptの遅延ロードについてです。
単純にJavaScriptのコードで動的にスクリプトを読み込みたいだけならオンデマンドにScript
タグをDOMに追加するだけですみます。
ところが、遅延ロードありきでライブラリやらを構築し出すと、読み込み完了している必要があるだとか依存関係だとかの問題が発生するわけです。
これから説明するコードは拙作のライブラリ(dq.js)から抜粋しています。
とりあえず遅延ロード
スクリプト内から動的にスクリプトをロードするinclude()
関数の例です。
include = function (file) { var s = document.createElement('script'); s.src = file; s.type = 'text/javascript'; s.defer = 'defer'; document.head.appendChild(s); }
このコード自体はなんでも無いですね。script
タグをheadタグに追加しているだけです。
ロード完了を待つ
そして読み込み完了を待って、任意のコードを実行するlazyLoad()
を実装します。
lazyLoad = function(url, ctl, fn) { function _check(ctl) { //オブジェクトの有無でロード完了を判定 return !!(eval(ctl)); } if (_check(ctl)) { fn && fn(); //ロード済みなら指定の関数をすぐ実行 } else { include(url); setTimeout(function () { if (!_check(ctl)) { setTimeout(arguments.callee, 15.625); } else { fn && fn();//読み込みが終わったら関数を実行 } }, 15.625); } }
今でもそうだと思うのですが、scriptタグの読み込み完了イベントは拾えないので代わりに遅延ロードしたスクリプトを実行した結果として作成されるオブジェクトの有無を判定に利用します。
後は既に読み込み済みなら遅延ロードを実行しないようにチェックを挟んでおきます。
複数箇所からの呼び出し
先のコードではとりあえず読み込み済みのチェックは実施していますが、遅延ロードが完了する前に複数回同じスクリプトが呼び出されると無駄があったり面倒くさいことが発生するかもしれません。
そこで同じスクリプトに対しては一度しかinclude()が呼び出されないようにします。
_urls = []; lazyLoad = function (url, ctl, fn) { function _check(ctl) { return !!(eval(ctl)); } if (_check(ctl)) { fn && fn(); _remove$lazyLoad(); } else { if (_urls[url]) { //リクエスト済みならfnのみ登録して終了。 _urls[url].push(fn); return; } _urls[url] = []; include(url); _urls[url].push(fn); setTimeout(function () { if (!_check(ctl)) { setTimeout(arguments.callee, 15.625); } else { for (var i = 0; i < DQ._urls[url].length; i++) { _urls[url][i] && DQ._urls[url][i].call(); } delete _urls[url]; } }, 15.625); } }
読み込み中のスクリプトと読み込み完了時に実行される関数を_urls
に追加しています。
`
複数のスクリプトを遅延ロードする
複数のスクリプトを遅延ロードしてから自身のコードを実行したい場合どうなるのでしょうか。
lazyLoad("first.js", "Window.FIRST", function() { layzLoad("second.js", "Window.SECOND", function() { ... //自分のコード }); });
もしくは
lazyLoad("first.js", "Window.FIRST"); layzLoad("second.js", "Window.SECOND", function() { ... //自分のコード });
まあ、どちらもいまいちですね。
そこで、全ての遅延ロードが完了したら呼び出される、afterLoad()
という関数を用意します。
lazyLoad = function (url, ctl, fn) { function _check(ctl) { return !!(eval(ctl)); } if (_check(ctl)) { fn && fn(); __remove$lazyLoad(); } else { if (_urls[url]) { _urls[url].push(fn); return; } _urls[url] = []; include(url); _urls[url].push(fn); setTimeout(function () { if (!_check(ctl)) { setTimeout(arguments.callee, 15.625); } else { for (var i = 0; i < _urls[url].length; i++) { _urls[url][i] && _urls[url][i].call(); } delete _urls[url]; _remove$lazyLoad(); } }, 15.625); } _ function _remove$lazyLoad() { var cnt = 0; for (var nm in DQ._urls) { cnt += _urls.hasOwnProperty(nm) ? 1 : 0; } if (cnt == 0) { __trigger && setTimeout(DQ.__trigger, 0); } } } __trigger = function () { for (var i = 0; i < _onLoad.length; i++) { _onLoad[i].call(); } _onLoad.length = 0; delete __trigger; } _onLoad = []; afterLoad = function (callback) { _onLoad.push(callback); }
使い方は以下の通りです。
lazyLoad("first.js", "Window.FIRST"); layzLoad("second.js", "Window.SECOND"); afterLoad(function() { ... //自分のコード });
より複雑なケースでは問題が出る可能性もありますが、そこは宿題ということで。