fastclick.js源碼解讀分析

  閱讀優秀的js插件和庫源碼,能夠加深咱們對web開發的理解和提升js能力,本人能力有限,只能粗略讀懂一些小型插件,這裏帶來對fastclick源碼的解讀,望各位大神不吝指教~!javascript

fastclick誕生背景與使用

  在解讀源碼前,仍是簡單介紹下fastclick:html

誕生背景

  咱們都知道,在移動端頁面開發上,會出現一個問題,click事件會有300ms的延遲,這讓用戶感受很不爽,感受像是網頁卡頓了同樣,實際上,這是瀏覽器爲了更好的判斷用戶的雙擊行爲,移動瀏覽器都支持雙擊縮放或雙擊滾動的操做,好比一個連接,當用戶第一次點擊後,瀏覽器不能馬上判斷用戶確實要打開這個連接,仍是想要進行雙擊的操做,所以幾乎如今全部瀏覽器都效仿Safari當年的約定,在點擊事件上加了300毫秒的延遲。html5

  就由於這300ms的延遲,催生了fastclick的誕生~java

使用方法

  1.引入fastclick到本身的開發環境(源碼第829~840行,後面都採用簡寫了哈,如:829~840)node

//優先兼容AMD方式
if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) {
  define(function() {
    return FastClick;
  });
} else if (typeof module !== 'undefined' && module.exports) {
  //兼容commonJs風格
  module.exports = FastClick.attach;
  module.exports.FastClick = FastClick;
} else {
  //最後兼容原生Js  
  window.FastClick = FastClick;
}   

  fastclick的引入兼容AMD、commonJs風格、原生Js的方式,在本人的大半年開發過程當中,只接觸過commonJs的風格,這裏就很少作介紹了,根據本身項目技術棧選擇吧~ios

  2.入口函數(824~826)git

//layer參數:要監聽的dom對象,通常是document.body
//options參數:用來覆蓋自定義參數,我的建議不去覆蓋,
//由於裏面的參數設定都是FastClick的精華,不要着急,參數在後面會詳細介紹
FastClick.attach = function(layer, options) {
  return new FastClick(layer, options);
}; 

  咱們若是要使用fastclick的話,只須要在本身的js上寫上FastClick.attach(document.body),這樣就能夠了,沒錯,就是這麼簡單!github

fastclick源碼解讀

  

  判斷是否須要調用FastClick(105~107)

  fastclick在某些狀況下是不須要的,固然fastclick的開發者早已經替咱們想到了,在官網上有詳細的解釋,若是你想詳細瞭解,請點擊這裏web

//全部在不須要FastClick的瀏覽器會直接return掉,不會執行fastclick.js後面的代碼。
if (FastClick.notNeeded(layer)) {
  return;
}

 

  參數解讀(23~103)

  上面提到了入口函數中的options參數,這裏不得不讚一下fastclick的源碼,對每一個參數都作出了詳細的解釋(雖然都是英文,但很容易懂),這裏介紹幾個我認爲比較精華的參數,以下代碼:瀏覽器

//好比這幾個參數,上面提到不建議自定義覆蓋,
//這些參數正是FastClick的精華所在,
//大幅度修改數值可能讓整個庫的功效大打折扣。
this.touchBoundary = options.touchBoundary || 10;
this.tapDelay = options.tapDelay || 200;
this.tapTimeout = options.tapTimeout || 700;

  touchBoundary: 這個參數是用於判斷用戶觸摸屏幕後,移動的距離,若是大於10px,那麼就不被看作是一次點擊事件(具體實現後面介紹,下面的參數也一樣會解釋)。

  tapDelay: 這個參數規定了touchstart和touchend事件之間的200毫秒最小間隔,若是在這段時間內,發生了第二次點擊將會被阻止。

  tapTimeout: 這個參數規定了一次tap事件(源碼解釋爲tap事件)最長的事件,即touchstart和touchend事件之間的700毫秒最大間隔,超過這個時間,將不會被視做tap事件。

  固然還有不少參數,由於篇幅的關係,這裏就不一一解釋了,也不貼出源碼,若是你想了解更多,請下載並閱讀源碼23~103行,每一個參數都有詳細的解釋,只要學太高中英語都能讀得懂- -(我四級沒過都能讀得懂。。。)

    主幹部分解讀(23~174)

function FastClick(layer, options) {
        var oldOnClick;

        options = options || {};

        //這裏原本是定義了一些參數的,但我在以前講過了,這裏的代碼被我刪掉了

        //若是是屬於不須要處理的元素類型,則直接返回,notNeeded方法已在上方提到
        if (FastClick.notNeeded(layer)) {
            return;
        }

        //語法糖,兼容一些用不了 Function.prototype.bind 的舊安卓
        //因此後面不走 layer.addEventListener('click', this.onClick.bind(this), true);而是調用的這裏的bind方法
        function bind(method, context) {
            return function() { return method.apply(context, arguments); };
        }


        var methods = ['onMouse', 'onClick', 'onTouchStart', 'onTouchMove', 'onTouchEnd', 'onTouchCancel'];
        var context = this;
        for (var i = 0, l = methods.length; i < l; i++) {
            context[methods[i]] = bind(context[methods[i]], context);//調用上面定義的bind()方法
        }

        //綁定事件,安卓須要作額外處理
        if (deviceIsAndroid) {
            layer.addEventListener('mouseover', this.onMouse, true);
            layer.addEventListener('mousedown', this.onMouse, true);
            layer.addEventListener('mouseup', this.onMouse, true);
        }

        layer.addEventListener('click', this.onClick, true);
        layer.addEventListener('touchstart', this.onTouchStart, false);
        layer.addEventListener('touchmove', this.onTouchMove, false);
        layer.addEventListener('touchend', this.onTouchEnd, false);
        layer.addEventListener('touchcancel', this.onTouchCancel, false);

        // 兼容不支持 stopImmediatePropagation 的瀏覽器
        if (!Event.prototype.stopImmediatePropagation) {
            layer.removeEventListener = function(type, callback, capture) {
                var rmv = Node.prototype.removeEventListener;
                if (type === 'click') {
                    rmv.call(layer, type, callback.hijacked || callback, capture);
                } else {
                    rmv.call(layer, type, callback, capture);
                }
            };

            layer.addEventListener = function(type, callback, capture) {
                var adv = Node.prototype.addEventListener;
                if (type === 'click') {
                    //留意這裏 callback.hijacked 中會判斷 event.propagationStopped 是否爲真來確保(安卓的onMouse事件)只執行一次
                    //在 onMouse 事件裏會給 event.propagationStopped 賦值 true
                    adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) {
                            if (!event.propagationStopped) {
                                callback(event);
                            }
                        }), capture);
                } else {
                    adv.call(layer, type, callback, capture);
                }
            };
        }

        // 若是layer直接在DOM上寫了 onclick 方法,那咱們須要把它替換爲 addEventListener 綁定形式
        if (typeof layer.onclick === 'function') {
            oldOnClick = layer.onclick;
            layer.addEventListener('click', function(event) {
                oldOnClick(event);
            }, false);
            layer.onclick = null;
        }
    }

  在fastclick的主幹部分,主要作了這麼幾件事情:
  1.定義一些參數,在後面的代碼中會用到,做用已在前面提過。

  2.判斷是否須要使用fastclick。

  3.綁定了事件:注意,這裏綁定的都是fastclick中定義的事件,並非原生事件,由於使用bind()方法作了處理,事件回調中的this都是fastclick實例上下文。

  4.兼容不支持 stopImmediatePropagation 的瀏覽器。

  5.將dom上寫的onclick方法替換爲addEventListener綁定形式

  核心部分解讀(包括核心部分涉及到的方法)

  下面代碼中的註釋是我本身的理解,若有不對的地方請各位閱讀者指出~~  

  1.onTouchStart(391-450): 

FastClick.prototype.onTouchStart = function(event) {
        var targetElement, touch, selection;

        // 若是是多點觸摸,將被忽略,直接返回true,不會執行後面代碼
        if (event.targetTouches.length > 1) {
            return true;
        }
        //得到觸摸對象,這個getTargetElementFromEventTarget方法將稍後講解
        targetElement = this.getTargetElementFromEventTarget(event.target);
        touch = event.targetTouches[0];
                //判斷系統是否爲ios
        if (deviceIsIOS) {

            // 在ios中,受信任的事件將會被取消,返回true。相關知識:若是一個事件是由設備自己(如瀏覽器)觸發的,而不是經過JavaScript模擬合成的,那個這個事件被稱爲可信任的(trusted)
            //得到激活選中區
            selection = window.getSelection();
            //判斷是否有range被選中&&選中「起點」和「結束點」是否重合,這一部分我猜想應該是ios自帶的複製文字效果,爲了防止用戶意圖複製文字時觸發tap事件。
            if (selection.rangeCount && !selection.isCollapsed) {
                return true;
            }
//這一部分應該是對ios4中的bug進行處理吧,不過如今也沒什麼人用ios4這種古董系統,因此註釋我就不翻譯了,有興趣本身去了解吧~
            if (!deviceIsIOS4) {

                // Weird things happen on iOS when an alert or confirm dialog is opened from a click event callback (issue #23):
                // when the user next taps anywhere else on the page, new touchstart and touchend events are dispatched
                // with the same identifier as the touch event that previously triggered the click that triggered the alert.
                // Sadly, there is an issue on iOS 4 that causes some normal touch events to have the same identifier as an
                // immediately preceeding touch event (issue #52), so this fix is unavailable on that platform.
                // Issue 120: touch.identifier is 0 when Chrome dev tools 'Emulate touch events' is set with an iOS device UA string,
                // which causes all touch events to be ignored. As this block only applies to iOS, and iOS identifiers are always long,
                // random integers, it's safe to to continue if the identifier is 0 here.
                if (touch.identifier && touch.identifier === this.lastTouchIdentifier) {
                    event.preventDefault();
                    return false;
                }

                this.lastTouchIdentifier = touch.identifier;

                // If the target element is a child of a scrollable layer (using -webkit-overflow-scrolling: touch) and:
                // 1) the user does a fling scroll on the scrollable layer
                // 2) the user stops the fling scroll with another tap
                // then the event.target of the last 'touchend' event will be the element that was under the user's finger
                // when the fling scroll was started, causing FastClick to send a click event to that layer - unless a check
                // is made to ensure that a parent layer was not scrolled before sending a synthetic click (issue #42).
                this.updateScrollParent(targetElement);
            }
        }

//記錄click已經發生,這也是一個參數喲!
        this.trackingClick = true;
//記錄click發生的時間戳,參數一員
        this.trackingClickStart = event.timeStamp;
//記錄click的目標對象,參數一員
        this.targetElement = targetElement;

//這裏不解釋,大家懂得
        this.touchStartX = touch.pageX;
        this.touchStartY = touch.pageY;

        //防止200ms內的屢次點擊,tapDelay這個參數在上面提到過
        if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {
            event.preventDefault();
        }

        return true;
    };      

  

FastClick.prototype.getTargetElementFromEventTarget = function(eventTarget) {

		// 在一些舊的瀏覽器(尤爲是Safari瀏覽器在iOS4.1)事件目標多是一個文本節點。那麼這個時候返回它的父節點。(⊙o⊙)…漲知識,不過目前來看可能這種狀況不多了。
		if (eventTarget.nodeType === Node.TEXT_NODE) {
			return eventTarget.parentNode;
		}

		return eventTarget;
	};

 onTouchStart這個單詞,很容易讓咱們知道fastclick中的tap仍然是經過touch事件進行模擬的,在touchStart時,fastclick主要作了這麼幾件事:

  1.忽略了多點觸摸的狀況

  2.解決了一些兼容性問題(ios4 和 ios複製文字效果)

 

    3.追蹤click事件,得到click對象,記錄了發生click事件時的時間戳

  4.防止200ms內的屢次點擊

  這裏其實有點亂,由於實際上是touch事件,可是爲何記做click事件呢(有的時候又說是tap事件),咱們能夠這樣理解:本質上發生是touch事件,而fastclick要根據touch事件模擬click(tap)事件,這有一些條件,當該次觸摸事件符合條件時,即可以認爲是一次click事件,tap事件就是相對於pc端的click事件,因此移動端tap事件==pc端click事件。恩,由於源碼中用到了trackingClickStart和一些帶click的參數,因此大家懂的。tap事件自己是不存在的,是一種合成事件。

  2.onTouchMove(476~488)

