【iScroll源码学习04】分离IScroll核心
【iScroll源码学习04】分离IScroll核心
前言
最近几天我们前前后后基本将iScroll源码学的七七八八了,文章中未涉及的各位就要自己去看了
- 1. 【iScroll源码学习03】iScroll事件机制与滚动条的实现
- 2. 【iScroll源码学习02】分解iScroll三个核心事件点
- 3. 【iScroll源码学习01】准备阶段
- 4. 【iScroll源码学习00】模拟iScroll
我们学习源码的目的绝不是学习人家的源码,而是由高手的代码里面学习思想,或者研究解决方案,就拿我们这次学习iScroll,我的目的就是
“抄袭”,我今天就针对自己的需求来抄袭iScroll的源码,组成自己的库,然后用于项目,然后高高兴兴的装B.......
关系图
工具类
第一,我们将iScroll的工具类搬离出来,但是我们系统是一定支持CSS3动画的,所以requestAnimationFrame方法我们就不予理睬了
第二,因为我们的系统是依赖于zepto的,甚至还用到了backbone(但是backbone与underscore大有被移除的可能,所以只用zepto就好)
属性总览
_elementStyle
用于后面检测CSS3 兼容属性
_vendor
CSS3 兼容前缀
hasTouch
是否支持touch事件
style
几个CSS3 动画属性,比如在chrome下面是这样的
Object {
transform: "webkitTransform",
transitionTimingFunction: "webkitTransitionTimingFunction",
transitionDuration: "webkitTransitionDuration",
transitionDelay: "webkitTransitionDelay",
transformOrigin: "webkitTransformOrigin"
}
eventType
touch事件的话值就是1,mouse事件便是2,这个对后面有影响的
ease
这个是CSS3的动画参数,会形成动画曲线,各位自己去看吧
_prefixStyle
会用到的私有方法,会返回CSS3兼容前缀
getTime
获取当前时间戳
addEvent
为dom绑定事件,注意其中的fn是对象,具体处理在handleEvent里面
removeEvent
为dom注销事件,注意其中的fn是对象,具体处理在handleEvent里面
momentum
这个对象中最难的一个BUG,也很重要,他会根据我们的拖动返回运动的长度与耗时,这个是根据物理公式计算而出的,十分靠谱,哥是没看懂用什么公式的
样式兼容
于是我们开始吧,首先,因为各个浏览器问题,我们需要做CSS3动画的兼容
1 var _elementStyle = document.createElement('div').style;
2 //获得CSS3前缀
3 var _vendor = (function () {
4 var vendors = ['t', 'webkitT', 'MozT', 'msT', 'OT'];
5 var transform;
6 var i = 0;
7 var l = vendors.length;
8
9 for (; i < l; i++) {
10 transform = vendors[i] + 'ransform';
11 if (transform in _elementStyle) return vendors[i].substr(0, vendors[i].length - 1);
12 }
13 return false;
14 })();
这句代码会返回需要做兼容的CSS属性的前缀,然后提供一个方法特别的干这个事情:
1 //获取样式(CSS3兼容)
2 function _prefixStyle(style) {
3 if (_vendor === false) return false;
4 if (_vendor === '') return style;
5 return _vendor + style.charAt(0).toUpperCase + style.substr(1);
6 }
获取当前时间戳
获得当前时间戳这个方法比较简单,这个会在计算动画用到:
me.getTime = Date.now || function getTime() { return new Date().getTime(); };
动画数据
1 me.momentum = function (current, start, time, lowerMargin, wrapperSize) {
2 var distance = current - start,
3 speed = Math.abs(distance) / time,
4 destination,
5 duration,
6 deceleration = 0.0006;
7
8 destination = current + (speed * speed) / (2 * deceleration) * (distance < 0 ? -1 : 1);
9 duration = speed / deceleration;
10
11 if (destination < lowerMargin) {
12 destination = wrapperSize ? lowerMargin - (wrapperSize / 2.5 * (speed / 8)) : lowerMargin;
13 distance = Math.abs(destination - current);
14 duration = distance / speed;
15 } else if (destination > 0) {
16 destination = wrapperSize ? wrapperSize / 2.5 * (speed / 8) : 0;
17 distance = Math.abs(current) + destination;
18 duration = distance / speed;
19 }
20
21 return {
22 destination: Math.round(destination),
23 duration: duration
24 };
25 };
这个方法尤其关键,是iScroll动画平滑的一大功臣,这个方法内部用到了物理一个计算速度的公式,我可耻的忘了是什么了,这里直接不予关注了,解释下参数即可
current:当前鼠标位置
start:touchStart时候记录的Y(可能是X)的开始位置,但是在touchmove时候可能被重写
time: touchstart到手指离开时候经历的时间,同样可能被touchmove重写
lowerMargin:y可移动的最大距离,这个一般为计算得出 this.wrapperHeight - this.scrollerHeight
wrapperSize:如果有边界距离的话就是可拖动,不然碰到0的时候便停止
这个动画方法,我们后面用到还需要再说下
属性检测
ease时动画平滑度相关的东西,我们暂时用着,后面看看可以用zepto或者CSS3默认的属性以减少代码量,工具类到此为止
IScroll
IScroll类是我们的关键,贯穿其中的是两个事件机制:
① 原生的touch(鼠标)事件
② 系统自建的异步事件模型
起始点是touchstart,其中的动画又有很多开关,我们下面会慢慢涉及到,这里先说下他的简单属性
基本属性
wrapper
基本外壳,这个dom的style属性一般是 position: absolute; overflow: hidden;是我们内部滚动条的外壳
scroller
我们真正滚动的元素,我们拖动的就是他
PS:这里要求必须传入这两个DOM结构,不然就是错
scrollerStyle
scroller 的 Style对象,通过set他的属性改变样式
options
设置的基本参数信息
this.options = {
//是否具有滚动条
scrollbars: true,
// 其实时期Y的位置
startY: 0,
//超出边界还原时间点
bounceTime: 600,
//超出边界返回的动画
bounceEasing: utils.ease.circular,
//超出边界时候是否还能拖动
bounce: true,
//当window触发resize事件60ms后还原
resizePolling: 60,
startX: 0,
startY: 0
};
这里的startY值得关注,我这里其实是想将X相关属性去掉,但是还没有完全去掉,这个后面点重构代码时候搞掉吧
x/y
当前属性的x/y坐标,用于后面touch事件
_events
保存系统中注册的事件,这里我们做个统计,系统一个注册了这些事件:
this.on('scrollEnd', function () {
this.indicator.fade();
});
var scope = this;
this.on('scrollCancel', function () {
scope.indicator.fade();
});
this.on('scrollStart', function () {
scope.indicator.fade(1);
});
this.on('beforeScrollStart', function () {
scope.indicator.fade(1, true);
});
this.on('refresh', function () {
scope.indicator.refresh();
});
① scrollEnd
这个事件在系统中主要用于改变滚动条的透明度
在touchstart时候因为会停止动画所以会触发,但是因为touchstart结束后又会触发move所以滚动条还是会显示
在touchend时候会停止动画,当然也会触发
transitionend事件触发时候,动画真正的停止,scrollEnd也会触发
② scrollCancel
当touchend时候若是认定是一次点击事件便会触发该事件,将滚动条透明度设置为透明
③ scrollStart
当我们手指开始滑动时候,就会触发该事件,将滚动条透明度设置为可见
④ beforeScrollStart
当手指触屏屏幕便会将透明度设置为非透明
这个有一个判断点就是连续拖动才会设置非透明,如果首次触屏一定要拖动才会显示
⑤ refresh
在触发系统refresh方法时候会该事件,这个事件会触发滚动条的refresh事件,改变其位置,以及相关的尺寸(因为滚动条的尺寸根据他而来)
_initEvents
这个为IScroll滚动区域设定了基本的DOM事件,有方向改变和尺寸改变的resize事件
然后注册了停止动画时候触发的事件,以及基础三个事件点touchstart/touchmove/touchend
_start/_move/_end
touch时候执行的方法,这几个方法我们后面详细解析
_resize
当我们初始化时候会计算wrapper与scroller的尺寸,resize时候也会执行,这里不予关注
_transitionTime
该方法会设置运动时间,不传的话运动时间为0 ,即没有动画
getComputedPosition
获得一个DOM的实时样式样式,在touchstart时候保留DOM样式状态十分有用
_initIndicator
该方法用于初始化滚动条(这个方法位置其实该靠前),他会初始化一个滚动条,然后在自己五个事件状态下依次执行滚动条的一个方法
_translate
该方法比较重要
他会根据传入的x,y值设置translate属性,然后就会产生动画,完了调用滚动条的updatePosition方法更新其位置
resetPosition
该方法用于当超出边界时候要还原scroller位置的方法
scrollTo
该方法比较关键,其实是由他调用_translate改变scroller具体位置,其中可以传入一些时间以及动画曲线的参数进行动画
这个时候如果touchstart触屏了话便会停止动画,实现方式是_transitionTime()
disable/enable
统一的开关
on
这个用于注册事件,我们之前做过介绍
_execEvent
触发注册的事件
destroy
销毁注册的DOM事件
_transitionEnd
CSS3动画结束时候会触发的事件,这个里面会将isInTransition设置为false,这是一个动画的重要参数依据,我们后面要详细说
handleEvent
具体事件执行的地方,但是真正的逻辑又分散到了上述的各个方法
至此,IScroll的属性就介绍完毕了,我们下面抽出核心的做介绍
初始化方法
refresh
该方法其实应该属于系统第一步,这个方法会让缓存IScroll中的几个DOM尺寸,动画多是和尺寸打关系嘛
1 refresh: function () {
2 var rf = this.wrapper.offsetHeight; // Force reflow
3
4 this.wrapperHeight = this.wrapper.clientHeight;
5 this.scrollerHeight = this.scroller.offsetHeight;
6 this.maxScrollY = this.wrapperHeight - this.scrollerHeight;
7
8 this.endTime = 0;
9
10 var offset = $(this.wrapper).offset();
11
12 this.wrapperOffset = {
13 left: offset.left * (-1),
14 top: offset.top * (-1)
15 };
16
17 this._execEvent('refresh');
18
19 this.resetPosition();
20
21 },
首先做了一步操作让下面的操作不会发生重绘
var rf = this.wrapper.offsetHeight;
然后初始化了几个关键DOM的高度,并且获得maxScrollY(真正使用的时候会/3)
接下来重置了endtime为0,并且设置一些基本参数后触发refresh事件(会触发滚动条的refresh)
当然,如果超出了边界的话,会resetPosition,重置位置
refresh本身并不复杂,但是对后面的影响比较大,算是初始化第一步的工作
_initIndicator
根据该方法会初始化一个滚动条,当然我们可以配置参数不让滚动条出现
1 _initIndicator: function () {
2 //滚动条
3 var el = createDefaultScrollbar();
4 this.wrapper.appendChild(el);
5 this.indicator = new Indicator(this, { el: el });
6
7 this.on('scrollEnd', function () {
8 this.indicator.fade();
9 });
10
11 var scope = this;
12 this.on('scrollCancel', function () {
13 scope.indicator.fade();
14 });
15
16 this.on('scrollStart', function () {
17 scope.indicator.fade(1);
18 });
19
20 this.on('beforeScrollStart', function () {
21 scope.indicator.fade(1, true);
22 });
23
24 this.on('refresh', function () {
25 scope.indicator.refresh();
26 });
27
28 },
因为滚动条的类,我们后面会详细说,这里便不关注了
_resize
在我们窗口变化时候,或者竖着的屏幕横着时候便会触发该事件,他会执行我们的refresh方法,重置页面信息
PS:其实就动画一块,IScroll逻辑是很严密的
1 _resize: function () {
2 var that = this;
3
4 clearTimeout(this.resizeTimeout);
5
6 this.resizeTimeout = setTimeout(function () {
7 that.refresh();
8 }, this.options.resizePolling);
9 },
resetPosition
1 resetPosition: function (time) {
2 var x = this.x,
3 y = this.y;
4
5 time = time || 0;
6
7 if (this.y > 0) {
8 y = 0;
9 } else if (this.y < this.maxScrollY) {
10 y = this.maxScrollY;
11 }
12
13 if (y == this.y) {
14 return false;
15 }
16
17 this.scrollTo(x, y, time, this.options.bounceEasing);
18
19 return true;
20 },
在refresh时候会触发(因为可能导致scroller超出边界),在touchend时候超出边界也会使用该方法,这个时候重置边界会有动画(600ms)
动画结束如果检测到超出边界也会执行
运动相关
_transitionTime
1 _transitionTime: function (time) {
2 time = time || 0;
3 this.scrollerStyle[utils.style.transitionDuration] = time + 'ms';
4
5 //滚动条,我们这里只会出现一个滚动条就不搞那么复杂了
6 this.indicator && this.indicator.transitionTime(time);
7
8 },
9
10 getComputedPosition: function () {
11 var matrix = window.getComputedStyle(this.scroller, null), x, y;
12
13 matrix = matrix[utils.style.transform].split(')')[0].split(', ');
14 x = +(matrix[12] || matrix[4]);
15 y = +(matrix[13] || matrix[5]);
16
17 return { x: x, y: y };
18 },
该方法用于重置CSS3的时间参数属性,设置为0的话就不会具有动画,有一点需要注意的是,滚动条总是与他是同步的
_transitionTimingFunction
1 _transitionTimingFunction: function (easing) {
2 this.scrollerStyle[utils.style.transitionTimingFunction] = easing;
3
4 this.indicator && this.indicator.transitionTimingFunction(easing);
5 },
该方法用于重置CSS3的时间曲线,这个我没搞懂,滚动条同样是同步的
_translate
1 _translate: function (x, y) {
2
3 this.scrollerStyle[utils.style.transform] = 'translate(' + x + 'px,' + y + 'px)' + this.translateZ;
4
5 this.x = x;
6 this.y = y;
7
8 if (this.options.scrollbars) {
9 this.indicator.updatePosition();
10 }
11
12 },
该方法会改变DOM的样式,但是其动画由CSS3的 transitionDuration 控制,这里改变了scroller样式会同步其滚动条的
scrollTo
该方法是以上三个方法的合集,除了_transitionTime会在外部经常出现以停止动画外,其它方法基本在该方法内部使用
1 scrollTo: function (x, y, time, easing) {
2 easing = easing || utils.ease.circular;
3
4 this.isInTransition = time > 0;
5
6 if (!time || easing.style) {
7 this._transitionTimingFunction(easing.style);
8 this._transitionTime(time);
9 this._translate(x, y);
10 }
11 },
因为滚动条的同步在各个子类方法中了,所以这个方法比较简单了,其逻辑进入了各个子方法
如此整个继承的方法介绍结束,我们开始对三大核心事件进行解析
三大事件核心
说到三大核心就必须将isInTransition单独提出来,他只有0,1两个值,但是是记录DOM是否处于运动的关键
start
1 _start: function (e) {
2 if (!this.enabled || (this.initiated && utils.eventType[e.type] !== this.initiated)) {
3 return;
4 }
5
6 var point = e.touches ? e.touches[0] : e, pos;
7 this.initiated = utils.eventType[e.type];
8
9 this.moved = false;
10
11 this.distY = 0;
12
13 //开启动画时间,如果之前有动画的话,便要停止动画,这里因为没有传时间,所以动画便直接停止了
14 this._transitionTime();
15
16 this.startTime = utils.getTime();
17
18 //如果正在进行动画,需要停止,并且触发滑动结束事件
19 if (this.isInTransition) {
20 this.isInTransition = false;
21 pos = this.getComputedPosition();
22
23 //移动过去
24 this._translate(Math.round(pos.x), Math.round(pos.y));
25 this._execEvent('scrollEnd');
26 }
27
28 this.startX = this.x;
29 this.startY = this.y;
30 this.absStartX = this.x;
31 this.absStartY = this.y;
32 this.pointX = point.pageX;
33 this.pointY = point.pageY;
34
35 this._execEvent('beforeScrollStart');
36
37 e.preventDefault();
38
39 },
在开始阶段,首先将moved属性设置为false,然后停止动画,如果当前isInTransition值为1,需要将他设置为false并且保持当前的状态
PS:这一步是停止动画并且保持当前状态的关键,这里会触发scrollEnd事件,滚动条会有所联动
在做一些初始化操作后,该方法结束,这个里面的核心就是停止动画并且做初始化操作
move
该阶段是第二核心,这里会根据移动获得新的坐标开始移动,如果超出边界会做一个处理,移动的值不会超过1/3个MaxY
一个关键点是会触发scrollStart事件,让滚动条可见
另一个关键点是没过300ms会重置开始状态值,所以就算使劲按着屏幕不放,突然放开,DOM仍然会移动很远
end
end阶段首先会将isInTransition设置为0,如果超出边界会进行处理,进行动画又会将 isInTransition 设置为1,在动画事件结束后设置为0
期间还会处理一些其他情况,我们不予理睬,重点就是使用momentum获取了当前运动参数
然后开始运动,此处逻辑全部分解开了,这里反而显得不难了
至此,整个核心块IScroll便分解结束了,我们最后看看滚动条相关
Indicator
因为我们对滚动条的需求变动简单,我们的滚动条现在就真的很简单了,完全与scroller是一个从属关系,随着scroller而变化
有兴趣的朋友自己去看看吧,我这里就不管他了
源码:
http://sandbox.runjs.cn/show/naryr1vy
结语
我们这次抄袭了IScroll核心代码,形成了一个简单的拖动库,我们对IScroll的学习可能暂时就到这里了,接下来一段时间我们的重心会放到nodeJS上面
中途可能会涉及到requireJS的源码学习,但是都不会偏离nodeJS的核心
注意,请看最后的代码......