1. 程式人生 > >從原始碼角度深入理解iScroll中的scrollbars和indicators配置

從原始碼角度深入理解iScroll中的scrollbars和indicators配置

問題1:在IScroll中都是使用同樣的方法對scrollbars和indicators進行初始化

 if ( this.options.scrollbars || this.options.indicators ) {
		this._initIndicators();
	}
如果配置了scrollbars和indicators都是呼叫_initIndicators方法來完成的
問題2:scrollX和scrollY表示的是什麼?
       this.options.scrollY = this.options.eventPassthrough == 'vertical' ? false : this.options.scrollY;
       this.options.scrollX = this.options.eventPassthrough == 'horizontal' ? false : this.options.scrollX;
eventPassthrough表示忽略哪一個方向上的滾動,如果為vertical那麼表示忽略垂直方向的滾動,這時候this.options.scrollY就是false!
問題3:如何建立滾動條

建立滾動條和滾動槽是通過下面的方法來完成的:

function createDefaultScrollbar (direction, interactive, type) {
	var scrollbar = document.createElement('div'),
		indicator = document.createElement('div');
          //如果含有滾動條,那麼我們給滾動條設定absolute定位
	if ( type === true ) {
		scrollbar.style.cssText = 'position:absolute;z-index:9999';
		indicator.style.cssText = '-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;position:absolute;background:rgba(0,0,0,0.5);border:1px solid rgba(255,255,255,0.9);border-radius:3px';
	}
         //indicator含有className為iScrollIndicator
	indicator.className = 'iScrollIndicator';
        //如果方向是水平的滾動條同時也有滾動條
	if ( direction == 'h' ) {
		if ( type === true ) {
			scrollbar.style.cssText += ';height:7px;left:2px;right:2px;bottom:0';
			indicator.style.height = '100%';
		}
		scrollbar.className = 'iScrollHorizontalScrollbar';
	} else {
		//如果是垂直方向的滾動條
		if ( type === true ) {
			scrollbar.style.cssText += ';width:7px;bottom:2px;top:2px;right:1px';
			indicator.style.width = '100%';
		}
		scrollbar.className = 'iScrollVerticalScrollbar';
	}

	scrollbar.style.cssText += ';overflow:hidden';
    
       //如果interactive為false表示不允許響應事件,那麼為scrollbar元素的style新增pointerEvents為"none"就可以了,預設是""空字串
	if ( !interactive ) {
		scrollbar.style.pointerEvents = 'none';
	}
      //scrollbar新增子元素為indicator元素
	scrollbar.appendChild(indicator);
	return scrollbar;
}
注意:其中scrollbar表示的是滾動槽,而我們的indicator表示的是滾動條,這一點要理解。直接呼叫這個函式就會看到效果(滾動條的高度要設定,否則預設為0)。到了這一步,滾動條就建立好了,同時append到wrapper後面就完成了。下面就會如何讓滾動條在滾動槽中移動。
問題4:到底什麼是indicators?

解答:滾動條(非滾動槽);自定義指示元素

看個demo原始碼:

<div id="viewport">
	<div id="wrapper">
		<div id="scroller">
		<!--scroller中的元素才是我們可以看到的元素,wrapper定寬,而scroller不定寬-->
			<div class="slide">
				<div class="painting giotto"></div>
			</div>
			<div class="slide">
				<div class="painting leonardo"></div>
			</div>
			<div class="slide">
				<div class="painting gaugin"></div>
			</div>
			<div class="slide">
				<div class="painting warhol"></div>
			</div>
		</div>
	</div>
</div>

<div id="indicator">
	<div id="dotty"></div>
</div>
然後我們這樣使用iScroll元件:
var myScroll;
function loaded () {
	myScroll = new IScroll('#wrapper', {
		scrollX: true,
		scrollY: false,
		momentum: false,
		snap: true,
		snapSpeed: 400,
		keyBindings: true,
		//可以通過indicators來指定自己的Indicator,而滾動條也有自己的Indicator。iScroll會把兩者結合起來然後逐個
		//建立Indicator元素
		indicators: {
			el: document.getElementById('indicator'),
			resize: false
		}
	});
}
document.addEventListener('touchmove', function (e) { e.preventDefault(); }, false);
我們看看最後生成的DOM結構:


我們看看Indicator的建構函式主要做了什麼:

//注意:這裡建立Indicator是基於上面對滾動條的建立來完成的,其中Indicator的wrapper屬性就是對滾動條的包裹元素,即scrollbar滾動槽元素的引用!
function Indicator (scroller, options) {
	this.wrapper = typeof options.el == 'string' ? document.querySelector(options.el) : options.el;
	//wrapper自己指定(此處的wrapper是Indicator物件具有的wrapper)。返回的DOM結構為<div id="scrollbar"><div id="indicator"></div></div>,也就是wrapper物件就是內部的scrollbar元素DOM。
	//因為這裡構造的是Indicator物件,所以其wrapper當然就是scrollbar元素。如果是建立指示元素那麼其wrapper就表示我們自己通過el指定
	this.wrapperStyle = this.wrapper.style;
	//scrollbar元素的style屬性
	this.indicator = this.wrapper.children[0];
	//獲取indticator屬性,也是一個DOM
	this.indicatorStyle = this.indicator.style;
	//獲取indicator的style屬性
	this.scroller = scroller;
    //indicator的scroller屬性持有的就是iScroll元素的引用
	this.options = {
		listenX: true,//表示監聽X軸
		listenY: true,//表示監聽Y軸
		interactive: false,//可以操作
		resize: true,//滾動條的大小是基於wrapper和scroller的width/height來設定的,通過設定resizeScrollbars可以把滾動條設定為一個指定的大小
		defaultScrollbars: false,
		shrink: false,
		fade: false,//fade
		speedRatioX: 0,//指示元素的移動速度是根據sroller的大小來設定的。預設情況下是自動設定的,一般yuansu不需要改變這個值
		speedRatioY: 0//指示元素的移動速度是根據sroller的大小來設定的。預設情況下是自動設定的,一般不需要改變這個值
	};
     //繫結listenX,listenY,speedRatioX,speedRatioY,shrink,fade屬性等
	for ( var i in options ) {
		this.options[i] = options[i];
	}

	this.sizeRatioX = 1;
	this.sizeRatioY = 1;
	this.maxPosX = 0;
	this.maxPosY = 0;

	if ( this.options.interactive ) {
		//如果可以是touch事件,那麼我們為Indicator新增touchstart,touchend事件
		if ( !this.options.disableTouch ) {
			utils.addEvent(this.indicator, 'touchstart', this);
			utils.addEvent(window, 'touchend', this);
		}
		//如果可以有pointer事件,我們為Indicator新增pointerdown,pointerup事件
		if ( !this.options.disablePointer ) {
			utils.addEvent(this.indicator, utils.prefixPointerEvent('pointerdown'), this);
			utils.addEvent(window, utils.prefixPointerEvent('pointerup'), this);
		}
		//為Indicator新增mousedown,mouseup事件
		if ( !this.options.disableMouse ) {
			utils.addEvent(this.indicator, 'mousedown', this);
			utils.addEvent(window, 'mouseup', this);
		}
	}
    //如果沒有操作滾動條就消失,fade對應於this.options.fadeScrollbars
	if ( this.options.fade ) {
		//為iscrollbar元素新增transform屬性,也就是啟動硬體加速
		this.wrapperStyle[utils.style.transform] = this.scroller.translateZ;
		var durationProp = utils.style.transitionDuration;
		if(!durationProp) {
			return;
		}
		//為scrollbar元素新增transition-duration屬性
		this.wrapperStyle[durationProp] = utils.isBadAndroid ? '0.0001ms' : '0ms';
		// remove 0.0001ms
		var self = this;
		if(utils.isBadAndroid) {
			rAF(function() {
				if(self.wrapperStyle[durationProp] === '0.0001ms') {
					self.wrapperStyle[durationProp] = '0s';
				}
			});
		}
		//為我們的scrollbar元素新增opaitcity,然後讓它開始執行transform動畫
		this.wrapperStyle.opacity = '0';
	}
}
其實在這裡我們只為Indicator指定了wrapper,其對應於滾動條的滾動槽物件,而Indicator屬性對應於滾動條物件,同時scroller對應於iScroll物件。同時為Indicator綁定了一系列的事件(注意是繫結到this.indicator上還是window物件上的)。當然,還有一部分Indicator的方法全部定義在prototype上的,以後再分析
建立了Indicator後,我們需要做的就是為他繫結各種事件,不過在這之前我們看看一個方法:
          fade: function (val, hold) {
			//如果hold為true同時當前元素是不可見的,那麼不會呼叫fade放啊
			if ( hold && !this.visible ) {
				return;
			}

			clearTimeout(this.fadeTimeout);
			this.fadeTimeout = null;
			var time = val ? 250 : 500,
				delay = val ? 0 : 300;
				//如果沒有傳遞val

			val = val ? '1' : '0';
			this.wrapperStyle[utils.style.transitionDuration] = time + 'ms';
			//下面是一個立即執行函式
			this.fadeTimeout = setTimeout((function (val) {
				this.wrapperStyle.opacity = val;
				this.visible = +val;
			}).bind(this, val), delay);
		}
下面就是繫結的各種事件,如scrollCancel,scrollStart,beforeScrollStart,refresh,destroy事件等:
if ( this.options.fadeScrollbars ) {
	this.on('scrollEnd', function () {
		_indicatorsMap(function () {
			this.fade();
			//預設time是500(也就是anmation-durantion),delay為300,val(也就是opacity)為"0"(表示完全透明)。
			//就是使用val引數來指定animation-duration和animation-delay屬性的值,其中iScroll元素的visible屬性也是通過val來指定的
			//如果第一個引數沒有指定那麼就是0,否則就是1
		});
	});

	this.on('scrollCancel', function () {
		_indicatorsMap(function () {
			this.fade();
			//呼叫Indicator的prototype上的fade方法。
		});
	});

	this.on('scrollStart', function () {
		_indicatorsMap(function () {
			this.fade(1);
			//scrollstart表示開始滾動,這時候opacity就是1,也就是要讓它顯示出來
		});
	});

	this.on('beforeScrollStart', function () {
		_indicatorsMap(function () {
			this.fade(1, true);
			//beforeScrollStart還沒有開始滾動
		});
	});
}