FastClick.prototype.onTouchMove = function(event) {
		if (!this.trackingClick) {
			return true;
		}

		// 若是touchMove超過了規定距離(10px),那麼取消追蹤此次touch事件,不會被模擬爲tap,能夠理解爲:用戶手指在滑動屏幕。。
		if (this.targetElement !== this.getTargetElementFromEventTarget(event.target) || this.touchHasMoved(event)) {
			this.trackingClick = false;
			this.targetElement = null;
		}

		return true;
	};

  

FastClick.prototype.touchHasMoved = function(event) {
		var touch = event.changedTouches[0], boundary = this.touchBoundary;
      //這裏就是判斷touchMove移動的距離(x軸和y軸)是否超過boundary(10px),超過返回true    
		if (Math.abs(touch.pageX - this.touchStartX) > boundary || Math.abs(touch.pageY - this.touchStartY) > boundary) {
			return true;
		}

		return false;
	};

onTouchMove很明顯就是在觸摸過程當中手指發生位移觸發的事件,fastclick在這裏主要作了兩件事:

  1.首先判斷是否有符合條件的tranckingClick,tranck意思是追蹤,就是在onTouchStart階段提供的判斷條件,條件經過那麼該次touch事件將被追蹤,記做tranckingClick。

  2.若是touchMove超過了規定距離(x軸或y軸10px),那麼取消追蹤此次touch事件,不會被模擬爲tap,能夠理解爲:用戶手指在滑動屏幕。。

  3.onTouchEnd(521~610)

