最近项目需要实现一个fixed标题栏的功能,很普通的功能,实现核心也是在sroll事件中切换到fixed状态即可,但是在某些版本ios的某些内核中,在惯性滚动过程中不执行任何js代码,亦即不会触发scroll事件,基本任何事情都做不了,为了解决这个问题不得不使用div内滚动,然后使用iscroll库实现滚动逻辑。
基于使用过程中的一些问题,抱着学习的态度,稍微看了一下源代码,现把学习所得记录如下。
滑动相关组件(如swipe库)的实现基本都是类似的,就是通过3个核心事件:touchstart,touchmove,touchend完成操作。
switch ( e.type ) {
case 'touchstart':
case 'mousedown':
this._start(e);
break;
case 'touchmove':
case 'mousemove':
this._move(e);
break;
case 'touchend':
case 'mouseup':
this._end(e);
break;
}
注:下面的源码只罗列核心部分,而且只展示y轴方向
touchstart需要做的事情有:
function _start(e) {
var point = e.touches ? e.touches[0] : e;
//[1]
//初始化相关数据,一般是开始滑动的位置基点,时间基点
//还有相关的变量
this.moved = false;
this.distY = 0;
this.directionY = 0;
this.startTime = utils.getTime();
this.startY = this.y;
this.pointY = point.pageY;
//[2]
//如果正在滑动中,需要对此做处理,一般策略有:
//1. 在当前滑动状态的基础上,叠加新的滑动状态
//2. 立刻停止当前的滑动,开始新的滑动
//iscroll使用的是方案2
//方案1对于状态处理,滑速计算等方面略偏复杂,但这是更加合理的处理策略(原生的scroll也是这样的)
//这有点类似开车时踩油门的场景,想象一下就清楚了。。。
if ( !this.options.useTransition && this.isAnimating ) {
this.isAnimating = false;
this._execEvent('scrollEnd');
}
}
touchmove需要做的事情有:
function _move(e) {
//[1]
//计算位置和时间,各种增量
var point = e.touches ? e.touches[0] : e,
deltaY = point.pageY - this.pointY,
timestamp = utils.getTime(),
newY, absDistY;
this.pointY = point.pageY;
this.distY += deltaY;
absDistY = Math.abs(this.distY);
//[2]
//判定是否是标准滑动,防止手抖干扰
//干扰有时候是很大的,特别是有惯性滑动逻辑的时候就更甚了,所以这个细节是少不了的
if ( timestamp - this.endTime > 300 && (absDistX < 10 && absDistY < 10) ) {
return;
}
newY = this.y + deltaY;
//[3]
//判断滑动是否超出范围了
//自从ios出现了负向滚动效果之后,各种滑动组件都跟着实现了这种bounce效果
if ( newY > 0 || newY < this.maxScrollY ) {
newY = this.options.bounce ? this.y + deltaY / 3 : newY > 0 ? 0 : this.maxScrollY;
}
//[4]
//触发scrollStart事件
//一个健全的组件肯定有相关的插口,一般都是用事件机制实现的
//这里的细节是,开始事件是要在判定为标准滑动才会触发的,并且只触发一次
//如果考虑不细的话,很容易会在touchstart事件中触发事件
if ( !this.moved ) {
this._execEvent('scrollStart');
}
this.moved = true;
//[5]
//万事俱备,让页面(元素)滑过去吧!
this._translate(newX, newY);
}
touchend需要做的事情有:
function _end(e) {
//[1]
//进行必要的计算
var duration = utils.getTime() - this.startTime,
newY = Math.round(this.y),
distanceY = Math.abs(newY - this.startY);
this.endTime = utils.getTime();
//[2]
//最后的位置也要滑过去
this.scrollTo(newX, newY); // ensures that the last position is rounded
//[3]
//实现惯性滑动
if ( this.options.momentum && duration < 300 ) {
momentumY = this.hasVerticalScroll ? utils.momentum(this.y, this.startY, duration, this.maxScrollY, this.options.bounce ? this.wrapperHeight : 0, this.options.deceleration) : { destination: newY, duration: 0 };
newY = momentumY.destination;
time = Math.max(momentumX.duration, momentumY.duration);
this.isInTransition = 1;
}
if ( newX != this.x || newY != this.y ) {
this.scrollTo(newX, newY, time, easing);
return;
}
//[4]
//触发滑动结束事件
this._execEvent('scrollEnd');
}
基本所有滑动相关的组件所做的事情都是这些,都可以借鉴一二的。
用js处理特殊css的时候,可以先缓存prefix,这样就不用每次都操作所有的内置属性
var _elementStyle = document.createElement('div').style;
var _vendor = (function () {
var vendors = ['t', 'webkitT', 'MozT', 'msT', 'OT'],
transform,
i = 0,
l = vendors.length;
for ( ; i < l; i++ ) {
transform = vendors[i] + 'ransform';
if ( transform in _elementStyle ) return vendors[i].substr(0, vendors[i].length-1);
}
return false;
})();
function _prefixStyle (style) {
if ( _vendor === false ) return false;
if ( _vendor === '' ) return style;
return _vendor + style.charAt(0).toUpperCase() + style.substr(1);
}
addEventListener绑定事件可以传入一个对象而不是一个cb函数,事件触发的时候,就会调用该对象的handleEvent方法来处理事件。例如:
var event = {
handleEvent: function(e) {
switch ( e.type ) {
case 'touchstart':
this._start(e);
break;
case 'touchmove':
this._move(e);
break;
case 'touchend':
this._end(e);
break;
}
},
_start: function() {},
_move: function() {},
_end: function() {}
}
el.addEventListener('touchstart', event);
el.addEventListener('touchmove', event);
el.addEventListener('touchend', event);
这种绑定方式的优点有:
还记得那种绑定事件时bind(this)的日子吗。。。 这种方式也方便了实现事件代理
对于一些触发频率较高的事件,我们通常会控制一下事件处理的频率,例如scroll,resize事件。 另一方面,在实现一个公共组件的时候可以考虑从组件本身来解决这个问题,iScroll通过配置来设置scroll事件的触发频率
//下面代码在_move方法里
//probeType == 1 则300ms才会触发一次scroll
if ( timestamp - this.startTime > 300 ) {
this.startTime = timestamp;
if ( this.options.probeType == 1 ) {
this._execEvent('scroll');
}
}
//probeType > 1 则一直触发
if ( this.options.probeType > 1 ) {
this._execEvent('scroll');
}
下面是针对版本5.1.3的iscroll使用过程中的一些问题
iScroll没有zepto/jquery插件版本,一些基础方法都需要自己实现,导致了库的体积偏大。
通过查看源代码找到了停止滑动的方法,如下:
var iScroll = new IScroll({ /* ... */ });
//直接通过修改iScroll对象的状态来停止滑动
//通过这种方式停止动画是不会触发scrollEnd事件的!
iScroll.isAnimating = false
可以通过scrollTo方法来手动滑动,但是这样的滑动过程是不会触发scroll事件的。
在使用iScroll的过程中遇到不少坑,但使用起来还是比较容易的,文档也比较齐全。 iScroll在实现上也非常成熟,里面许多实现细节都是值得学习的