//繫結refresh事件
this.on('refresh', function () {
	_indicatorsMap(function () {
		this.refresh();
	});
});
//繫結destroy事件
this.on('destroy', function () {
	_indicatorsMap(function () {
		this.destroy();
	});

	delete this.indicators;
});

從上面我們可以清楚的看到,我們為iScroll物件綁定了refresh事件

       //繫結refresh事件
		this.on('refresh', function () {
			_indicatorsMap(function () {
				this.refresh();
			});
		});
在refresh事件中我們呼叫了Indicators中的所有的refresh事件,我們先看看iScroll物件的refresh事件:
//重新整理:refresh做的事情就是獲取水平垂直可以滾動的距離,然後觸發refresh事件
	refresh: function () {
		utils.getRect(this.wrapper);
		//首先獲取到包裹元素矩形物件的clientWidth/clientHeight,clientWidth=width+2*borderWidth
		this.wrapperWidth	= this.wrapper.clientWidth;
		this.wrapperHeight	= this.wrapper.clientHeight;
		//獲取scroller元素的矩形物件,也就是他的width/height屬性
		var rect = utils.getRect(this.scroller);
		this.scrollerWidth	= rect.width;
		this.scrollerHeight	= rect.height;
        //maxScrollX,maxScrollY表示的最大的滾動距離,其值為父元素的clientWidth-子元素的width
        //wrapper可以設定width,但是scroll是不可以設定寬度的,所以maxScrollX如果為負數,那麼表示scroll特別寬,這時候表示可以往左邊移動,也就是是負數
        //wrapper可以設定height,但是scroll是不可以設定高度的,所以maxScrollY如果為負數,表示元素可以往上面移動
		this.maxScrollX		= this.wrapperWidth - this.scrollerWidth;
		this.maxScrollY		= this.wrapperHeight - this.scrollerHeight;
		this.hasHorizontalScroll	= this.options.scrollX && this.maxScrollX < 0;
		this.hasVerticalScroll		= this.options.scrollY && this.maxScrollY < 0;
		//如果指定了scrollX,同時maxScrollX<0。那麼這時候表示有水平的滾動條,如果>=0肯定是沒有水平滾動條的
	    //如果指定了scrollY,同時maxScrollY<0。那麼這時候表示有垂直的滾動條,如果>=0肯定是沒有垂直滾動條的
		if ( !this.hasHorizontalScroll ) {
			this.maxScrollX = 0;
			this.scrollerWidth = this.wrapperWidth;
		}
         //如果沒有垂直滾動條,那麼maxScrollY就是0,同時scroll的高度和wrap的高度是一樣的
		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);
		//獲取wrapper元素的offset值,一直往上計算,一直到該元素沒有offsetParent為止,同時要記住:這是逐級往上計算的,而且這是負數,通過這種方式可以簡單的獲取到距離document的距離!
		this._execEvent('refresh');
        //觸發refresh事件
		this.resetPosition();
	}
在iScroll物件的refresh事件中主要是獲取到可以滾動的垂直方向和水平方向的距離,同時觸發iScroll物件的refresh事件。在看iScroll的refresh事件之前我們首先看看resetPosition方法:
//重新設定位置,以time作為引數
   //that.resetPosition(that.options.bounceTime)
	resetPosition: function (time) {
		//this.x、this.y表示iScroll物件當前所在的位置
		var x = this.x,
			y = this.y;

		time = time || 0;
        //如果沒有水平滾動條或者this.x>0那麼x=0
		if ( !this.hasHorizontalScroll || this.x > 0 ) {
			x = 0;
		 //如果this.x<this.maxScrollX那麼水平方法可以滾動的距離為this.maxScrollX 
		} else if ( this.x < this.maxScrollX ) {
			x = this.maxScrollX;
		}
        
        //沒有垂直滾動條y=0,如果有垂直滾動條那麼就是this.maxScrollY
		if ( !this.hasVerticalScroll || this.y > 0 ) {
			y = 0;
		} else if ( this.y < this.maxScrollY ) {
			y = this.maxScrollY;
		}
         
		if ( x == this.x && y == this.y ) {
			return false;
		}
        //滾動到x,y的座標,時間為time,函式為this.options.bounceEasing。呼叫物件為該iScroll物件
		this.scrollTo(x, y, time, this.options.bounceEasing);
		return true;
	}
看完resetPosition,我們看看觸發了iScroll的refresh事件時候,iScroll是如何處理的:
this.on('refresh', function () {
			_indicatorsMap(function () {
				this.refresh();
			});
		});
很顯然,其會觸發所有的Indicator的refresh事件:

參考文獻: