iSlider的整體思路是,不考慮邊界的情況下,我們實際滑動的時候,其實只有三張圖片會出現在我們的視角里,所以首先將滑動的內容外層dom拷貝,然後將這個dom的內容置空:
doc.innerHTML=""
複製程式碼
然後將當前包含有滑動內容的_prev, _current, _next三個dom節點插入外層dom:this.wrap。滑動到下一張,則之前的_prev被刪除,下一張dom被新增進this.wrap;滑動到上一張,則之前的_next被刪除,上一張dom被新增進this.wrap。
/* iSlider建構函式裡面其實已經寫好this.opts
* 使用者在new這個建構函式的時候,如果傳入opts引數
* 則使用for in 遍歷使用者傳參
* 然後覆蓋掉預設的opts
**/
//例項化建構函式
new iSlider({
//some configurations...
})
//建構函式內部
//關於HTML5 storage的詳細介紹,點選。。。
function iSlider(opts){
this.opts = {
//some configurations...
};
/* 這裡的this.SS其實是引用了sessionStoratge
* 為什麼要使用sessionStorage呢?
* 假設使用者點選列表的詳情button
* 進入詳情頁面後,退回滑動列表
* 使用者其實是想回到剛剛的滑動位置
* 而當前的視窗(瀏覽器)未關閉
* sessionStorage還儲存著
* 所以用session是最理想的選擇
**/
this.SS = false;
//ios使用私密模式,會報錯
try {
this.SS = sessionStorage;
this.['spt'] = 1;
} catch(e){
this.SS = 0;
}
//初始化
this.init();
}
複製程式碼
iSlider建構函式的原型物件,定義了init在內的大部分方法:
//iSlider的原型物件被覆寫了
//我覺得這裡其實應該將constructor重新指回iSlider
iSlider.prototype = {
//這裡介紹幾個屬性:
_sesiionKey: location.host + location.pathname,
_tpl: [],
_prev: null,
_current: null,
_next: null,
//_sesiionKey, _tpl引數在init中被賦值
init: function(){
/* _sesiionKey: location.host + location.pathname
* 用location作為ID,更有辨識度
**/
this._sesiionKey = btoa(encodeURIComponent(this._sessionKey+this.wrap.id+this.wrap.className));
var lastLocateIndex = parseInt(this.SS[this._sessionKey]);
//index代表當前圖片, 是當前圖片的索引值
/* this.index = ...這句話很重要
* 假設詳情頁有一個“回到列表”的button
* 點選button, 頁面被重新整理一遍
* init被重新執行, this.index被重新賦值
* this.SS[_sessionKey]會在init和prev, next函式中被賦值
**/
if(this.SS){
this.index = (this.opts.lastLocate && lastLocateIndex>=0) ? lastLocateIndex : 0;
}else {
//...
}
//querySelector查詢".wrap"
//然後this.wrap查詢到wrap的dom節點
//拷貝一份dom給this._tpl,避免對dom直接操作
this._tpl = this.wrap.cloneNode(true);
//將外層wrap dom裡面的滑動內容返回給_tpl
this._tpl = this.opts.item ? this._tpl.querySelectorAll(this.opts.item) : this._tpl.children;
//...
if(this.opts.fullScr){
//如果是全屏
//這裡新增css樣式
//html,body{...}
}
//...
//初始化DOM
this._setHTML();
//事件委託的方式,繫結事件
this._bindEvt();
},
}
複製程式碼
這幾個值,是在this._setHTML()中賦值
_prev: null, // 上一個節點
_current: null, // 當前節點
_next: null, // 下一個節點
複製程式碼
this._setHTML主要是初始化頁面滑動區域的dom節點,這裡用到了createDocumentFragment 即dom碎片的方式,優化了效能
iSlider.prototype = {
//...
_setHTML: function(){
//對頁面的滑動區域置空
this.wrap.innerHTML = ""
//建立DOM 碎片
var initDOM = document.createDocumentFragment();
//下面對上述三個屬性賦值
if(this.index > 0){
//如果index>0,則表示當前節點包含了至少兩個
this._prev = this._tpl[this.index-1].cloneNode(true);
//前移clientWidth//clientHeight大小
this._prev.style.cssText += this._getTransform('-'+this.scrollDist+'px');
initDom.appendChild(this._prev);
}else{
/* 重新置為null
* 主要是為了後面滑動事件時判斷
* if(this._prev){
表示當前頁面的上一張存在,
則可以往前滑動
}else{...}
**/
this._prev = null
}
this._current =this._tpl[this.index].cloneNode(true);
this._current.style.cssText+=this._getTransform(0);
initDom.appendChild(this._current);
//同理,對_next節點賦值
if (this.index<this.length-1) {
this._next=this._tpl[this.index+1].cloneNode(true);
this._next.style.cssText+=this._getTransform(this.scrollDist+'px');
initDom.appendChild(this._next)
}else {
this._next=null;
}
this.wrap.appendChild(initDom);
}
}
複製程式碼
然後是this._bindEvt函式,該函式將事件繫結到父節點上。即當前頁面是全屏滑動的時候,事件繫結在dom上,否則繫結到this.wrap外層dom上。
/* 這裡的"data-stop"屬性
* 我理解為作者的業務程式碼
* 即如果target設定了data-stop屬性
* 並且該屬性值為"true",就不用阻止預設行為
**/
if (this.opts.fullScr || this.opts.preventMove) {
handlrElm.addEventListener('touchmove', function (e) {
e.target.getAttribute('data-stop') !== "true" && e.preventDefault();
}, false);
}
複製程式碼
_pageInit()函式主要是為每一個滑動dom新增"play"的class, 以及動畫回撥。
之後是三個滑動事件, 分別是_touchstart, _touchmove, _touchend.
_touchstart: 這裡有幾個變數需要梳理, lockSlide: 標記變數. 如果lockSlide為true, 則表示當前不允許滑動, 即退出之後的_touchmove, 這裡有個疑問: 每次_touchstart都會將該變數置為false _touchmove裡面的以下判斷, 應該是沒有必要的?
_touchmove: function(){
/*............................*/
if(e.touches.length !== 1 || this.lockSlide){
return;
}
}
複製程式碼
_touchstart: function(){
this._touchstartX = e.touches[0].pageX;
this._touchstartY = e.touched[0].pageY;
//初始化觸控位置
this.touchInitPos = this.opts.isVertical ? e.touches[0].pageY:e.touches[0].pageX;
//為了避免卡幀的情況出現
//我們需要清除動畫效果
//判斷_next, _prev是否存在
//存在的話, duration置為0
//this._current.style.cssText = ...
if(this._next){
//
}
if(this._prev){
//
}
}
複製程式碼
_touchmove事件。下面這段程式碼比較有意思 do-while迴圈, 迴圈條件是 parent不為null , 且parent不是外層dom .wrap。結束迴圈的其中一種可能是: parent為this.wrap。會出現這種情況, 則說明, 使用者在滑動元件區域滑動手指的時候, 滑動的範圍仍然是在滑動元件的區域裡(因為使用了事件委託, e.target是觸控點). 即e.target是外層.wrap的子節點. 所以能正常的觸發後面的滑動效果.
還有一種情況. 就是parent為null的時候, 迴圈終止. 也就是說此時遍歷到了document(document.parentNode值為null), 緊接的if判斷, 終止touchmove事件. 這種情況只有一種可能, 就是手指滑到了this.wrap的外面.
_touchmove: function(){
var parent = e.target;
do{
parent = parent.parentNode;
}while(parent && parent != this.wrap)
if(!parent && e.target != this.wrap){
return ;
}
複製程式碼
滑動的時候,需要判斷手指滑動方向, 是否與頁面滑動方向一致. gx, gy分別代表一個三角形的x, y邊. 根據幾何知識, 垂直三角形的底邊. 哪邊長,則邊所對應的角大. gx>gy, 則手指滑動方向往x軸傾斜, 即判定手指為橫向滑動.
_touchmove: function(){
/*********分割線*************/
var gx=Math.abs(e.touches[0].pageX - this._touchstartX);
var gy=Math.abs(e.touches[0].pageY - this._touchstartY);
if (gx>gy && this.opts.isVertical) {
//頁面是垂直滑動
//當前手指在做水平滑動
this.lockSlide=true;
return ;
}else if(gx<gy && !this.opts.isVertical){
//頁面是水平滑動
//當前手指在做垂直滑動
this.lockSlide=true;
return ;
}
}
複製程式碼
接下來, 就主要是計算偏移量, 如果this.totalDist<0, 那麼就露出(不是滑到)下一張, 反之上一張. 這裡用到了tranlante3d的屬性(啟用GPU加速的話).
接著再詳細講一下_loading函式: 由於圖片的大小不一樣, 導致載入成功所耗費的時間也不一樣, 這裡的思想是, 不管後面的圖片載入得怎麼樣, 首先需要保證首張(首屏)圖片載入成功, 所以當this.src===imgurl[0]的時候, 清除回撥(置為null, 同時也釋放了部分記憶體)
for (var i=0; i<imgurls.length; i++) {
imgs[i]=new Image();
imgs[i].src=imgurls[i];
imgs[i].onload=imgs[i].onerror=imgs[i].onabort=function (e) {
loaded++;
if (this.src === imgurls[0] && e.type === 'load') {
clearTimeout(fallback)
}
checkloaded();
this.onload=this.onerror=this.onabort=null;
}
}
複製程式碼
[0] iSlider原始碼