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