【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)

其中的所有參數都沒有問題,唯獨第二個參數,為什么這么說呢?請看以下代碼:

1 var eventObj = {};
2 eventObj.a = 1;
3 document.addEventListener('click', eventObj)

各位覺得這個代碼有問題嗎?第二個參數顯然不是一個函數,但是function也是object呢,其實這樣也是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) {
2   el.addEventListener(type, fn, !!capture);
3 };

那么iScroll事件綁定的具體點便捕捉到了:

可以看到我們這里的fn是一個對象,但是不要擔心,我們的具體的方法在此:

 1 handleEvent: function (e) {
 2   switch (e.type) {
 3     case 'touchstart':
 4     case 'MSPointerDown':
 5     case 'mousedown':
 6       this._start(e);
 7       break;
 8     case 'touchmove':
 9     case 'MSPointerMove':
10     case 'mousemove':
11       this._move(e);
12       break;
13     case 'touchend':
14     case 'MSPointerUp':
15     case 'mouseup':
16     case 'touchcancel':
17     case 'MSPointerCancel':
18     case 'mousecancel':
19       this._end(e);
20       break;
21     case 'orientationchange':
22     case 'resize':
23       this._resize();
24       break;
25     case 'transitionend':
26     case 'webkitTransitionEnd':
27     case 'oTransitionEnd':
28     case 'MSTransitionEnd':
29       this._transitionEnd(e);
30       break;
31     case 'wheel':
32     case 'DOMMouseScroll':
33     case 'mousewheel':
34       this._wheel(e);
35       break;
36     case 'keydown':
37       this._key(e);
38       break;
39     case 'click':
40       if (!e._constructed) {
41         e.preventDefault();
42         e.stopPropagation();
43       }
44       break;
45   }
46 }

高大帥哈,如此整個iScroll的DOM事件相關就沒問題了,在具體就回到了上次的三大事件點了

自定義事件機制

其實在我們學習backbone時候我們就提到了這塊操作

 1 var Events = Backbone.Events = {
 2 
 3   on: function (name, callback, context) {
 4     if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this;
 5     this._events || (this._events = {});
 6     var events = this._events[name] || (this._events[name] = []);
 7     events.push({ callback: callback, context: context, ctx: context || this });
 8     return this;
 9   },
10 
11   off: function (name, callback, context) {
12     var retain, ev, events, names, i, l, j, k;
13     if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this;
14     if (!name && !callback && !context) {
15       this._events = {};
16       return this;
17     }
18 
19     names = name ? [name] : _.keys(this._events);
20     for (i = 0, l = names.length; i < l; i++) {
21       name = names[i];
22       if (events = this._events[name]) {
23         this._events[name] = retain = [];
24         if (callback || context) {
25           for (j = 0, k = events.length; j < k; j++) {
26             ev = events[j];
27             if ((callback && callback !== ev.callback && callback !== ev.callback._callback) ||
28               (context && context !== ev.context)) {
29               retain.push(ev);
30             }
31           }
32         }
33         if (!retain.length) delete this._events[name];
34       }
35     }
36 
37     return this;
38   },
39 
40   trigger: function (name) {
41     if (!this._events) return this;
42     var args = slice.call(arguments, 1);
43     if (!eventsApi(this, 'trigger', name, args)) return this;
44     var events = this._events[name];
45     var allEvents = this._events.all;
46     if (events) triggerEvents(events, args);
47     if (allEvents) triggerEvents(allEvents, arguments);
48     return this;
49   },
50 };
51 
52 // Regular expression used to split event strings.
53 var 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.
58 var eventsApi = function (obj, action, name, rest) {
59   if (!name) return true;
60 
61   // Handle event maps.
62   if (typeof name === 'object') {
63     for (var key in name) {
64       obj[action].apply(obj, [key, name[key]].concat(rest));
65     }
66     return false;
67   }
68 
69   // Handle space separated event names.
70   if (eventSplitter.test(name)) {
71     var names = name.split(eventSplitter);
72     for (var i = 0, l = names.length; i < l; i++) {
73       obj[action].apply(obj, [names[i]].concat(rest));
74     }
75     return false;
76   }
77 
78   return true;
79 };

