完全に状況を掌握した画像の遅延読み込みの実現
IE8の挙動について追記しました。
IE8は、img.complete は 画像読み込みでも true になりません(falseのままです)。 そのかわり、img.readyState が "complete" になります。
JavaScriptでの画像の動的/遅延読み込みといえば (new Image).src = URL; なんですが、
タイムアウトやエラーの状況を把握したい時もあったりします。GoogleMapライクなアプリを作ってるときとか。
今回ちょっと必要になったのでまずは調査から。
以下のコードで、存在しないファイル(missing.jpg)を読み込ませ、実行経路を確認してみます。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml11.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>JavaScript::loadImage test</title> </head> <body> <div id="footprint"></div> <script type="text/javascript"> var uu = window.uu = { /** 画像を非同期にロードする * * @param map map { array:path, function:onLoad, function:onError, * number:delay = 50, number:timeout = 2000 } * * path: 画像のURLの配列です。絶対パスまたは相対パスで指定します。 * onLoad: ロード完了後にコールバックするメソッドを指定します。 * 絶対URLを引数にcallbackします。 * onError: エラー, 読込中断, タイムアウト時にコールバックするメソッドを指定します。 * 絶対URLを引数にcallbackします。 * delay: 遅延時間の指定です。デフォルトは10msです。 * timeout: タイムアウト時間の指定です。デフォルトは2秒(2000ms)です。 * 0を指定するとタイムアウトしません。 */ loadImage: function(map) { uu.mix(map, { onLoad: uu.dummy, onError: uu.dummy, delay: 50, timeout: 2000 }, true); var images = uu.toArray(document.images); map.path.forEach(function(v) { uu["#loadImage"](images, v.match(RegExp("^(file|http|https):\/\/")) ? v : uu.getAbsolutePath(v), map.onLoad, map.onError, map.delay, map.timeout); }); }, "#loadImage": function(images, abspath, onLoad, onError, delay, timeout) { for (var i = 0, sz = images.length; i < sz; ++i) { if (images[i].src === abspath && images[i].complete) { onLoad(abspath); return; } } var img = new Image(), tick = 0; img.onabort = img.onerror = function() { window.status += "e"; if (img.complete) { window.status += "[+comp]"; } }; img.onload = function() { window.status += "l"; if (img.complete) { window.status += "[+comp]"; } }; img.src = abspath; if (timeout) { setTimeout(function() { window.status += "."; if (img.complete) { window.status += "[+comp2]"; } if (img.complete || (tick += delay) > timeout) { window.status += (img.complete) ? "C" : "T"; if (img.width) { window.status += "[w:" + img.width + "]"; } return; } setTimeout(arguments.callee, delay); }, 0); } }, mix: function(mapSrc, mapMixer, supplement /* = false */) { if (supplement) { for (var p in mapMixer) { (p in mapSrc) ? 0 : mapSrc[p] = mapMixer[p]; } } else { for (var p in mapMixer) { mapSrc[p] = mapMixer[p]; } } return mapSrc; }, toArray: function(map) { var rv = new Array(map.length || 0), i = 0, sz = rv.length; for (; i < sz; ++i) { rv[i] = map[i]; } return rv; }, getAbsolutePath: function(path) { var e = document.createElement("div"); e.innerHTML = '<a href=\"' + path + '\" />'; return e.firstChild.href; } }; if (!Array.prototype.forEach) { Array.prototype.forEach = function(iter, bindThis /* = undefined */) { if (typeof iter !== "function") { throw TypeError(); } var i = 0, sz = this.length; for (; i < sz; ++i) { (i in this) && iter.call(bindThis, this[i], i, this); } return this; }; } function imageLoaded(abs) { var e = document.getElementById("footprint"); e.appendChild(document.createTextNode(abs)); } function imageLoadError(abs) { window.status += "[" + abs + "]"; } window.onload = function boot() { var map = { path: ["missing.jpg"], onLoad: imageLoaded, onError: imageLoadError }; uu.loadImage(map); } </script> </body> </html>
読込失敗時に以下の挙動が確認できました。
また、1つのブラウザで複数の実行経路が存在することも確認しました。
ブラウザ | 実行経路 | 読込失敗時の挙動 | ||
---|---|---|---|---|
img.completeの 最終状態 | img.onload | img.onerror | ||
Firefox2(2.0.013) | .[+comp2]C | true | 呼ばれない | 呼ばれない |
Firefox3(Bata3) | e[+comp].[+comp2]C | true | 呼ばれない | 呼ばれる その時completeはtrue |
Safari3(3.1) | e[+comp].[+comp2]C | true | 呼ばれない | 呼ばれる その時completeはtrue |
Safari3(3.1) | .e[+comp].[+comp2]C | true | 呼ばれない | 呼ばれる その時completeはtrue |
Safari3(3.1) | .[+comp2]Ce[+comp] | true | 呼ばれない | 呼ばれる その時completeはtrue |
Opera9(9.25) | l..........T | false | 呼ばれる その時completeはfalse | 呼ばれない |
Opera9(9.50Beta) | e..........T | false | 呼ばれない | 呼ばれる その時completeはfalse |
IE6(6.0) | e..........T[w:28] | false | 呼ばれない | 呼ばれる その時completeはfalse |
IE7 | e..........T[w:28] | false | 呼ばれない | 呼ばれる その時completeはfalse |
IE8 | 未調査 |
次に、幅が160pxの存在するファイルを読み込ませた場合の挙動を調査しました。
ブラウザ | 実行経路 | 読込成功時の挙動 | ||
---|---|---|---|---|
img.completeの 最終状態 | img.onload | img.onerror | ||
Firefox2(2.0.013) | l[+comp][+comp2]C[w:160] | true | 呼ばれる | 呼ばれない |
Firefox2(2.0.013) | .[+comp2]C[w:160]l[+comp] | true | 呼ばれる | 呼ばれない |
Firefox3(Bata3) | l[+comp].[+comp2]C[w:160] | true | 呼ばれる | 呼ばれない |
Firefox3(Bata3) | .[+comp]C[w:160]l[+comp] | true | 呼ばれる | 呼ばれない |
Safari3(3.1) | l[+comp].[+comp2]C[w:160] | true | 呼ばれる | 呼ばれない |
Safari3(3.1) | .[+comp2]C[w:160]l[+comp] | true | 呼ばれる | 呼ばれない |
Safari3(3.1) | .l[+comp].[+comp2]C[w:160] | true | 呼ばれる | 呼ばれない |
Opera9(9.25) | l[+comp].[+comp2]C[w:160] | true | 呼ばれる | 呼ばれない |
Opera9(9.50Beta) | l[+comp].[+comp2]C[w:160] | true | 呼ばれる | 呼ばれない |
IE6(6.0) | l.[+comp2]C[w:160] | true | 呼ばれる | 呼ばれない |
IE7 | l.[+comp2]C[w:160] | true | 呼ばれる | 呼ばれない |
IE8 | 未調査 |
見事にバラバラですね(いつものことですが)。
結構厳しい気もしますが、めげずに実装してみます。
実装する機能は、
- 読込失敗でonerror()を一度だけ発生させ、タイムアウトは発生させない。
- 読込成功でonload()を一度だけ発生させる
です。
"#loadImage": function(images, abspath, onLoad, onError, delay, timeout) { for (var i = 0, sz = images.length; i < sz; ++i) { if (images[i].src === abspath && images[i].complete) { onLoad(abspath); return; } } var img = new Image(), tick = 0; // ここから下を差し替え img.finish = false; img.onabort = img.onerror = function() { if (img.finish) { return; } img.finish = true; onError(abspath); }; img.onload = function() { img.finish = true; if (window.opera && !img.complete) { onError(abspath); return; } onLoad(abspath); }; img.src = abspath; if (!img.finish && timeout) { setTimeout(function() { if (img.finish) { return; } if (img.complete) { img.finish = true; if (img.width) { return; } onError(abspath); return; } if ((tick += delay) > timeout) { img.finish = true; onError(abspath); return; } setTimeout(arguments.callee, delay); }, 0); } },
うまくやれました。
反省会: