iSlider移動端原生滑動元件原始碼解讀

So ?發表於2018-03-09

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原始碼

相關文章