前言
作為前端技術(shù)人員,我們一開始使用著jquery做各種dom操作,使用基于它的插件例如nicescroll、pagination等等做各種特殊功能處理,后來業(yè)界又出現(xiàn)了backbone、angularjs、vue、react等等各種庫,給我們的開發(fā)帶來無盡的便利,但對這些庫或框架使用得越多,你有沒有感覺自己的底層能力越來越弱?對基本js的駕馭能力越來越力不從心?
這就是過渡使用框架的結(jié)果,我自己在這方面也深有體會,需要什么直接就去github上搜索相關(guān)的庫,直接引入到代碼中,當(dāng)長期以往,沒有底層能力的支撐,就像是一棟建筑沒有了地基一樣,底部不穩(wěn),往上想更進(jìn)一步也會越來越難。
廢話了這么多,核心一點(diǎn):通過造輪子去探索js底層的奧秘,學(xué)習(xí)那些優(yōu)美的藝術(shù),夯實(shí)js根基!
本篇主要實(shí)現(xiàn)swiper的核心功能:能讓它滑動起來!
<b>完整代碼可在我的github找到:</b>
https://github.com/kekobin/KSwiper
基本構(gòu)型
;(function() {
var KSwiper = function KSwiper(element, options) {//且傳進(jìn)來的必須為DOM
if(!element) return;
this.container = element;
this.element = this.container.children[0];
this.opts = options;
this.speed = options.speed || 100;
this.index = 0;//默認(rèn)開始的索引
this.callback = options.callback;
this.init();
};
if(typeof module !== 'undefined' && module.exports) {
module.exports = KSwiper;
} else if(typeof define === 'function' && (define.amd || define.cmd)) {
define(function() { return KSwiper; });
} else {
this.KSwiper = KSwiper;
}
}).call(function() {
return this || (typeof window != 'undefined' ? window : global);
}());
這個沒什么好說的,不過為了簡單,傳進(jìn)去的element必須為DOM(2.0版會增加相應(yīng)DOM操作功能).
關(guān)鍵的初始化
這一塊包括樣式的初始化和事件的初始化。
html結(jié)構(gòu)實(shí)例
<div class="kswiper-container">
<div class="kswiper-wrap">
<div class="kswiper-item">slide 1</div>
<div class="kswiper-item">slide 2</div>
<div class="kswiper-item">slide 3</div>
<div class="kswiper-item">slide 4</div>
</div>
</div>
樣式初始化
一個swiper應(yīng)該包括外部容器、內(nèi)部容器、子項(xiàng)目,外部容器應(yīng)該是overflow:hidden的,內(nèi)部容器應(yīng)該動態(tài)設(shè)置其寬度為 子項(xiàng)目寬度x子項(xiàng)目個數(shù)(如果子項(xiàng)目之間有間隙,則還應(yīng)該加上該間隙,此處忽略).
this.slides = this.element.children;
this.length = this.slides.length;
this.width = ('getBoundingClientRect' in this.container) ? this.container.getBoundingClientRect().width : this.container.offsetWidth;
//設(shè)置包裹元素的寬
this.element.style.width = this.width * this.length + 'px';
//設(shè)置slide item的樣式
var index = this.length;
while(index--) {
var slide = this.slides[index];
slide.style.width = this.width + 'px';
slide.style.height = '100%';
slide.style.display = 'table-cell';
}
至此,基本工作準(zhǔn)備就緒了,接下來才是能讓子項(xiàng)目滑動起來的關(guān)鍵!
事件初始化
既然是滑動,肯定要用到touch事件了。無論是左右滑動,還是上下滑動,核心是:在滑動結(jié)束后,看這次滑動是否滿足一定條件(例如左右滑動時,滑動了一個子項(xiàng)目的一半視為有效滑動,若有效滑動則將內(nèi)部容器橫向移動一個子項(xiàng)目的寬度,否則仍回到初始狀態(tài)),這是一種動態(tài)的變化過程,使用什么能做到這種動畫效果呢?答案是transform或者transform3D,這種動畫往往伴著著transitionEnd事件,這樣我們可以在動畫結(jié)束后作出回調(diào)處理。
if(this.element.addEventListener) {
this.element.addEventListener('touchstart', this, false);
this.element.addEventListener('touchmove', this, false);
this.element.addEventListener('touchend', this, false);
this.element.addEventListener('webkitTransitionEnd', this, false);
this.element.addEventListener('msTransitionEnd', this, false);
this.element.addEventListener('oTransitionEnd', this, false);
this.element.addEventListener('transitionEnd', this, false);
}
需要主意的是,這里對各種事件處理部分(即第二個參數(shù)),傳入的是this,是為了簡單、統(tǒng)一處理的目的,前提是在this對象上定義了handEvent方法:
KSwiper.prototype.handleEvent = function(event) {
var type = event.type;
switch(type) {
case 'touchstart':
this.onTouchStart(event);
break;
case 'touchmove':
this.onTouchMove(event);
break;
case 'touchend':
this.onTouchEnd(event);
break;
case 'webkitTransitionEnd':
case 'msTransitionEnd':
case 'oTransitionEnd':
case 'transitionEnd':
this.transitionEnd(event);
break;
}
};
在滑動的整個過程中,我們要做到滑動時子模塊要跟著動,在滑動結(jié)束時判斷是否為一次成功的滑動,成功則滑到下一個或者上一個子項(xiàng)目,非成功則回到滑動前的狀態(tài)。
那么,在滑動開始需要處理的也很簡單,只需要記錄下開始滑動點(diǎn)的x軸和y軸的偏移量:
滑動開始
var touch = e.touches[0];
this.start = {
pageX: touch.pageX,
pageY: touch.pageY
};
//設(shè)置滑動的標(biāo)識(主要從性能上考慮)
this.isScrolling = undefined;
//在每次滑動觸發(fā)的開始重置element的動畫的持續(xù)時間為0
var style = this.element.style;
style.webkitTransitionDuration = style.MozTransitionDuration = style.msTransitionDuration = style.OTransitionDuration = style.transitionDuration = 0 + 'ms';
滑動時
上面說過,滑動時應(yīng)該讓子項(xiàng)目跟著動,怎么做到呢?其實(shí)也很簡單,只要將滑動時的偏移量與上一步驟中初始化量做差值,動態(tài)設(shè)置內(nèi)部容器在x軸上的變換即可:
if(e.touches.length > 1) return;//多指操作不認(rèn)為是swipe操作
var touch = e.touches[0];
this.delta = {
x: touch.pageX - this.start.pageX,
y: touch.pageY - this.start.pageY
};
//x軸的偏移大于y軸偏于才認(rèn)為是正?;瑒?this.isScrolling = Math.abs(this.delta.x) > Math.abs(this.delta.y);
if(this.isScrolling) {
var duration = this.speed;
var style = this.element.style;
style.MozTransform = style.webkitTransform = 'translate3d(' + (this.delta.x - this.index * this.width) + 'px, 0, 0)';
style.msTransform = style.OTransform = 'translateX(' + (this.delta.x - this.index * this.width) + 'px)';
}
這里有個問題是,當(dāng)滑動得非常快速的時候,從性能角度上講是消耗很大的,可以給一個時間限制,超過某個時間間隔的滑動才算正?;瑒?,否則不做處理。
滑動結(jié)束
經(jīng)過上面的滑動后,究竟這次滑動是否成功呢(這里的成功指的是成功滑動到下一個或者上一個),那么就需要在這里計算上面兩個步驟的偏移差值是否符號成功滑動的條件,符合條件則將當(dāng)前的索引index加1或者減1,然后調(diào)用公共滑動方法,滑動到經(jīng)過計算后的index,即達(dá)到目的:
//滑動item寬度的1/6(已經(jīng)足夠靈敏了)即認(rèn)為是有效的滑動到下一個或者上一個了,
//否則依然是當(dāng)前index.
if(this.delta.x > 0 && this.delta.x > this.width / 6) {
this.index -= 1;
if(this.index < 0) this.index = 0;
}
if(this.delta.x < 0 && Math.abs(this.delta.x) > this.width / 6) {
this.index += 1;
if(this.index == this.length) this.index = this.length - 1;
}
if(this.isScrolling) this.slideTo(this.index);
由步驟二滑動時的處理可看到slideTo方法就很簡單了,即變換到內(nèi)部容器的-(index * width)即可:
duration = duration || this.speed;
var gap = index % this.length;
if(index >= this.length) index = gap;
if(index < 0) index = this.length + gap - 1;
var style = this.element.style;
style.webkitTransitionDuration = style.MozTransitionDuration = style.msTransitionDuration = style.OTransitionDuration = style.transitionDuration = duration + 'ms';
style.MozTransform = style.webkitTransform = 'translate3d(' + -(index * this.width) + 'px, 0, 0)';
這里使用index % this.length是為了兼容傳入的index >= length的情況.
動畫結(jié)束
在整個transform動畫結(jié)束后,即可處理我們的回調(diào),反饋整個滑動的結(jié)果:
this.callback && this.callback.call(this,this.index);
至此,一個簡陋但核心功能完整的swiper就實(shí)現(xiàn)了.
不過,上述項(xiàng)目依然存在著如下幾個問題:</br>
(1) 傳進(jìn)去的element必須為dom太不靈活</br>
(2) 沒有paganation功能</br>
(3) 沒有autoplay功能</br>
(4) 使用原生的寫法太麻煩,也不易于多功能的擴(kuò)展</br>
綜上,為了完成(1)、(4)以及擴(kuò)展其它更多功能,最好是避免使用原生那種寫法,轉(zhuǎn)而在內(nèi)部構(gòu)件一套簡版的jquery功能:
然后使用它去重構(gòu)上面的代碼,具體可參看我github上的實(shí)現(xiàn):
https://github.com/kekobin/KSwiper/blob/master/kswiper-0.1.1.js
pagination和autoplay的實(shí)現(xiàn)
有了簡版jquery后,(2)、(3)的實(shí)現(xiàn)就簡單容易多了,這部分完整的代碼參見:
https://github.com/kekobin/KSwiper/blob/master/kswiper-0.1.2.js
pagination
對于(2),這是一個可配置項(xiàng),當(dāng)傳入的參數(shù)中pagination為true時,首先根據(jù)子項(xiàng)目的個數(shù)初始化它:
var $wrap = $("<div class='kswiper-pagination'></div");
var child = '';
for(var i=0;i<this.length;i++) {
var className = i === 0 ? 'active' : "";
i === 0 ? child = "<a class='active' data-index='0'></a>" : child += "<a data-index="+i+"></a>";
}
$wrap.append(child);
this.$container.append($wrap);
從而動態(tài)得到類似如下的結(jié)構(gòu):
<div class="kswiper-pagination">
<a class="active" data-index="0"></a>
<a data-index="1"></a>
<a data-index="2"></a>
<a data-index="3"></a>
</div>
同時,對所有分頁子元素,即上面的a標(biāo)簽添加點(diǎn)擊事件,實(shí)現(xiàn)相關(guān)的邏輯:
if(this.pagination) {
var _this = this;
this.$container.find('.kswiper-pagination>a').on('click', function(e) {
e.preventDefault();
$(this).addClass('active').siblings().removeClass('active');
_this.slideTo($(this).attr('data-index'));
});
}
分頁功能似乎完善了,但等等。。。當(dāng)成功滑動到上一個或者下一個時,我們?nèi)匀恍枰礁路猪撨x中的狀態(tài),也即上面a標(biāo)簽active的狀態(tài),所以在slideTo方法中需要做同步處理:
if(this.pagination) {
this.$container.find('.kswiper-pagination>a.active').removeClass('active');
this.$container.find('.kswiper-pagination>a:nth-child('+(index+1)+')').addClass('active');
}
autoplay
autoplay這個功能就是一個定時滑動而已,沒什么值得深入的問題,直接上代碼:
if(this.auto) {
var autoIndex = 1;
var _this = this;
this.autoTimeout = setInterval(function() {
_this.slideTo(autoIndex++);
}, this.autoDuration);
}
其中,autoDuration是定時滑動的延時時間.
至此,一個趨向完善的swiper就大功告成了!