【iScroll源码学习01】准备阶段

  1. 云栖社区>
  2. 博客>
  3. 正文

【iScroll源码学习01】准备阶段

范大脚脚 2018-01-09 10:58:15 浏览1683
展开阅读全文
前言

我们昨天初步了解了为什么会出现iScroll:【SPA】移动站点APP化研究之上中下页面的iScroll化(上),然后简单的写了一个demo来模拟iScroll,其中了解到了以下知识点:

① viewport相关知识点(device-width等)

② CSS3硬件加速

③ 如何暂停CSS动画

④ e.preventDefault导致文本不能获取焦点解决方案

......

当然,我们写的demo自然不能和iScroll本身的代码比肩,但是demo过程中我们也大概了解了iScroll代码过程中需要注意的一些问题

于是,今天让我们进入iScroll的学习吧

初探iScroll

 View Code
http://sandbox.runjs.cn/show/pscjy3a3


下面是他初始化时候的核心代码:

 1 var myScroll;
 2 function loaded () {
 3     myScroll = new IScroll('#wrapper', {
 4         scrollbars: true,
 5         mouseWheel: true,
 6         interactiveScrollbars: true,
 7         shrinkScrollbars: 'scale',
 8         fadeScrollbars: true
 9     });
10 }
11 document.addEventListener('touchmove', function (e) { e.preventDefault(); }, false);
真不得不说,这个滑动起来感觉挺不错的,第一感觉有几大特点:

① 顺畅

② 连续拖动会加速滑动

③ 没有BUG :)

看看他的构造函数,然后去网上找一点参数说明(Iscroll应用文档):

hScroll: true, //是否水平滚动
vScroll: true, //是否垂直滚动
x: 0, //滚动水平初始位置
y: 0, //滚动垂直初始位置
snap: true, //值可以为true或是DOM元素的tagname,当为true时,对齐的坐标会根据可滚动的位置和滚动区域计算得到可滑动几页,如果为tagname,则滑动会对齐到元素上
bounce: true, //是否超过实际位置反弹
bounceLock: false, //当内容少于滚动是否可以反弹,这个实际用处不大
momentum: true, //动量效果,拖动惯性
lockDirection: true, //当水平滚动和垂直滚动同时生效时,当拖动开始是否锁定另一边的拖动
useTransform: true, //是否使用CSS形变
useTransition: false, //是否使用CSS变换
topOffset: 0, //已经滚动的基准值(一般情况用不到)
checkDOMChanges: false, //是否自动检测内容变化(这个检测不是很准)

//Scrollbar相关参数,通过scrollbar这些参数可以配置iscroll的滚动条,通过scrollbarClass可以自己定义一套滚动条的样式。
hScrollbar: true, //是否显示水平滚动条
vScrollbar: true, //同上垂直滚动条
fixedScrollbar: isAndroid, //对andriod的fixed
hideScrollbar: isIDevice, //是否隐藏滚动条
fadeScrollbar: isIDevice && has3d, //滚动条是否渐隐渐显
scrollbarClass: '', //自定义滚动条的样式名

//Zoom放大相关的参数,通过它,对于一个固定显示图片区域的类似应用,可以非常简单的做到固定滚动,包括两指放大的应用。
zoom: false, //默认是否放大
zoomMin: 1, //放大的最小倍数
zoomMax: 4, //最大倍数
doubleTapZoom: 2, //双触放大几倍
wheelAction: 'scroll', //鼠标滚动行为(还可以是zoom)

//自定义Events相关参数 
onRefresh: null, //refresh 的回调,关于自身何时调用refresh 后面会继续谈到
onBeforeScrollStart: function(e){ e.preventDefault(); }, //开始滚动前的时间回调,默认是阻止浏览器默认行为
onScrollStart: null, //开始滚动的回调
onBeforeScrollMove: null, //在内容移动前的回调
onScrollMove: null, //内容移动的回调
onBeforeScrollEnd: null, //在滚动结束前的回调
onScrollEnd: null, //在滚动完成后的回调
onTouchEnd: null, //手离开屏幕后的回调
onDestroy: null, //销毁实例的回调
onZoomStart: null, //开始放大前的回调
onZoom: null, //放大的回调
onZoomEnd: null //放大完成后的回调
Iscroll 提供的调用方法

destroy 
顾名思义,是用来销毁你实例化的iScroll 实例,包括之前绑定的所有iscroll 事件。 

refresh 
这个方法非常有用,当你的滚动区域的内容发生改变 或是 滚动区域不正确,都用通过调用refresh 来使得iscroll 重新计算滚动的区域,包括滚动条,来使得iscroll 适合当前的dom。 

scrollTo 
这个方法接受4个参数 x, y, time, relative x 为移动的x轴坐标,y为移动的y轴坐标, time为移动时间,relative表示是否相对当前位置。 

scrollToElement 
这个方法实际上是对scrollTo的进一步封装,接受两个参数(el,time),el为需要滚动到的元素引用,time为滚动时间。 

scrollToPage 
此方法接受三个参数(pageX,pageY,time) 当滚动内容的高宽大于滚动范围时,iscroll 会自动分页,然后就能使用scrollToPage方法滚动到页面。当然,当hscroll 为false 的时候,不能左右滚动。pageX这个参数就失去效果 

disable 
调用这个方法会立即停止动画滚动,并且把滚动位置还原成0,取消绑定touchmove, touchend、touchcancel事件。 

enable 
调用这个方法,使得iscroll恢复默认正常状态 

stop 
立即停止动画 

zoom 
改变内容的大小倍数,此方法接受4个参数,x,y,scale,time 分别表示的意思为,放大的基准坐标,以及放大倍数,动画时间 

isReady 
当iscroll 没有处于正在滚动,没有移动过,没有改变大小时,此值为true
功能非常丰富啊,对于应用来说够用了,但是一些功能我这里用不到,就忽略了

功能很好,size为48k,压缩后稍微好一点,将近2000行的代码,作为基础库来说,有点大了,比整个zepto还大

而且整个库的注释写的不好,好像压根就没写......不知道阅读上会不会有障碍,于是我们进入源码

iScroll笔记

requestAnimationFrame

1 var rAF = window.requestAnimationFrame    ||
2     window.webkitRequestAnimationFrame    ||
3     window.mozRequestAnimationFrame        ||
4     window.oRequestAnimationFrame        ||
5     window.msRequestAnimationFrame        ||
6     function (callback) { window.setTimeout(callback, 1000 / 60); };
这段代码是要做能力检测的,这里来说一下requestAnimationFrame这个东西(参考:http://www.kimhou.com/?p=155)

在jquery中javascript动画是通过定时器(settimeout)实现的,没一个时间点改变一点style,而CSS3后便推出了transition以及animation开始实现动画

我们昨天提到的硬件加速,也是CSS3相关的东西。CSS3动画效率与顺畅度比Js高,所以现在动画开始楚河汉界了

js的好处是可以很好的控制动画状态、css动画带来的性能较高,但是控制度就低一点(是很低)

定时器实现动画

 1 <html xmlns="http://www.w3.org/1999/xhtml">
 2 <head>
 3   <title></title>
 4 </head>
 5 <body>
 6   <div id="el" style="position: absolute;">
 7     Javascript时钟实现动画
 8   </div>
 9   <script src="zepto.js" type="text/javascript"></script>
10   <script type="text/javascript">
11     var pos = 0;
12     var final = 200;
13     var dir = 0;
14     var el = $('#el');
15     function upLeft() {
16       var left = parseInt(el.css('left')) || 0;
17       if (left >= final) dir = 1;
18       if (left <= pos) dir = 0;
19 
20       if (dir == 0) {
21         left++;
22         el.css('left', left + 'px');
23         setTimeout(upLeft);
24       } else {
25         left--;
26         el.css('left', left + 'px');
27         setTimeout(upLeft);
28       }
29     }
30     upLeft();
31   </script>
32 </body>
33 </html>
效果见下面

这便是使用javascript实现的一个最简单的动画,各位看到了,里面的定时器不停的在运动,性能不差的话我就改名叫素还真了

这个阶段,比较棘手的问题往往在延迟的计算,间隔要短所以动画顺畅,但是浏览器渲染也得耗费时间,这个就要求每次变化留给浏览器的时间够长了(60HZ/75Hz)

所以之前javascript的间隔一般为20左右,这个时候的动画比较流畅,这个数字与浏览器的频率比较接近(1000/60)

function (callback) { window.setTimeout(callback, 1000 / 60); }
但是通过前面对时钟的学习,我们知道settimeout只是将回调函数加入UI线程队列,那么同一时间有多个动画待执行的话,延迟就发生了,效果也会打折扣

这里原来用js实现坦克大战的朋友就会有所体会了

javascript问题解决

CSS的transition与animations的优势在于浏览器知道哪些动画将会发生,所以动画会得到正确的间隔来刷新UI(javascript当然不知道)

于是这里就多了一个方法:RequestAnimationFrame,他可以告诉浏览器自己要执行动画了,于是js的动画事实上得到了优化

RequestAnimationFrame接受一个参数,也就是屏幕重绘前会调用的函数,这个函数用来改变dom样式,这个方法使用有点类似于settimeout

 1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 2 <html xmlns="http://www.w3.org/1999/xhtml">
 3 <head>
 4   <title></title>
 5 </head>
 6 <body>
 7 
 8   <div id="el" style="position: absolute;">
 9     Javascript时钟实现动画
10   </div>
11   <script src="zepto.js" type="text/javascript"></script>
12   <script type="text/javascript">
13     var rAF = window.requestAnimationFrame ||
14     window.webkitRequestAnimationFrame ||
15     window.mozRequestAnimationFrame ||
16     window.oRequestAnimationFrame ||
17     window.msRequestAnimationFrame ||
18     function (callback) { window.setTimeout(callback, 1000 / 60); };
19     var pos = 0;
20     var final = 200;
21     var dir = 0;
22     var el = $('#el');
23     function upLeft() {
24       var left = parseInt(el.css('left')) || 0;
25       if (left >= final) dir = 1;
26       if (left <= pos) dir = 0;
27 
28       if (dir == 0) {
29         left++;
30         el.css('left', left + 'px');
31         rAF(upLeft);
32       } else {
33         left--;
34         el.css('left', left + 'px');
35         rAF(upLeft);
36       }
37     }
38     upLeft();
39   </script>
40 </body>
41 </html>
这个动画将会有不一样的感受:


动画效果相关,各位自己去感受,代码各位可以自己调整。如此这个方法其实就是做javascript动画处理优化方案的

PS:尼玛,iScroll第一段就搞了这么久啊这么久

utils

然后iScroll将自己下面会用到的常用操作封装到了这个对象中——utils。

 View Code
兼容性检测

很烦的事情一而再再而三的在浏览器上面出现,比如我们在chrome要定义动画参数得加上一个前缀webkit,然后ff要使用MozT,这个事情很烦,所以iScroll这段代码就在处理这个事情

 1 var _vendor = (function () {
 2     var vendors = ['t', 'webkitT', 'MozT', 'msT', 'OT'],
 3         transform,
 4         i = 0,
 5         l = vendors.length;
 6 
 7     for ( ; i < l; i++ ) {
 8         transform = vendors[i] + 'ransform';
 9         if ( transform in _elementStyle ) return vendors[i].substr(0, vendors[i].length-1);
10     }
11 
12     return false;
13 })();
14 
15 function _prefixStyle (style) {
16     if ( _vendor === false ) return false;
17     if ( _vendor === '' ) return style;
18     return _vendor + style.charAt(0).toUpperCase() + style.substr(1);
19 }
这里做了几个操作:

① 动态创建标签样式——_elementStyle

② 检测样式支持度,并且返回需要的前缀

③ 获取验证结果,比如在chrome下变会返回webkit-XXX



当然,这里要加前缀的样式,一般都与CSS3有关,而下面就会遇到是transform

getTime

me.getTime = Date.now || function getTime () { return new Date().getTime(); };
//获取当前时间戳
extend

最最简单的扩展对象的方法

1 me.extend = function (target, obj) {
2     for ( var i in obj ) {
3         target[i] = obj[i];
4     }
5 };
addEvent/removeEvent

事件注册相关,我在想,若是使用了zepto这个代码量会减少点么?

1 me.addEvent = function (el, type, fn, capture) {
2     el.addEventListener(type, fn, !!capture);
3 };
5 me.removeEvent = function (el, type, fn, capture) {
6     el.removeEventListener(type, fn, !!capture);
7 };
momentum

这个方法比较重要,用于计算动画参数,会根据这个计算结果而决定动画运动效果,其实我们昨天的demo也用到了类似的东西

 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 };