所謂自建事件機制,其實是唬人的,就是用一個數組保存各個階段的函數,到特定階段執行便可,iScroll這塊做的尤其簡單,而且又注冊沒有注銷:

 1 on: function (type, fn) {
 2   if (!this._events[type]) {
 3     this._events[type] = [];
 4   }
 5 
 6   this._events[type].push(fn);
 7 },
 8 
 9 _execEvent: function (type) {
10   if (!this._events[type]) {
11     return;
12   }
13 
14   var i = 0,
15     l = this._events[type].length;
16 
17   if (!l) {
18     return;
19   }
20 
21   for (; i < l; i++) {
22     this._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的流量

 1 function Indicator(scroller, options) {
 2   this.wrapper = typeof options.el == 'string' ? document.querySelector(options.el) : options.el;
 3   this.wrapperStyle = this.wrapper.style;
 4   this.indicator = this.wrapper.children[0];
 5   this.indicatorStyle = this.indicator.style;
 6   this.scroller = scroller;
 7 
 8   this.options = {
 9     listenX: true,
10     listenY: true,
11     interactive: false,
12     resize: true,
13     defaultScrollbars: false,
14     shrink: false,
15     fade: false,
16     speedRatioX: 0,
17     speedRatioY: 0
18   };
19 
20   for (var i in options) {
21     this.options[i] = options[i];
22   }
23 
24   this.sizeRatioX = 1;
25   this.sizeRatioY = 1;
26   this.maxPosX = 0;
27   this.maxPosY = 0;
28 
29   if (this.options.interactive) {
30     if (!this.options.disableTouch) {
31       utils.addEvent(this.indicator, 'touchstart', this);
32       utils.addEvent(window, 'touchend', this);
33     }
34     if (!this.options.disablePointer) {
35       utils.addEvent(this.indicator, 'MSPointerDown', this);
36       utils.addEvent(window, 'MSPointerUp', this);
37     }
38     if (!this.options.disableMouse) {
39       utils.addEvent(this.indicator, 'mousedown', this);
40       utils.addEvent(window, 'mouseup', this);
41     }
42   }
43 
44   if (this.options.fade) {
45     this.wrapperStyle[utils.style.transform] = this.scroller.translateZ;
46     this.wrapperStyle[utils.style.transitionDuration] = utils.isBadAndroid ? '0.001s' : '0ms';
47     this.wrapperStyle.opacity = '0';
48   }
49 }

設個就是滾動條的構造函數,這有一個關鍵點:

interactiveScrollbars: true

默認我們的滾動條是不會具有滾動等事件的,如果設置了此參數便具有可拖動特性了

 1 if (this.options.interactive) {
 2   if (!this.options.disableTouch) {
 3     utils.addEvent(this.indicator, 'touchstart', this);
 4     utils.addEvent(window, 'touchend', this);
 5   }
 6   if (!this.options.disablePointer) {
 7     utils.addEvent(this.indicator, 'MSPointerDown', this);
 8     utils.addEvent(window, 'MSPointerUp', this);
 9   }
10   if (!this.options.disableMouse) {
11     utils.addEvent(this.indicator, 'mousedown', this);
12     utils.addEvent(window, 'mouseup', this);
13   }
14 }

這里雖然給滾動條綁定的事件,但是會一並操作我們的body主體,但是我們后面會直接忽略這步操作

1 if (this.options.fade) {
2   this.wrapperStyle[utils.style.transform] = this.scroller.translateZ;
3   this.wrapperStyle[utils.style.transitionDuration] = utils.isBadAndroid ? '0.001s' : '0ms';
4   this.wrapperStyle.opacity = '0';
5 }

然后,會給滾動條一個漸隱的效果,這個影響較小,直接使用了CSS3實現

下面繼續實現了他的事件handleEvent

 1 handleEvent: function (e) {
 2   switch (e.type) {
 3     case 'touchstart':
 4     case 'MSPointerDown':
 5     case 'mousedown':
 6       this._start(e);
 7       break;
 8     case 'touchmove':
 9     case 'MSPointerMove':
10     case 'mousemove':
11       this._move(e);
12       break;
13     case 'touchend':
14     case 'MSPointerUp':
15     case 'mouseup':
16     case 'touchcancel':
17     case 'MSPointerCancel':
18     case 'mousecancel':
19       this._end(e);
20       break;
21   }
22 }
View Code

接下來又是touch幾個事件了:

 1 _start: function (e) {
 2   var point = e.touches ? e.touches[0] : e;
 3 
 4   e.preventDefault();
 5   e.stopPropagation();
 6 
 7   this.transitionTime();
 8 
 9   this.initiated = true;
10   this.moved = false;
11   this.lastPointX = point.pageX;
12   this.lastPointY = point.pageY;
13 
14   this.startTime = utils.getTime();
15 
16   if (!this.options.disableTouch) {
17     utils.addEvent(window, 'touchmove', this);
18   }
19   if (!this.options.disablePointer) {
20     utils.addEvent(window, 'MSPointerMove', this);
21   }
22   if (!this.options.disableMouse) {
23     utils.addEvent(window, 'mousemove', this);
24   }
25 
26   this.scroller._execEvent('beforeScrollStart');
27 },
28 
29 _move: function (e) {
30   var point = e.touches ? e.touches[0] : e,
31     deltaX, deltaY,
32     newX, newY,
33     timestamp = utils.getTime();
34 
35   if (!this.moved) {
36     this.scroller._execEvent('scrollStart');
37   }
38 
39   this.moved = true;
40 
41   deltaX = point.pageX - this.lastPointX;
42   this.lastPointX = point.pageX;
43 
44   deltaY = point.pageY - this.lastPointY;
45   this.lastPointY = point.pageY;
46 
47   newX = this.x + deltaX;
48   newY = this.y + deltaY;
49 
50   this._pos(newX, newY);
51 
52   // INSERT POINT: indicator._move
53 
54   e.preventDefault();
55   e.stopPropagation();
56 },
57 
58 _end: function (e) {
59   if (!this.initiated) {
60     return;
61   }
62 
63   this.initiated = false;
64 
65   e.preventDefault();
66   e.stopPropagation();
67 
68   utils.removeEvent(window, 'touchmove', this);
69   utils.removeEvent(window, 'MSPointerMove', this);
70   utils.removeEvent(window, 'mousemove', this);
71 
72   if (this.scroller.options.snap) {
73     var snap = this.scroller._nearestSnap(this.scroller.x, this.scroller.y);
74 
75     var time = this.options.snapSpeed || Math.max(
76             Math.max(
77                 Math.min(Math.abs(this.scroller.x - snap.x), 1000),
78                 Math.min(Math.abs(this.scroller.y - snap.y), 1000)
79             ), 300);
80 
81     if (this.scroller.x != snap.x || this.scroller.y != snap.y) {
82       this.scroller.directionX = 0;
83       this.scroller.directionY = 0;
84       this.scroller.currentPage = snap;
85       this.scroller.scrollTo(snap.x, snap.y, time, this.scroller.options.bounceEasing);
86     }
87   }
88 
89   if (this.moved) {
90     this.scroller._execEvent('scrollEnd');
91   }
92 },
View Code

這個地方由於我們后面不會實現便直接不予關注了

結語

突然來了幾個BUG,等下要發布測試環境了,今天暫時到這里,我們下次繼續好了,下次我們就直接分離iScroll了,抽出我們想要的功能


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM