临窗旋墨的博客       临窗旋墨的导航

【iScroll源码学习04】分离IScroll核心

【iScroll源码学习04】分离IScroll核心

前言

最近几天我们前前后后基本将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的时候便停止

这个动画方法,我们后面用到还需要再说下

属性检测

View Code

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

View Code

end阶段首先会将isInTransition设置为0,如果超出边界会进行处理,进行动画又会将 isInTransition 设置为1,在动画事件结束后设置为0

期间还会处理一些其他情况,我们不予理睬,重点就是使用momentum获取了当前运动参数
然后开始运动,此处逻辑全部分解开了,这里反而显得不难了

至此,整个核心块IScroll便分解结束了,我们最后看看滚动条相关

Indicator

因为我们对滚动条的需求变动简单,我们的滚动条现在就真的很简单了,完全与scroller是一个从属关系,随着scroller而变化
有兴趣的朋友自己去看看吧,我这里就不管他了

源码:

View Code
View Code

http://sandbox.runjs.cn/show/naryr1vy

结语

我们这次抄袭了IScroll核心代码,形成了一个简单的拖动库,我们对IScroll的学习可能暂时就到这里了,接下来一段时间我们的重心会放到nodeJS上面
中途可能会涉及到requireJS的源码学习,但是都不会偏离nodeJS的核心

注意,请看最后的代码......

posted @ 2015-08-31 15:45  临窗旋墨  阅读(477)  评论(0编辑  收藏  举报