我们来做一点解释,首先看看我们的参数:

① 这个方法应该是在touchend时候使用,第一个参数为当前鼠标的位置

② 第二个参数是touchstart时候记录的坐标位置

③ 第三个参数为时间参数,便是开始触屏到离开时候所用时间(touchstart到touchend)

PS:这里我们其实可以做一个猜测了,我们有一次触屏的时间与距离,自然可以根据动力加速度计算出此次应该运动的时间与距离

④ 第四个参数是干神马的还不太明确,应该是控制边界位置的,这个就决定了我们不能无限制的拖动wrapper

⑤ 第五个参数为容器的高度

然后我们来以此读一读这里的代码:

① 得出此次拉动的距离distance/然后计算出这次拖动的速度(PS:个人觉得这里操作很不错,我没有想到)

② 然后定义了一些其它参数,deceleration这个用于计算速度/距离的参数,然后两个就是要返回的距离以及时间了

PS:我想说他这里的计算最终位置的函数应该是物理里面的一个计算摩擦参数的公式,尼玛是什么我真的不知道了,还有平方来着......

③ 这里还有一个关键点就是distance有可能是负值,这个会决定向上还是向下运动

④ 一般情况这里就结束来了,然后下面if里面一大段计算是用于处理运动轨迹超出时候的距离与速度重新计算(反弹效果)

好了,这个函数比较关键,他主要返回了最后要去到的位置,已经到这个位置的时间,里面具体的实现我们暂时不关系,后面这个必须理一理

检测与初始化

接下来做了一大段能力检测,比如:

① 是否支持CSS3动画相关(transform、transition)

② 是否支持touch事件

然后做了一些简单的初始化操作,这里新增了一个style对象,为他赋予了CSS动画相关的属性

③ 接下来是一些简单的样式操作,这样有个函数需要注意,他可以获取一个元素真正的位置信息

 1 me.offset = function (el) {
 2     var left = -el.offsetLeft,
 3         top = -el.offsetTop;
 4 
 5     // jshint -W084
 6     while (el = el.offsetParent) {
 7         left -= el.offsetLeft;
 8         top -= el.offsetTop;
 9     }
10     // jshint +W084
11 
12     return {
13         left: left,
14         top: top
15     };
16 };
④ 动画曲线

 View Code
这里定义了动画曲线供我们选取,我一般使用linear......

PS:这个地方的代码,不明觉厉!!!

自定义点击事件

 1 me.tap = function (e, eventName) {
 2     var ev = document.createEvent('Event');
 3     ev.initEvent(eventName, true, true);
 4     ev.pageX = e.pageX;
 5     ev.pageY = e.pageY;
 6     e.target.dispatchEvent(ev);
 7 };
 8 
 9 me.click = function (e) {
10     var target = e.target,
11         ev;
12 
13     if (target.tagName != 'SELECT' && target.tagName != 'INPUT' && target.tagName != 'TEXTAREA') {
14         ev = document.createEvent('MouseEvents');
15         ev.initMouseEvent('click', true, true, e.view, 1,
16             target.screenX, target.screenY, target.clientX, target.clientY,
17             e.ctrlKey, e.altKey, e.shiftKey, e.metaKey,
18             0, null);
19 
20         ev._constructed = true;
21         target.dispatchEvent(ev);
22     }
23 };
iScroll这里干了一件坏事,自己定义了tap以及click,意思是他可以触发绑定到dom上的tap或者click事件

然后整个util便结束了,其中momentum方法很是关键,接下来我们跟着程序流程走了

构造函数

构造函数是iScroll的入口,我们来详细读一读:

wrapper/scroller

为我们的外层结构,再里面一点就是拖动元素了,iscroll的处理是认为wrapper下第一个元素就是可拖动元素,我这里任务不妥......



 

this.scroller = this.wrapper.children[0];
我们拖动的就是这个scroller了,我为什么说这样不妥呢?因为我如果现在又一个弹出层想使用iScroll的话,若是我弹出层有了wrapper了,我想自己往里面装DOM

而我的DOM搞不好有几个兄弟节点,这个时候我肯定不想自己再包裹一层的,所以iScroll这里的scroller我觉得系统构建比较合理(当然这只是个人认为)

下面还缓存了下当前scroll元素的style

this.scrollerStyle = this.scroller.style;    
初始化参数

iScroll当然自己会初始化一些默认属性了:

 1 this.options = {
 2 
 3     resizeIndicator: true,
 4 
 5     mouseWheelSpeed: 20,
 6 
 7     snapThreshold: 0.334,
 8 
 9 // INSERT POINT: OPTIONS 
10 
11     startX: 0,
12     startY: 0,
13     scrollY: true,
14     directionLockThreshold: 5,
15     momentum: true,
16 
17     bounce: true,
18     bounceTime: 600,
19     bounceEasing: '',
20 
21     preventDefault: true,
22     preventDefaultException: { tagName: /^(INPUT|TEXTAREA|BUTTON|SELECT)$/ },
23 
24     HWCompositing: true,
25     useTransition: true,
26     useTransform: true
27 };
若是我们传了相关属性会被复写的(这里上面定义了extend,却没有使用):

1 for ( var i in options ) {
2     this.options[i] = options[i];
3 }
能力检测

然后下面一大片结果基本做了一些能力检测的功能,然后将检测结果保存,因为我暂时只关心纵向滑动,所以一些地方便不予关心了

1 this.x = 0;
2 this.y = 0;
3 this.directionX = 0;
4 this.directionY = 0;
5 this._events = {};
这一坨东西还是要关注的,方向和初始值

初始化_init

上面一些默认属性定义结束便进入真正的初始化阶段,

 1 _init: function () {
 2     this._initEvents();
 3 
 4     if ( this.options.scrollbars || this.options.indicators ) {
 5         this._initIndicators();
 6     }
 7 
 8     if ( this.options.mouseWheel ) {
 9         this._initWheel();
10     }
11 
12     if ( this.options.snap ) {
13         this._initSnap();
14     }
15 
16     if ( this.options.keyBindings ) {
17         this._initKeys();
18     }
19 // INSERT POINT: _init
20 },
代码很清晰,我现在的需求关注_initEvents与_initIndicators就好了,其它暂时可以不管,关键点便是事件绑定了

 

