【iScroll源码学习03】iScroll事件机制与滚动条的实现

前言

想不到又到周末了,周末的时间要抓紧学习才行,前几天我们学习了iScroll几点基础知识:

今天我们来学习其事件机制以及滚动条的实现,完了后我们iScroll就学习的差不多了,最后会抽离iScroll的精华部分组成一个阉割版iScroll

并总结下iScroll的一些地方结束iScroll的学习,然后彻底扑向nodeJS了iScroll事件机制

我们平时所说的事件机制其实应该分开,分成两块:

①DOM的事件相关

②系统自建事件机制

在我们前端的页面里面,最重要的当然是交互,交互其实就是一个个事件的体现,所以任何前端库的核心一定是其事件,iScroll就是由三大事件串联整个流程iScroll同样包括DOM事件以及自建事件,其中DOM事件便是浏览器的表现,而自建事件就是用户可以插一脚的地方了

DOM事件参数盲点

iScroll DOM事件实现与可能让一些不熟悉javascript事件机制的同学大跌眼镜(在与Aaron讨论前,我其实也摸不着头脑)

简单来说,标准情况下我们这样实现事件注册

el.addEventListener(type, fn, capture)

其中的所有参数都没有问题,唯独第二个参数,为什么这么说呢?请看以下代码:1var eventObj = {};

2 eventObj.a = 1;

3 document.addEventListener('click', eventObj)

各位觉得这个代码有问题吗?第二个参数显然不是一个函数,但是function也是object呢,https://www.360docs.net/doc/ad13456104.html,其实这样也是javascript规范之一,不知道只是我们寡闻而已

这样写有以下好处,我们的作用域就是我们的对象:

var eventObj = {};

eventObj.a = 1;

eventObj.handleEvent = function () {

alert(this.a);

}

document.addEventListener('click', eventObj)

//这个代码点击会弹出1

这个便是一个javascript规范,我们传入的对象如果具有handleEvent 函数,便会执行,如果没有,此次注册便无意义,这样绑定的话,作用域便指向了eventObj

iScroll DOM 事件

有了以上知识,再说回iScroll的DOM事件:

①构造函数会执行_initEvents方法初始化事件,我们抽出我们关心的一块:

1 eventType(this.wrapper, 'touchstart', this);

2 eventType(target, 'touchmove', this);

3 eventType(target, 'touchcancel', this);

4 eventType(target, 'touchend', this);

var eventType = remove ? utils.removeEvent : utils.addEvent

这个代码其实就是调用的addEvent方法:

1 me.addEvent = function (el, type, fn, capture) {

2el.addEventListener(type, fn, !!capture);

3 };

那么iScroll事件绑定的具体点便捕捉到了:

可以看到我们这里的fn是一个对象,但是不要担心,我们的具体的方法在此:

1 handleEvent: function (e) {

2switch (e.type) {

3case 'touchstart':

4case 'MSPointerDown':

https://www.360docs.net/doc/ad13456104.html,

5case 'mousedown':

6this._start(e);

7break;

8case 'touchmove':

9case 'MSPointerMove':

10case 'mousemove':

11this._move(e);

12break;

13case 'touchend':

14case 'MSPointerUp':

15case 'mouseup':

16case 'touchcancel':

17case 'MSPointerCancel':

18case 'mousecancel':

19this._end(e);

20break;

21case 'orientationchange':

22case 'resize':

23this._resize();

24break;

25case 'transitionend':

26case 'webkitTransitionEnd':

27case 'oTransitionEnd':

28case 'MSTransitionEnd':

29this._transitionEnd(e);

30break;

31case 'wheel':

32case 'DOMMouseScroll':

33case 'mousewheel':

34this._wheel(e);

35break;

36case 'keydown':

37this._key(e);

38break;

39case 'click':

40if (!e._constructed) {

41 e.preventDefault();

42 e.stopPropagation();

43}

44break;

45}

46 }

