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源码