開發圖片預載入框架
本文會依照“產生需求——>實現需求——>優化程式碼”的過程來講解,主要原因在於:我們是要依據我們的需求而構思程式碼,而不是分析一段成品程式碼。因此在最初並不給出最終成品程式碼。
關於成品程式碼
關於成品程式碼,利利已經上傳到GitHub當中,各位可以訪問並下載(連結在文章最底部),除了基本功能JS之外,利利還上傳了API文件(README.md檔案,可以用Sublime編輯器開啟)以及相關demo,輔助大家理解。
功能需求
功能需求:在原有程式碼基礎上做優化,並針對程式碼進行封裝,提升程式碼的複用性。(為了方便大家檢視,我已經把之前的成品程式碼放置在了步驟一中),如果想要回看具體的“圖片預載入”的知識,可以回覆“預載入”到公眾號
基本功能需求分析與實現流程
1 調整程式碼並調整變數名稱(此步驟純粹是為了防止變數名對大家產生的影響)
2 思考預載入時需要哪些屬性和方法,進行封裝
2.1 思考必要的屬性和方法(主要是為了複用性,此處通常會考慮函式封裝)
2.2 函式封裝與引數控制(通常使用引數進行不同功能的控制)
2.3 利用物件優化引數
3 功能優化以及bug排查
3.1 防止閃屏出現
3.2 newImg.src的位置 - 相容問題
3.3 onload方法賦值為null
4 高階優化 - 混合模式封裝
4.1 混合模式封裝的基本原理
4.2 混合模式封裝的需求原因
4.3 關於混合模式方面的相關知識
4.4 混合模式的程式碼實現
4.5 如何看待混合模式這些高大上的知識點
5 訪問層面的優化
5.1 防止全域性作用域受影響
5.2 工程師更方便的訪問
5.3 保證全域性下能夠訪問
能看出來嗎?其實今天講解的最主要的內容並非是簡單的知識點,而是一段程式碼的“優化”過程~用我經常逗我學生的一句話就是:“如何讓你把自己寫的一段程式碼,從讀得懂優化修改成你完全看不懂~”(程式碼優化以及擴充套件性、複用性的提升,其實是JS層面的靈魂)
1 之前的程式碼與基本變數名稱修改
之前的程式碼
var loadImg = ['test (1).jpg', 'test (2).jpg'];
var imgsNum = loadImg.length;
var nowNum = 0;
var nowPercentage = 0;
for (var i = 0; i < imgsNum; i++) {
var newImg = new Image();
newImg.onload = function() {
nowNum++;
if (nowNum == imgsNum) {
complete();
};
progress();
}
newImg.src = loadImg[i];
};
function complete() {
// 全部載入完畢之後要執行的程式碼
}
function progress() {
// 每載入完一張需要執行的程式碼
}
我們針對這段程式碼修改一下,這樣更有利於大家看懂我書寫的框架(此處主要是變數名的更換,並不涉及到什麼其他方面的東西)
var fileArr = ['test (1).jpg', 'test (2).jpg'];
var current = 0;
var percentage = 0;
for (var i = 0; i < fileArr.length; i++) {
var newImg = new Image();
newImg.onload = function() {
current++;
if (current == fileArr.length) {
complete();
};
progress();
}
newImg.src = fileArr[i];
};
function complete() {
// 全部載入完畢之後要執行的程式碼
}
function progress() {
// 每載入完一張需要執行的程式碼
}
2 提取預載入時需要的屬性和方法,進行功能封裝
2.1 需要的基本屬性和方法(每次呼叫都不同的)
2.1.1 不難想象,預載入,需要有預載入的物件,也就是那些圖片,這個屬性必不可少。
2.1.2 每一張圖片載入完成之後通常都需要執行一些東西(如修改loading條、修改頁面中的百分比值等)
2.1.3 當所有圖片載入完畢之後,必然要執行一些功能。
每一次在實現預載入時,上面的三點需求必然都不相同,那麼我們此處能夠想到的就是針對封裝後的函式,傳遞不同的引數來實現。
這個部分,決定著我們呼叫這個函式的方法以及函式中必須有屬性來接收這幾個“引數”。因此我們進行函式封裝。
2.2 函式封裝以及引數的使用
function filePreLoad(files, progress, complete) {
var fileArr = files;
var current = 0;
var percentage = 0;
for (var i = 0; i < fileArr.length; i++) {
var newImg = new Image();
newImg.onload = function() {
current++;
progress();
if (current == fileArr.length) {
complete();
};
}
newImg.src = fileArr[i];
};
}
2.3 引數的再度優化
三個引數實在是太複雜了,因此我們可以調整一下,將三個引數合併到一個物件當中,之後修改我們裡面的各類變數名即可。
var obj = {
files : [],
progress : function(precent, currentImg) {
// 具體程式碼 - HTML5學堂
},
complete : function() {
// 具體程式碼 - HTML5學堂
}
}
filePreLoad(obj);
function filePreLoad(obj) {
var fileArr = obj.files;
var current = 0;
var percentage = 0;
for (var i = 0; i < fileArr.length; i++) {
var newImg = new Image();
newImg.onload = function() {
current++;
var precentage = parseInt(current / obj.files.length * 100);
obj.progress(precentage, newImg);
if (current == fileArr.length) {
obj.complete();
};
}
newImg.src = fileArr[i];
};
}
此段程式碼中需要注意一點,對於progress,在執行時,應當能夠了解到當前的載入進度情況以及當前載入的具體影象,因此我們增加了計算百分比的語句,並修改了實參,即:
var precentage = parseInt(this.current / this.files.length * 100);
3 功能優化以及bug排查
當前的程式碼的確已經能夠實現圖片預載入的功能。但是卻存在著一定的問題。
3.1 閃屏的可能
我們採用上面的這種方式,會引發一個問題,當我們沒有把建立img標籤插到頁面時,會在切換圖片的時候出現“一閃”的現象,因此我們需要拿一個容器把這些需要預載入的圖片放置於頁面當中。
注意:盛放這些圖片的元素是不能夠出現在可視視窗中的,不然豈不是頁面亂了套?
於是在filePreload函式中增加如下程式碼
var box = document.createElement('div');
box.style.cssText = 'overflow:hidden; position: absolute; left: -9999px; top: 0; width: 1px; height: 1px;';
document.body.appendChild(box);
在newImg.onload函式當中增加如下程式碼
box.appendChild(newImg);
3.2 關於newImg.src的位置
在ie和opera下,先賦值src,再賦值onload,因為是快取圖片,就錯過了onload事件的觸發)。應該 先繫結onload事件,然後再給src賦值
3.3 在圖片載入完成之後,需要將onload方法賦值為null
經過測試,該步驟不適用於當前的這種函式封裝,適用於第四步之後的原型封裝,請各位務必注意。
不將onload方法賦值為null的壞處
1 此處建立了一個臨時匿名函式來作為圖片的onload事件處理函式,形成了閉包。ie下的記憶體洩漏中有一種是“迴圈引用”,閉包就有儲存外部執行環境的能力(依賴於作用域鏈的實現),所以newImg.onload這個函式內部又儲存了對newImg的引用,這樣就形成了迴圈引用,導致記憶體洩漏。(這種模式的記憶體洩漏只存在低版本的ie6中,打過補丁 的ie6以及高版本的ie都解決了迴圈引用導致的記憶體洩漏問題)。
2 另外,當我們遇到gif這種動態圖的載入時,可能會多次觸發onload。
解決辦法
為了解決上面這兩個問題,我們會在圖片下載完成之後先將newImg.onload設定為null。這樣既能解決記憶體洩漏的問題,又能避免動態圖片的事件多次觸發問題。
回撥函式與newImg.onload = null的位置:之前看過很多框架,大部分的框架都是在callback執行以後,才將newImg.onload設定為null,這樣雖然能解決迴圈引用的問題,但是對於動態圖片來說,如果callback執行比較耗時的話,還是有多次觸發的隱患的。
程式碼調整
newImg.onload = function() {
在上面這段程式碼之後,增加這樣一行程式碼:
newImg.onload = null;
於是,經過我們的修改,程式碼變成了這個樣子
// HTML5學堂提示:上面的呼叫過程在此省略
function filePreLoad(obj) {
var fileArr = obj.files;
var current = 0;
var percentage = 0;
// 新增程式碼
var box = document.createElement('div');
box.style.cssText = 'overflow:hidden; position: absolute; left: -9999px; top: 0; width: 1px; height: 1px;';
document.body.appendChild(box);
for (var i = 0; i < fileArr.length; i++) {
var newImg = new Image();
newImg.onload = function() {
// newImg.onload = null; 註釋掉,在這種模式下有問題
current++;
box.appendChild(newImg); // 新增程式碼
var precentage = parseInt(current / obj.files.length * 100);
obj.progress(precentage, newImg);
if (current == fileArr.length) {
obj.complete();
};
}
newImg.src = fileArr[i]; // 注意位置
};
}
到此為止,功能的封裝已經實現。這個框架也可以正常使用了~
4 更進一步? - 混合模式封裝
實現混合模式的部分,如果沒有接觸過面向物件、this、原型知識的童鞋,大家可以簡單的理解為:“將for迴圈位置開始的功能,封裝拆解,分別儲存在了3個函式當中,並相互呼叫訪問。”
利利溫馨提醒:該文章從此步驟開始,難度係數加大,學習該知識的先決條件為面向物件、原型繼承(混合模式)以及圖片預載入。建議在這幾個方面存在盲點的童鞋,先掌握這幾個知識,再看這篇文章,不然非常難看懂(畢竟知識是有階梯性的)。
4.1 混合模式封裝原理
屬性使用構造模式寫法,而方法掛載在原型上,並且將巢狀的函式拆分掉。
4.2 為何要再寫成面向物件形式
可能有人會問:對於預載入這個例子來說,普通的構造模式完全夠用了,一個物件解決,換句話說空間也還是這個物件,沒必要使用原型,複用的話可以在內部定義一個初始化函式。
對於此處的問題,如果我們基於當前功能往後封裝,就是JS的核心功能庫,幾十行,放一個js,沒啥必要,比如希望將移動端的一些功能封裝在一起,此時就需要用到混合模式了。換句話說,當從一個效果的複用性提升到一個個專案核心功能程式碼的複用性時,就需要考慮這個層面的東西。
4.3 關於混合模式的知識點
如果想具體瞭解混合模式的內容,可以回覆“混合模式”到公眾號
4.4 混合模式封裝的程式碼
function filePreLoad(obj) {
this.files = obj.files;
this.progress = obj.progress;
this.complete = obj.complete;
// 當前載入數量為0
this.current = 0;
// 容器設定
this.box = document.createElement('div');
this.box.style.cssText = 'overflow:hidden; position: absolute; left: -9999px; top: 0; width: 1px; height: 1px;';
document.body.appendChild(this.box);
this.getFiles();
}
// 獲取每一個圖片
filePreLoad.prototype.getFiles = function() {
var fileArr = [];
for (var i = 0; i < this.files.length; i++) {
fileArr[i] = this.files[i];
this.loadImg(fileArr[i]);
};
}
// 載入影象
filePreLoad.prototype.loadImg = function(file) {
var _this = this;
var newImg = new Image();
newImg.onload = function(){
newImg.onload = null;
_this.loadFtn(newImg);
}
newImg.src = file;
}
// 執行相關回調
filePreLoad.prototype.loadFtn = function(currentImg) {
this.current++;
this.box.appendChild(currentImg);
if (this.progress) {
var precentage = parseInt(this.current / this.files.length * 100);
this.progress(precentage, currentImg); // 需要返回些什麼呢?
};
if (this.current == this.files.length) {
if (this.complete) {
this.complete();
};
};
}
Plus: 可能有人看完程式碼之後想問 —— 為何要多拆分一個功能函式?
本著面向物件程式碼中,不會有函式巢狀函式的現象,其實這段程式碼並不需要拆解成這麼多的函式(getFiles和loadImg),此處利利主要是考慮以後可能會做“其他型別檔案”的預載入處理,因此拆分出來,便於之後程式碼的書寫
4.5 利利想囉嗦兩句~~~
利利表示,自己在書寫這個功能時,直接寫成了混合模式,然後又要講解,於是只能一步一步往回倒。順便也想提提這個方面的東西:很多人學東西都會比較“急於求成”,我自己有時候也是這樣。不過,話說回來,哪個攻城獅不希望能夠實現高大上的東西的呢?哪個又不希望自己寫出來的程式碼很流弊呢?
在大概3年前,利利僅僅對面向物件有所瞭解,能夠去寫個什麼使用者、屬性、方法,但是卻不知道如何應用,對於面向物件的瞭解浮於表面,那時候很希望能夠直接上手就能夠寫出“面向物件”的程式碼,但是這個“想法”在那時是不能夠實現的,因為我根本不知道從何下手;後來大概過了一年,到了2014年年初,自己突然對面向物件有了比較清晰的理解,開始逐漸的看明白this、想明白了意義;到了2014年中下旬吧,自己發現自己能夠去書寫面向物件的東西了,小效果、小遊戲,逐漸玩兒轉原型;而今,寫程式碼竟然能夠直接寫出一套“面向物件”的程式碼,說實在的,自己也有點兒小驚喜。
說了這麼多,其實利利只是想告訴當前看到下面這段程式碼,感到“不知從何下手”的童鞋:“知識是有一定的關係的,很多知識是其他知識的前置條件,看到一個東西我們希望能夠立即實現固然沒有錯,但是,卻需要我們擁有一定的基礎和沉澱。對於面向物件,this、作用域、函式封裝、原型都是其基礎,如果在這個方面並不是很瞭解,不建議糾結於上面這個步驟的程式碼,而更建議先學習這些基礎知識”,我自己在之前整理過相關的開發經驗,在HTML5學堂官網裡都能夠搜尋到,會比較方便大家對“面向物件”的理解,感興趣的可以檢視一下。
5 防止全域性作用域受影響並讓工程師能夠更方便的訪問
5.1 防止全域性作用域受影響,可以使用匿名函式
5.2 希望工程師能夠更方便的訪問,可以採用類似於JQ簡化呼叫的方法,具體詳見程式碼
5.3 為了保證全域性下能夠訪問,可以將函式賦值給window下的某個屬性
基於以上三點,我們針對程式碼做如下變化
(function(){
// HTML5學堂提醒:此處先放置上個步驟中的所有程式碼
// 如下程式碼為5.2的需求。[涉及知識:建構函式、函式返回值]
function preload(obj) {
return new filePreLoad(obj);
}
// 5.3的需求
window.preload = preload;
})();
具體成品程式碼,最開始利利就說過了,已經上傳到GitHub當中,評論區的連結,即可檢視下載。
從程式碼成品,到GitHub的維護,再到文章的書寫和案例的拆解,耗時14h……HTML5學堂小編-利利。
歡迎溝通交流~HTML5學堂
專案連結:https://github.com/iceswan/preload
小編提醒:使用第3步中的成品程式碼時,需要將 newImg.onload = function(){具體內容},修改為newImg.onload = (function(){具體內容})(); 需要立即令其執行,望注意~!!!