FastClick.prototype.onTouchEnd = function(event) {
		var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement;
                //很少說了,大家懂的
		if (!this.trackingClick) {
			return true;
		}

		// 仍是爲了防止屢次點擊,不過這裏多了一個參數cancleNextClick,該屬性會在onMouse事件中被判斷,爲true則完全禁用事件和冒泡
		if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {
			this.cancelNextClick = true;
			return true;
		}
          //識別長按事件,tapTimeOut默認爲700ms
		if ((event.timeStamp - this.trackingClickStart) > this.tapTimeout) {
			return true;
		}

		// 重置爲false避免input事件意外取消
		this.cancelNextClick = false;
                //標記touchEnd時間戳,方便下一次touchStart斷定雙擊
		this.lastClickTime = event.timeStamp;

		trackingClickStart = this.trackingClickStart;
          //重置這兩個參數 this.trackingClick = false; this.trackingClickStart = 0; //這裏又修復了一個ios的bug,啪啪啪一大串英文實在讀不懂,解決的是ios6的bug,沒興趣詳細瞭解。。 if (deviceIsIOSWithBadTarget) { touch = event.changedTouches[0]; // In certain cases arguments of elementFromPoint can be negative, so prevent setting targetElement to null targetElement = document.elementFromPoint(touch.pageX - window.pageXOffset, touch.pageY - window.pageYOffset) || targetElement; targetElement.fastClickScrollParent = this.targetElement.fastClickScrollParent; } targetTagName = targetElement.tagName.toLowerCase(); if (targetTagName === 'label') {//是lable的話激活其指向的組件 //findControl這個方法將在後面介紹,大概就是點擊label的時候,找到他指向的元素,並獲取焦點。 forElement = this.findControl(targetElement); //若是找到了對應的元素 if (forElement) { this.focus(targetElement); if (deviceIsAndroid) {//安卓直接返回 return false; } targetElement = forElement; } } else if (this.needsFocus(targetElement)) {//needsFocus方法我將稍後說明,用於判斷目標元素是否須要得到焦點 //觸摸在元素上的事件超過100ms,則置空targetElement並返回false,也就是去走原生的focus方法去了,至於爲何這麼作,目前還不是太明白 // 後面這裏又解決了ios五、6上的兩個兼容性bug,(⊙o⊙)…很少作研究了,由於這個狀況已經太少了。 if ((event.timeStamp - trackingClickStart) > 100 || (deviceIsIOS && window.top !== window && targetTagName === 'input')) { this.targetElement = null; return false; } //得到焦點(這裏不是原生的) this.focus(targetElement); //sendClick是重點,將在後面講解,咱們須要知道的是這裏將當即觸發,並無300ms延遲 this.sendClick(targetElement, event); // 這個地方是爲了防止ios四、六、7上面select展開的問題 if (!deviceIsIOS || targetTagName !== 'select') { this.targetElement = null; event.preventDefault(); } return false; } if (deviceIsIOS && !deviceIsIOS4) { //又是ios的hack代碼,貌似是解決滾動區域的點擊問題 scrollParent = targetElement.fastClickScrollParent; if (scrollParent && scrollParent.fastClickLastScrollTop !== scrollParent.scrollTop) { return true; } } //肯定目標元素是否須要原生click,方法後面會介紹 if (!this.needsClick(targetElement)) { //若是這不是一個須要使用原生click的元素,則屏蔽原生事件,避免觸發兩次click event.preventDefault(); //觸發一次模擬的click事件 this.sendClick(targetElement, event); } return false; };

  

	FastClick.prototype.findControl = function(labelElement) {

		// 支持html5 control屬性的話,返回其指向的元素
		if (labelElement.control !== undefined) {
			return labelElement.control;
		}

		// 支持html5 htmlFor屬性的話,返回其指向的元素
		if (labelElement.htmlFor) {
			return document.getElementById(labelElement.htmlFor);
		}

		// 若是以上屬性都不支持,嘗試返回lable的後代元素
		return labelElement.querySelector('button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea');
	};

 

//判斷是否須要得到焦點
FastClick.prototype.needsFocus = function(target) {
		switch (target.nodeName.toLowerCase()) {
		case 'textarea':
			return true;
		case 'select':
			return !deviceIsAndroid;
		case 'input':
			switch (target.type) {
			case 'button':
			case 'checkbox':
			case 'file':
			case 'image':
			case 'radio':
			case 'submit':
				return false;
			}

			return !target.disabled && !target.readOnly;
		default://目標元素若是有'needsfocus'的類,那麼返回true
			return (/\bneedsfocus\b/).test(target.className);
		}
	};

  

//一看名字就知道是判斷是否須要原生click事件
FastClick.prototype.needsClick = function(target) {
		switch (target.nodeName.toLowerCase()) {

		// Don't send a synthetic click to disabled inputs (issue #62)
		case 'button':
		case 'select':
		case 'textarea':
			if (target.disabled) {
				return true;
			}

			break;
		case 'input':

			//hack代碼,ios6瀏覽器的bug,input[type='file']須要原生click事件
			if ((deviceIsIOS && target.type === 'file') || target.disabled) {
				return true;
			}

			break;
		case 'label':
		case 'iframe': // iOS8 homescreen apps can prevent events bubbling into frames
		case 'video':
			return true;
		}
                //這裏須要注意了,後面會說明。
		return (/\bneedsclick\b/).test(target.className);
	};        

  onTouchEnd這個方法的代碼量比較多一些,由於解決了不少稀奇古怪的兼容性問題,寫一個好的js插件還真是不容易,就解決個點擊事件300ms延遲問題,hack代碼我並無很是認真的研究到底,也看的暈乎乎的。好了廢話很少說,這一部分主要是作了這麼幾件事情:

  1.首先判斷此次touch事件是否仍是處於追蹤狀態,若是不是,那麼什麼都不作了。

  2.防止屢次點擊問題

  3.若是是長按事件不予理會  

    4.若是目標元素是lable,那麼找到其指向的元素並獲取焦點,若是不是,那麼判斷元素是否須要獲取焦點,最後確認目標是否須要原生click事件,若是不須要那麼屏蔽掉原生click事件,並觸發一次模擬的click事件(tap事件)。

  5.解決了一大推兼容性問題。

     4.sendClick

//合成一個click事件並在指定元素上觸發
    FastClick.prototype.sendClick = function(targetElement, event) {
        var clickEvent, touch;

        // 在一些安卓機器中,得讓頁面所存在的 activeElement(聚焦的元素,好比input)失焦,不然合成的click事件將無效
        if (document.activeElement && document.activeElement !== targetElement) {
            document.activeElement.blur();
        }

        touch = event.changedTouches[0];

        // 合成(自定義事件) 一個 click 事件
        // 經過一個額外屬性確保它能被追蹤(tracked)
        clickEvent = document.createEvent('MouseEvents');
        clickEvent.initMouseEvent(this.determineEventType(targetElement), true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);
        clickEvent.forwardedTouchEvent = true; // fastclick的內部變量,用來識別click事件是原生仍是合成的
        targetElement.dispatchEvent(clickEvent); //當即觸發其click事件
    };

    FastClick.prototype.determineEventType = function(targetElement) {

        //安卓設備下 Select 沒法經過合成的 click 事件被展開,得改成 mousedown
        if (deviceIsAndroid && targetElement.tagName.toLowerCase() === 'select') {
            return 'mousedown';
        }

        return 'click';
    };

  終於走到這一步,這裏合成了一個click事件,而且合成的click事件當即觸發,是沒有300ms的延遲的~~~

   5.onMouse 和 onClick(630~704)

//用於決定是否採用原生click事件
    FastClick.prototype.onMouse = function(event) {

        // touch事件一直沒觸發
        if (!this.targetElement) {
            return true;
        }

        if (event.forwardedTouchEvent) { //觸發的click事件是合成的
            return true;
        }
        // 確保其沒執行過 preventDefault 方法(event.cancelable 不爲 true)便可
        if (!event.cancelable) {
            return true;
        }

        // 須要作預防穿透處理的元素,或者作了快速(200ms)雙擊的狀況
        if (!this.needsClick(this.targetElement) || this.cancelNextClick) {
            //中止當前默認事件和冒泡
            if (event.stopImmediatePropagation) {
                event.stopImmediatePropagation();
            } else {

                // 不支持 stopImmediatePropagation 的設備作標記,
                // 確保該事件回調不會執行
                event.propagationStopped = true;
            }

            // 取消事件和冒泡
            event.stopPropagation();
            event.preventDefault();

            return false;
        }

        
        return true;
    };


    //click事件常規都是touch事件衍生來的,也排在touch後面觸發。
    //對於那些咱們在touch事件過程沒有禁用掉默認事件的event來講,咱們還須要在click的捕獲階段進一步
    //作判斷決定是否要禁掉點擊事件
    FastClick.prototype.onClick = function(event) {
        var permitted;

        // 若是還有 trackingClick 存在,多是某些UI事件阻塞了touchEnd 的執行
        if (this.trackingClick) {
            this.targetElement = null;
            this.trackingClick = false;
            return true;
        }

        // 依舊是對 iOS 怪異行爲的處理 —— 若是用戶點擊了iOS模擬器裏某個表單中的一個submit元素
        // 或者點擊了彈出來的鍵盤裏的「Go」按鈕,會觸發一個「僞」click事件(target是一個submit-type的input元素)
        if (event.target.type === 'submit' && event.detail === 0) {
            return true;
        }

        permitted = this.onMouse(event);

        if (!permitted) { //若是點擊是被容許的,將this.targetElement置空能夠確保onMouse事件裏不會阻止默認事件
            this.targetElement = null;
        }

        //沒有什麼意義返回這個
        return permitted;
    };

  常規須要阻斷點擊事件的操做,在touch 監聽事件回調中已經作了處理,這裏主要是針對那些 touch 過程(有些設備甚至可能並無touch事件觸發)沒有禁用默認事件的 event 作進一步處理,從而決定是否觸發原生的 click 事件(若是禁止是在 onMouse 方法裏作的處理)。

 

   總結

  新知識get:

    stopImmediatePropagation與stopPropagation區別:

    1. 他們均可以阻止事件冒泡到父元素

    2. stopImmediatePropagation多作了一件事:好比某個元素綁定多個相同類型事件監聽函數,若是執行了stopImmediatePropagation,將按照順序執行第一個事件監聽函數,其他相同類型事件監聽函數被阻止。

    zepto「點透」現象被解決是爲何?

    這一點由於我尚未去閱讀zepto的源碼,因此暫時不能解答。。等待以後再去挖掘。      

 

  第一次閱讀源碼,感受很困難,不少東西都不知道,去github上面找問題,但英語太渣,有些看不懂,連蒙帶猜加翻譯,最終仍是求助於百度和谷歌,看到了不少大神的對fastclick的分析文章,感受本身還有很遠的路要走~    

相關文章
相關標籤/搜索