網站開發時經常需要在某個頁面需要實現對大量圖片的瀏覽,如果考慮流量的話,大可每個頁面只顯示一張圖片,讓用戶每看一張圖片就需要重新下載一下整個頁面。在web2.0時代,更多人愿意用javascript來實現一個圖片瀏覽器,讓用戶無需等待過長的時間就能看到其他圖片。所以,對于一個網站來說,網頁的預加載就顯得尤為重要。
一、常規實現圖片預加載的方法
知道了一張圖片的地址,需要把它在一個固定大小的html容器(可以是div等)里邊顯示出來,最重要的當然是需要知道這張即將顯示的圖片的寬和高,然后再結合容器的寬和高,按照一定的縮放比例使圖片顯示出來。因此,實現圖片預加載就成為圖片瀏覽器的核心功能了。
做過圖片翻轉效果的朋友其實都知道,要讓圖片輪換的時候不出現等待,最好是先讓圖片下載到本地,讓瀏覽器緩存起來。這時,一般都會用到js里邊的Image對象。一般的手段無非這樣:
function preLoadImg(url) {
var img = new Image();
img.src = url;
}
通過調用preLoadImg函數,傳入圖片的url,就能使圖片預先下載下來了。實際上,馬海祥覺得這里用到的預下載功能也和這基本一致。圖片預下載下來后,通過img的width和height屬性,就能知道圖片的寬和高了。
但是需要考慮到,在做圖片瀏覽器功能時,圖片都是實時顯示的。比如你點了顯示的按鈕,這個時候才會調用上邊類似的代碼來加載圖片。因此,如果你直接用img.width的時候,圖片還沒有完全下載下來。因此,需要用一些異步的方法,等到圖片下載完畢的時候才會再對img的width和height進行調用。
實現這樣的異步方法實際上不難,圖片的下載完畢事件也很簡單,就是簡單的onload事件。因此,我們可以利用下面的代碼:
function loadImage(url, callback) {
var img = new Image();
img.src = url;
img.onload = function(){ //圖片下載完畢時異步調用callback函數。
callback.call(img); // 將callback函數this指針切換為img。
};
}
測試用例:
function imgLoaded(){
alert(this.width);
}
<input type="button" value="loadImage" onclick="loadImage('aaa.jpg',imgLoaded)"/>
在firefox中測試一下,發現不錯,果然和預想的效果一樣,在圖片下載后,就會彈出圖片的寬度來。無論點擊多少次或者刷新結果都一樣。
不過,做到這一步,先別高興太早——還需要考慮一下瀏覽器的兼容性,于是,趕緊到ie里邊測試一下。沒錯,同樣彈出了圖片的寬度。但是,再點擊load的時候,情況就不一樣了,什么反應都沒有了。刷新一下,也同樣如此。
經過對多個瀏覽器版本的測試,發現ie6、opera都會這樣,而firefox和safari則表現正常。其實,原因也挺簡單的,就是因為瀏覽器的緩存了。當圖片加載過一次以后,如果再有對該圖片的請求時,由于瀏覽器已經緩存住這張圖片了,不會再發起一次新的請求,而是直接從緩存中加載過來。
對于firefox和safari,它們視圖使這兩種加載方式對用戶透明,同樣會引起圖片的onload事件,而ie和opera則忽略了這種同一性,不會引起圖片的onload事件,因此上邊的代碼在它們里邊不能得以實現效果。
怎么辦呢?最好的情況是Image可以有一個狀態值表明它是否已經載入成功了。從緩存加載的時候,因為不需要等待,這個狀態值就直接是表明已經下載了,而從http請求加載時,因為需要等待下載,這個值顯示為未完成。這樣的話,就可以搞定了。
經過一些分析,馬海祥終于發現一個為各個瀏覽器所兼容的Image的屬性——complete。所以,在圖片onload事件之前先對這個值做一下判斷即可。最后,代碼變成如下的樣子:
function loadImage(url, callback) {
var img = new Image(); //創建一個Image對象,實現圖片的預下載
img.src = url;
if (img.complete) { // 如果圖片已經存在于瀏覽器緩存,直接調用回調函數
callback.call(img);
return; // 直接返回,不用再處理onload事件
}
img.onload = function () { //圖片下載完畢時異步調用callback函數。
callback.call(img);//將回調函數的this替換為Image對象
};
};
二、動態圖片的預加載技術
一般來說,技術人員在實現圖片預加載的大體思路都是這樣的:
function loadImage(url, callback) {
var img = new Image(); //創建一個Image對象,實現圖片的預下載
img.src = url;
if (img.complete) { // 如果圖片已經存在于瀏覽器緩存,直接調用回調函數
callback(img);
return; // 直接返回,不用再處理onload事件
}
img.onload = function () { //圖片下載完畢時異步調用callback函數。
callback(img);
};
};
小編覺得這個方法功能是ok的,但是有一些隱患,具體如下:
1、創建了一個臨時匿名函數來作為圖片的onload事件處理函數,形成了閉包。
相信大家都看到過ie下的內存泄漏模式的文章,其中有一個模式就是循環引用,而閉包就有保存外部運行環境的能力(依賴于作用域鏈的實現),所以img.onload這個函數內部又保存了對img的引用,這樣就形成了循環引用,導致內存泄漏。(這種模式的內存泄漏只存在低版本的ie6中,打過補丁的ie6以及高版本的ie都解決了循環引用導致的內存泄漏問題)。
2、只考慮了靜態圖片的加載,忽略了gif等動態圖片,這些動態圖片可能會多次觸發onload。
要解決上面兩個問題很簡單,其實很簡單,代碼如下:
img.onload = function () { //圖片下載完畢時異步調用callback函數。
img.onload = null;
callback(img);
};
這樣既能解決內存泄漏的問題,又能避免動態圖片的事件多次觸發問題。
在一些相關博文中,也有人注意到了要把img.onload 設置為null,只不過時機不對,大部分文章都是在callback運行以后,才將img.onload設置為null,這樣雖然能解決循環引用的問題,但是對于動態圖片來說,如果callback運行比較耗時的話,還是有多次觸發的隱患的。
隱患經過上面的修改后,就消除了,但是這個代碼還有優化的余地:
if (img.complete) { // 如果圖片已經存在于瀏覽器緩存,直接調用回調函數
callback(img);
return; // 直接返回,不用再處理onload事件
}
經過對多個瀏覽器版本的測試,發現ie、opera下,當圖片加載過一次以后,如果再有對該圖片的請求時,由于瀏覽器已經緩存住這張圖片了,不會再發起一次新的請求,而是直接從緩存中加載過來。對于 firefox和safari,它們試圖使這兩種加載方式對用戶透明,同樣會引起圖片的onload事件,而ie和opera則忽略了這種同一性,不會引起圖片的onload事件,因此上邊的代碼在它們里邊不能得以實現效果。
確實,在ie,opera下,對于緩存圖片的初始狀態,與firefox和safari,chrome下是不一樣的(有興趣的話,可以在不同瀏覽器下,測試一下在給img的src賦值緩存圖片的url之前,img的狀態),但是對onload事件的觸發,卻是一致的,不管是什么瀏覽器。產生這個問題的根本原因在于,img的src賦值與 onload事件的綁定,順序不對(在ie和opera下,先賦值src,再賦值onload,因為是緩存圖片,就錯過了onload事件的觸發)。應該先綁定onload事件,然后再給src賦值,代碼如下:
function loadImage(url, callback) {
var img = new Image(); //創建一個Image對象,實現圖片的預下載
img.onload = function(){
img.onload = null;
callback(img);
}
img.src = url;
}
三、比onload更快獲取圖片尺寸的預加載技術
大部分技術人員使用預加載獲取圖片大小的方法,基本都是通過如下的代碼實現的:
var imgLoad = function (url, callback) {
var img = new Image();
img.src = url;
if (img.complete) {
callback(img.width, img.height);
} else {
img.onload = function () {
callback(img.width, img.height);
img.onload = null;
};
};
};
從以上代碼,我們可以看到上面必須等待圖片加載完畢才能獲取尺寸,其速度馬海祥還真不敢恭維,對此,我們需要改進。
web應用程序區別于桌面應用程序,響應速度才是最好的用戶體驗。如果想要速度與優雅兼得,那就必須提前獲得圖片尺寸,如何在圖片沒有加載完畢就能獲取圖片尺寸呢?
據馬海祥十多年的上網經驗:瀏覽器在加載圖片的時候你會看到圖片會先占用一塊地然后才慢慢加載完畢,并且不需要預設width與height屬性,因為瀏覽器能夠獲取圖片的頭部數據。基于此,只需要使用javascript定時偵測圖片的尺寸狀態便可得知圖片尺寸就緒的狀態。
當然實際中會有一些兼容陷阱,如width與height檢測各個瀏覽器的不一致,還有webkit new Image()建立的圖片會受以處在加載進程中同url圖片影響,經過反復測試后的最佳處理方式:
// 更新:
// 05.27: 1、保證回調執行順序:error > ready > load;2、回調函數this指向img本身
// 04-02: 1、增加圖片完全加載后的回調 2、提高性能
/**
* 圖片頭數據加載就緒事件 - 更快獲取圖片尺寸
* @version 2011.05.27
* @author TangBin
* @see http://www.mahaixiang.cn/wyzz/546.html
* @param {String} 圖片路徑
* @param {Function} 尺寸就緒
* @param {Function} 加載完畢 (可選)
* @param {Function} 加載錯誤 (可選)
* @example imgReady('http://www.mahaixiang.cn/uploads/allimg/1405/1-14050212013ML.jpg', function () {
alert('size ready: width=' + this.width + '; height=' + this.height);
});
*/
var imgReady = (function () {
var list = [], intervalId = null,
// 用來執行隊列
tick = function () {
var i = 0;
for (; i < list.length; i++) {
list[i].end ? list.splice(i--, 1) : list[i]();
};
!list.length && stop();
},
// 停止所有定時器隊列
stop = function () {
clearInterval(intervalId);
intervalId = null;
};
return function (url, ready, load, error) {
var onready, width, height, newWidth, newHeight,
img = new Image();
img.src = url;
// 如果圖片被緩存,則直接返回緩存數據
if (img.complete) {
ready.call(img);
load && load.call(img);
return;
};
width = img.width;
height = img.height;
// 加載錯誤后的事件
img.onerror = function () {
error && error.call(img);
onready.end = true;
img = img.onload = img.onerror = null;
};
// 圖片尺寸就緒
onready = function () {
newWidth = img.width;
newHeight = img.height;
if (newWidth !== width || newHeight !== height ||
// 如果圖片已經在其他地方加載可使用面積檢測
newWidth * newHeight > 1024
) {
ready.call(img);
onready.end = true;
};
};
onready();
// 完全加載完畢的事件
img.onload = function () {
// onload在定時器時間差范圍內可能比onready快
// 這里進行檢查并保證onready優先執行
!onready.end && onready();
load && load.call(img);
// IE gif動畫會循環執行onload,置空onload即可
img = img.onload = img.onerror = null;
};
// 加入隊列中定期執行
if (!onready.end) {
list.push(onready);
// 無論何時只允許出現一個定時器,減少瀏覽器性能損耗
if (intervalId === null) intervalId = setInterval(tick, 40);
};
};
})();
這樣的方式獲取攝影級別照片尺寸的速度往往是onload方式的幾十多倍,而對于web普通(800×600內)瀏覽級別的圖片能達到秒殺效果。看了這個再回憶一下你見過的web相冊,是否絕大部分都可以重構一下的。