_initEvents

 1 _initEvents: function (remove) {
 2     var eventType = remove ? utils.removeEvent : utils.addEvent,
 3         target = this.options.bindToWrapper ? this.wrapper : window;
 4 
 5     eventType(window, 'orientationchange', this);
 6     eventType(window, 'resize', this);
 7 
 8     if ( this.options.click ) {
 9         eventType(this.wrapper, 'click', this, true);
10     }
11 
12     if ( !this.options.disableMouse ) {
13         eventType(this.wrapper, 'mousedown', this);
14         eventType(target, 'mousemove', this);
15         eventType(target, 'mousecancel', this);
16         eventType(target, 'mouseup', this);
17     }
18 
19     if ( utils.hasPointer && !this.options.disablePointer ) {
20         eventType(this.wrapper, 'MSPointerDown', this);
21         eventType(target, 'MSPointerMove', this);
22         eventType(target, 'MSPointerCancel', this);
23         eventType(target, 'MSPointerUp', this);
24     }
25 
26     if ( utils.hasTouch && !this.options.disableTouch ) {
27         eventType(this.wrapper, 'touchstart', this);
28         eventType(target, 'touchmove', this);
29         eventType(target, 'touchcancel', this);
30         eventType(target, 'touchend', this);
31     }
32 
33     eventType(this.scroller, 'transitionend', this);
34     eventType(this.scroller, 'webkitTransitionEnd', this);
35     eventType(this.scroller, 'oTransitionEnd', this);
36     eventType(this.scroller, 'MSTransitionEnd', this);
37 },
这段代码,是整个iScroll的核心,整个入口函数其实在这里,我们暂时的关注点又在这里:

1 if ( utils.hasTouch && !this.options.disableTouch ) {
2     eventType(this.wrapper, 'touchstart', this);
3     eventType(target, 'touchmove', this);
4     eventType(target, 'touchcancel', this);
5     eventType(target, 'touchend', this);
6 }
PS:这里有一点让我比较疑惑的就是这里传递进去的fn是一个对象,而不是函数,看来我事件机制一块仍然不到家:



然后进入我们的touch事件,反正现在touchstart便会进入我们的start回调函数

touchStart

 1 _start: function (e) {
 2     // React to left mouse button only
 3     if ( utils.eventType[e.type] != 1 ) {
 4         if ( e.button !== 0 ) {
 5             return;
 6         }
 7     }
 8 
 9     if ( !this.enabled || (this.initiated && utils.eventType[e.type] !== this.initiated) ) {
10         return;
11     }
12 
13     if ( this.options.preventDefault && !utils.isBadAndroid && !utils.preventDefaultException(e.target, this.options.preventDefaultException) ) {
14         e.preventDefault();
15     }
16 
17     var point = e.touches ? e.touches[0] : e,
18         pos;
19 
20     this.initiated    = utils.eventType[e.type];
21     this.moved        = false;
22     this.distX        = 0;
23     this.distY        = 0;
24     this.directionX = 0;
25     this.directionY = 0;
26     this.directionLocked = 0;
27 
28     this._transitionTime();
29 
30     this.startTime = utils.getTime();
31 
32     if ( this.options.useTransition && this.isInTransition ) {
33         this.isInTransition = false;
34         pos = this.getComputedPosition();
35         this._translate(Math.round(pos.x), Math.round(pos.y));
36         this._execEvent('scrollEnd');
37     } else if ( !this.options.useTransition && this.isAnimating ) {
38         this.isAnimating = false;
39         this._execEvent('scrollEnd');
40     }
41 
42     this.startX    = this.x;
43     this.startY    = this.y;
44     this.absStartX = this.x;
45     this.absStartY = this.y;
46     this.pointX    = point.pageX;
47     this.pointY    = point.pageY;
48 
49     this._execEvent('beforeScrollStart');
50 }
前面做了一系列的兼容性处理,然后记录了一些数据便结束了

结语

今天有点晚了,我也暂时结束了,明天还要上班呢,下次详细研究下touch事件的几个阶段干的事情以及滚动条的实现

您可以考虑给小钗发个小额微信红包以资鼓励 





本文转自叶小钗博客园博客,原文链接:http://www.cnblogs.com/yexiaochai/p/3496369.html,如需转载请自行联系原作者

网友评论

登录后评论
0/500
评论
范大脚脚
+ 关注