iScroll学习笔记2--浅读源码
iscroll的架子是这样的
(function (window, document, Math){ var utils = (function (){ var me = {}; // 扩展一些常用的工具方法为me的方法 return me; }()); function IScroll(el, options){ // 初始化一些属性和状态 } IScroll.prototype = { constructor: IScroll, // 主体方法都在这里 } }(window, document, Math))
来看看utils里有哪些好东西
var _elStyle = document.createElement('div').style; // 取得浏览器前缀 var _vendor = (function (){ var vendors = ['t', 'webkitT', 'MozT', 'msT', 'OT'], transform, i = 0, len = vendors.length; for(; i<len; i++){ transform = vendors[i] + 'ransform'; if(transform in _elStyle) return vendors[i].substr(0, vendors[i].length-1); } return false; }()); // 每次用css3动画都要加上烦人的浏览器前缀,都靠它了 function _prefixStyle (style){ if(_vendor === false) return false; if(_vendor === '') return style; // transform return _vendor + style.charAt(0).toUpperCase() + style.substr(1); } /** * 势能动画, touchEnd的时候触发, 计算手指(鼠标)离开之后运动的时间和距离 * @param {[type]} current [当前鼠标位置] * @param {[type]} start [touchstart时的鼠标位置] * @param {[type]} time [touchstart到touchend所用的时间] * @param {[type]} lowerMargin [边界距离] * @param {[type]} wrapperSize [容器的高度] * @return {[type]} [返回一个包含距离和时间两个属性的对象] */ me.momentum = function (current, start, time, lowerMargin, wrapperSize){ var distance = current -start, speed = Math.abs(distance) / time, destination, duration, deceleration = 0.0006; // v * (0-v)/2 * a 表示 速度由v到0所运动的距离 destination = current + (speed * speed ) / ( 2 * deceleration ) * ( distance < 0 ? -1 : 1 ); // t = v / a; duration = speed / deceleration; if( destination < lowerMargin ){ destination = wrapperSize ? lowerMargin - ( wrapperSize / 2.5 * (speed / 8) ) : lowerMargin; distance = Math.abs(destination - current); duration = distance / speed; }else if( destination > 0 ){ destination = wrapperSize ? wrapperSize / 2.5 * (speed / 8) : 0; distance = Math.abs(current) + destination; duration = distance / speed; } return { destination : Math.round(destination), duration : duration }; };
momentum这个函数相当好,用到了一点点物理知识,嘿嘿,花了点时间才理解是怎么回事
utils还扩展了一些功能性判定的函数,一起来看看
// 获取当前浏览器支持的transform属性,保存在变量中 var _transform = _prefixStyle('transform'); // 一些功能检测 me.extend(me, { hasTransform: _transform !== false, hasPerspective: _prefixStyle('perspective') in _elementStyle, hasTouch: 'ontouchstart' in window, hasPointer: window.PointerEvent || window.MSPointerEvent, // IE10 is prefixed hasTransition: _prefixStyle('transition') in _elementStyle }); // 对某些安卓版本做了检测 // This should find all Android browsers lower than build 535.19 (both stock browser and webview) me.isBadAndroid = /Android /.test(window.navigator.appVersion) && !(/Chrome\/\d/.test(window.navigator.appVersion)); // 保存 transform 和 transition 属性到 style属性下,方便后面操作, 这种思路值得学习 me.extend(me.style = {}, { transform: _transform, transitionTimingFunction: _prefixStyle('transitionTimingFunction'), transitionDuration: _prefixStyle('transitionDuration'), transitionDelay: _prefixStyle('transitionDelay'), transformOrigin: _prefixStyle('transformOrigin') });
还有一些,暂时不太懂,以后再补上
构造函数
在构造函数中,主要是初始化了一些配置参数和属性,以及初始状态
function IScroll (el, options) { this.wrapper = typeof el == 'string' ? document.querySelector(el) : el; this.scroller = this.wrapper.children[0]; // 缓存style以提高性能... this.scrollerStyle = this.scroller.style; // cache style for better performance this.options = { resizeScrollbars: true, mouseWheelSpeed: 20, snapThreshold: 0.334, // INSERT POINT: OPTIONS startX: 0, startY: 0, scrollY: true, directionLockThreshold: 5, momentum: true, bounce: true, bounceTime: 600, bounceEasing: '', preventDefault: true, preventDefaultException: { tagName: /^(INPUT|TEXTAREA|BUTTON|SELECT)$/ }, HWCompositing: true, useTransition: true, useTransform: true }; // 这里居然没用到 utils封装的 extend for ( var i in options ) { this.options[i] = options[i]; } // Some defaults this.x = 0; this.y = 0; this.directionX = 0; this.directionY = 0; this._events = {}; // INSERT POINT: DEFAULTS this._init(); this.refresh(); this.scrollTo(this.options.startX, this.options.startY); this.enable(); }
构造函数里调用了4个方法 _init , refresh , scrollTo, enable 来看看它们都干了什么
this.enabled 是个全局开关
enable: function () { this.enabled = true; }
// 构造函数里调用该函数并没有传第三个参数, 默认是用css3动画 scrollTo: function (x, y, time, easing) { easing = easing || utils.ease.circular; this.isInTransition = this.options.useTransition && time > 0; if ( !time || (this.options.useTransition && easing.style) ) { this._transitionTimingFunction(easing.style); this._transitionTime(time); this._translate(x, y); } else { this._animate(x, y, time, easing.fn); } } _translate: function (x, y) { if ( this.options.useTransform ) { /* REPLACE START: _translate */ this.scrollerStyle[utils.style.transform] = 'translate(' + x + 'px,' + y + 'px)' + this.translateZ; /* REPLACE END: _translate */ } else { x = Math.round(x); y = Math.round(y); this.scrollerStyle.left = x + 'px'; this.scrollerStyle.top = y + 'px'; } this.x = x; this.y = y; if ( this.indicators ) { for ( var i = this.indicators.length; i--; ) { this.indicators[i].updatePosition(); } } // INSERT POINT: _translate }
refresh: function () { // 这里获取wrapper的offsetHeight只是为了强制浏览器进行重排 var rf = this.wrapper.offsetHeight; // Force reflow // 重新计算各种尺寸 this.wrapperWidth = this.wrapper.clientWidth; this.wrapperHeight = this.wrapper.clientHeight; /* REPLACE START: refresh */ this.scrollerWidth = this.scroller.offsetWidth; this.scrollerHeight = this.scroller.offsetHeight; this.maxScrollX = this.wrapperWidth - this.scrollerWidth; this.maxScrollY = this.wrapperHeight - this.scrollerHeight; /* REPLACE END: refresh */ this.hasHorizontalScroll = this.options.scrollX && this.maxScrollX < 0; this.hasVerticalScroll = this.options.scrollY && this.maxScrollY < 0; if ( !this.hasHorizontalScroll ) { this.maxScrollX = 0; this.scrollerWidth = this.wrapperWidth; } if ( !this.hasVerticalScroll ) { this.maxScrollY = 0; this.scrollerHeight = this.wrapperHeight; } this.endTime = 0; this.directionX = 0; this.directionY = 0; this.wrapperOffset = utils.offset(this.wrapper); this._execEvent('refresh'); this.resetPosition(); // INSERT POINT: _refresh }
_init: function (){ this._initEvents(); }, _initEvents: function (remove){ var eventType = remove ? utils.removeEvent : utils.addEvent, target = this.options.bindToWrapper ? this.wrapper : window; eventType(this.wrapper, 'touchstart', this); eventType(target, 'touchmove', this); eventType(target, 'touchcancel', this); eventType(target, 'touchend', this); }, handleEvent: function (ev){ switch (ev.type){ case 'touchstart': this._start(ev); break; case 'touchmove': this._move(ev); break; case 'touchend': this._end(ev); break; } }
这里有一个知识点要提到,就是addEventListener/removeEventListener的第二个参数,也就是上面eventType的第三个参数,这里传递的是整个IScroll对象,而不是一个事件处理函数。
当传递的是一个对象时,会执行该对象下的 handleEvent方法,该方法就是真正的事件处理函数。具体可以参考这里
核心三大事件处理函数
_start: function (ev){ var point = ev.touches ? ev.touches[0] : ev, pos; this.initialed = utils.eventType[ev.type]; this.moved = false; // 偏移尺寸 this.distX = 0; this.distY = 0; this.startTime = utils.getTime(); // 如果当前动画没有停止,则停止动画 if(this.isInTransition){ this.isInTransition = false; pos = this.getComputedPosition(); this._translate(Math.round(pos.x), Math.round(pos.y)); this._execEvent('scrollEnd'); } // 起始位置 this.startX = this.x; this.startY = this.y; this.absStartX = this.x; this.absStartY = this.y; // 保存坐标位置 this.pointX = point.pageX; this.pointY = point.pageY; }, _move: function (ev){
if ( this.options.preventDefault ) { // increases performance on Android? TODO: check!
e.preventDefault();
}
var point = ev.touches ? ev.touches[0] : ev, deltaX = point.pageX - this.pointX, deltaY = point.pageY - this.pointY, newX, newY, absDistX, absDistY, timeStamp = utils.getTime(); this.pointX = point.pageX; this.pointY = point.pageY; // 保存移动的尺寸大小 this.distX += deltaX; this.distY += deltaY; absDistX = Math.abs(this.distX); absDistY = Math.abs(this.distY); // 至少移动10px if( timestamp - this.endTime > 300 && (absDistX < 10 && absDistY < 10)){ return; } newX = this.x + deltaX; newY = this.y + deltaY; // 超出边界减小拖动的速度 if( newX > 0 || newX < this.maxScrollX ){ newX = this.options.bounce ? this.x + deltaX / 3 : newX > 0 ? 0 : this.maxScrollX; } if( newY > 0 || newY < this.maxScrollY ){ newY = this.options.bounce ? this.x + deltaY / 3 : newY > 0 ? 0 : this.maxScrollY; } // 执行scrollStart用户注册函数 if(!this.moved){ this._execEvent('scrollStart'); } this.moved = true; // 移动 this.scroller this._translate(newX, newY); // 每300ms 重新更新起点坐标和起始时间 if(timeStamp - this.startTime > 300){ this.startTime = timeStamp; this.startX = this.x; this.startY = this.y; } }, _end: function (ev){ var point = e.changedTouches ? e.changedTouches[0] : ev, momentumX, momentumY, duration = utils.getTime() - this.startTime, // 这里不是接触屏幕的时间, 因为每300秒会重置一次 newX = Math.round(this.x), newY = Math.round(this.y), distanceX = Math.abs(newX - this.startX), distanceY = Math.abs(newY - this.startY), time = 0; this.isInTransition = 0; this.initiatd = 0; this.endTime = utils.getTime(); this.scrollTo(newX, newY); // 确保最后的位置是整数. // 计算手指离开屏幕后继续滑动的距离 if( this.optionsmomentum && duration < 300 ){ momentumX = this.utils.momentum(this.x, this.startX, duration, this.maxScrollX, 0, this.options.deceleration); momentumY = this.utils.momentum(this.y, this.startY, duration, this.maxScrollY, 0, this.options.deceleration); newX = momentumX.destination; newY = momentumY.destination; time = Math.max(momentumX.duration, momentumY.duration); } if( newX != this.x || newY != this.y ){ this.scrollTo(newX, newY, time); return; } this._execEvent('scrollEnd'); }
只是粗浅地看了看源码,一方面自己功力不够,很多地方看不太懂,两一方面没有在实战中用过,更加无法体会这个库的精妙之处。但是已经感觉受益良多,可见这个库相当之优秀,日后一定认真研究一番。