高大帅哈,如此整个iScroll的DOM事件相关就没问题了,在具体就回到了上次的三大事件点了

自定义事件机制

其实在我们学习backbone时候我们就提到了这块操作

1var Events = Backbone.Events = {

2

3on: function (name, callback, context) {

4if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this; 5this._events || (this._events = {});

6var events = this._events[name] || (this._events[name] = []);

7events.push({ callback: callback, context: context, ctx: context || this }); 8return this;

9},

10

11off: function (name, callback, context) {

12var retain, ev, events, names, i, l, j, k;

13if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this;

14if (!name && !callback && !context) {

15this._events = {};

16return this;

17}

18

19names = name ? [name] : _.keys(this._events);

20for (i = 0, l = names.length; i < l; i++) {

21name = names[i];

22if (events = this._events[name]) {

23this._events[name] = retain = [];

24if (callback || context) {

25for (j = 0, k = events.length; j < k; j++) {

26ev = events[j];

27if ((callback && callback !== ev.callback && callback !== ev.callback._callback) ||

28(context && context !== ev.context)) {

29retain.push(ev);

30}

31}

32}

33if (!retain.length) delete this._events[name];

34}

35}

36

37return this;

38},

39

40trigger: function (name) {

41if (!this._events) return this;

42var args = slice.call(arguments, 1);

43if (!eventsApi(this, 'trigger', name, args)) return this;

44var events = this._events[name];

45var allEvents = this._events.all;

46if (events) triggerEvents(events, args);

47if (allEvents) triggerEvents(allEvents, arguments);

48return this;

49},

50 };

51

52// Regular expression used to split event strings.

53var eventSplitter = /\s+/;

54

55// Implement fancy features of the Events API such as multiple event

56// names `"change blur"` and jQuery-style event maps `{change: action}` 57// in terms of the existing API.

58var eventsApi = function (obj, action, name, rest) {

59if (!name) return true;

60

61// Handle event maps.

62if (typeof name === 'object') {

63for (var key in name) {

64obj[action].apply(obj, [key, name[key]].concat(rest));

65}

66return false;

67}

68

69// Handle space separated event names.

70if (eventSplitter.test(name)) {

71var names = name.split(eventSplitter);

72for (var i = 0, l = names.length; i < l; i++) {

73obj[action].apply(obj, [names[i]].concat(rest));

74}

75return false;

76}

77

78return true;

79 };

所谓自建事件机制,其实是唬人的,就是用一个数组保存各个阶段的函数,到特定阶段执行便可,iScroll这块做的尤其简单,而且又注册没有注销:

1 on: function (type, fn) {

2if (!this._events[type]) {

3this._events[type] = [];

4}

5

6this._events[type].push(fn);

7 },

8

9 _execEvent: function (type) {

10if (!this._events[type]) {

11return;

12}

13

14var i = 0,

15l = this._events[type].length;

16

17if (!l) {

18return;

19}

20

21for (; i < l; i++) {

22this._events[type][i].call(this);

23}

24 },

iScroll在构造函数中定义了_events这一对象,然后便可以开开心心使用on注册各种各样的事件了,其中每种事件对象是一个数组

定义好好,在特定阶段,比如touchstart阶段,便开开有没有注册相关事件,注册了便执行一发即可:

this._execEvent('scrollEnd');

这里要注意的是,他的this执行为iScroll,那么就可以使用很多有用的属性了至此,iScroll事件机制一块我们便分析结束了,接下来来简单看看我们关心的滚动条的实现

这里需要注意的一点是,这种实现的好处其实一个是方便在各个阶段注册、触发相关事件,主要缘由还是便于放出接口给外部调用

滚动条的实现

其实到这里,我们队iScroll的解析都七七八八了,这里我不得不说,iScroll虽然动画感受做的好以外,还是可能导致一些问题

iScroll本身没什么问题,问题出在各种各样的浏览器中,据我读代码的感受以及平时工作中遇到的问题,我相信项目中使用iScroll的朋友有可能

当然,这些问题出现在手机中:

①当滑动碰到input可能出现滑动不顺的问题

②滑动时候具有input时候滑动顺畅的话,input获取焦点不易

③点击时候可能出现问题(可能不能点击,可能双次点击)

④当你在ios点击时候碰到alert类似的东西,再点其它地方事件可能会重复触发

⑤ ......

当然以上问题只是我的猜测,是否真会导致问题还得经过验证,请各位不要搭理我,如果真有类似问题,获取其它问题请留言

上面扯了那么多也没有什么意义,我们现在还是来看滚动条的实现吧:

滚动条

iScroll为滚动条单独搞了一个类出来,因为在iScroll里面的滚动条是一等公民,具有以下特性:

①鼠标中间滚动

②可拖动滚动条

其实,多数时间以上功能可以取缔,尤其在手机上,其可点击区域还是过小,单独用于手机的话,鼠标中间也无意义

PS:iscroll使用键盘上下键也可以滚动,真的是大而全的功能啊,但是无意义......至少在移动端意义不大,去掉还可以节约1k的流量

1function Indicator(scroller, options) {

2this.wrapper = typeof options.el == 'string' ?

document.querySelector(options.el) : options.el;

3this.wrapperStyle = this.wrapper.style;

4this.indicator = this.wrapper.children[0];

5this.indicatorStyle = this.indicator.style;

6this.scroller = scroller;

7

8this.options = {

9listenX: true,

10listenY: true,

11interactive: false,

12resize: true,

13defaultScrollbars: false,

14shrink: false,

15fade: false,

16speedRatioX: 0,

17speedRatioY: 0

18};

19

20for (var i in options) {

21this.options[i] = options[i];

22}

23

24this.sizeRatioX = 1;

25this.sizeRatioY = 1;

26this.maxPosX = 0;

27this.maxPosY = 0;

28

29if (this.options.interactive) {

30if (!this.options.disableTouch) {

31utils.addEvent(this.indicator, 'touchstart', this);

32utils.addEvent(window, 'touchend', this);

33}

34if (!this.options.disablePointer) {

35utils.addEvent(this.indicator, 'MSPointerDown', this);

36utils.addEvent(window, 'MSPointerUp', this);

37}

38if (!this.options.disableMouse) {

39utils.addEvent(this.indicator, 'mousedown', this);

40utils.addEvent(window, 'mouseup', this);

41}

42}

43

44if (this.options.fade) {

45this.wrapperStyle[utils.style.transform] = this.scroller.translateZ;

46this.wrapperStyle[utils.style.transitionDuration] = utils.isBadAndroid ? '0.001s' : '0ms';

47this.wrapperStyle.opacity = '0';

48}

49 }

设个就是滚动条的构造函数,这有一个关键点:

interactiveScrollbars: true

默认我们的滚动条是不会具有滚动等事件的,如果设置了此参数便具有可拖动特性了

1if (this.options.interactive) {

2if (!this.options.disableTouch) {

3utils.addEvent(this.indicator, 'touchstart', this);

4utils.addEvent(window, 'touchend', this);

5}

6if (!this.options.disablePointer) {

7utils.addEvent(this.indicator, 'MSPointerDown', this);

8utils.addEvent(window, 'MSPointerUp', this);

9}

10if (!this.options.disableMouse) {

11utils.addEvent(this.indicator, 'mousedown', this);

12utils.addEvent(window, 'mouseup', this);

13}

14 }

这里虽然给滚动条绑定的事件,但是会一并操作我们的body主体,但是我们后面会直接忽略这步操作

1if (this.options.fade) {

2this.wrapperStyle[utils.style.transform] = this.scroller.translateZ;

3this.wrapperStyle[utils.style.transitionDuration] = utils.isBadAndroid ? '0.001s' : '0ms';

4this.wrapperStyle.opacity = '0';

5 }

然后,会给滚动条一个渐隐的效果,这个影响较小,直接使用了CSS3实现

下面继续实现了他的事件handleEvent

1 handleEvent: function (e) {

2switch (e.type) {

3case 'touchstart':

4case 'MSPointerDown':

5case 'mousedown':

6this._start(e);

7break;

8case 'touchmove':

9case 'MSPointerMove':

10case 'mousemove':

11this._move(e);

12break;

13case 'touchend':

14case 'MSPointerUp':

15case 'mouseup':

16case 'touchcancel':

17case 'MSPointerCancel':

18case 'mousecancel':

19this._end(e);

20break;

21}

22 }

接下来又是touch几个事件了:

1 _start: function (e) {

2var point = e.touches ? e.touches[0] : e;

3

4 e.preventDefault();

5 e.stopPropagation();

6

7this.transitionTime();

8

9this.initiated = true;

10this.moved = false;

https://www.360docs.net/doc/ad13456104.html,stPointX = point.pageX;

https://www.360docs.net/doc/ad13456104.html,stPointY = point.pageY;

13

14this.startTime = utils.getTime();

15

16if (!this.options.disableTouch) {

17utils.addEvent(window, 'touchmove', this);

18}

19if (!this.options.disablePointer) {

20utils.addEvent(window, 'MSPointerMove', this); 21}

22if (!this.options.disableMouse) {

23utils.addEvent(window, 'mousemove', this); 24}

25

26this.scroller._execEvent('beforeScrollStart');

27 },

28

29 _move: function (e) {

30var point = e.touches ? e.touches[0] : e,

31deltaX, deltaY,

32newX, newY,

33timestamp = utils.getTime();

34

35if (!this.moved) {

36this.scroller._execEvent('scrollStart');

37}

38

39this.moved = true;

40

41deltaX = point.pageX - https://www.360docs.net/doc/ad13456104.html,stPointX;

https://www.360docs.net/doc/ad13456104.html,stPointX = point.pageX;

43

44deltaY = point.pageY - https://www.360docs.net/doc/ad13456104.html,stPointY;

https://www.360docs.net/doc/ad13456104.html,stPointY = point.pageY;

46

47newX = this.x + deltaX;

48newY = this.y + deltaY;

49

50this._pos(newX, newY);

51

52// INSERT POINT: indicator._move

53

54 e.preventDefault();

55 e.stopPropagation();

56 },

57

58 _end: function (e) {

59if (!this.initiated) {

60return;

61}

62

63this.initiated = false;

64

65 e.preventDefault();

66 e.stopPropagation();

67

68utils.removeEvent(window, 'touchmove', this);

69utils.removeEvent(window, 'MSPointerMove', this);

70utils.removeEvent(window, 'mousemove', this);

71

72if (this.scroller.options.snap) {

73var snap = this.scroller._nearestSnap(this.scroller.x, this.scroller.y); 74

75var time = this.options.snapSpeed || Math.max(

76Math.max(

77Math.min(Math.abs(this.scroller.x - snap.x), 1000), 78Math.min(Math.abs(this.scroller.y - snap.y), 1000) 79), 300);

80

81if (this.scroller.x != snap.x || this.scroller.y != snap.y) {

82this.scroller.directionX = 0;

83this.scroller.directionY = 0;

84this.scroller.currentPage = snap;

85this.scroller.scrollTo(snap.x, snap.y, time,

this.scroller.options.bounceEasing);

86}

87}

88

89if (this.moved) {

90this.scroller._execEvent('scrollEnd');

91}

92 },

这个地方由于我们后面不会实现便直接不予关注了

结语

突然来了几个BUG,等下要发布测试环境了,今天暂时到这里,我们下次继续好了,下次我们就直接分离iScroll了,抽出我们想要的功能

相关文档
最新文档