Jquery源碼解析(二)

今天我們帶來jquery源碼分析第二期收班,可能是最后一期喲,具體看以后的發(fā)現(xiàn)吧,話不多說直接上代碼。

1.節(jié)點查詢

先看一個函數(shù)

jquery.extend({
map: function( elems, callback, arg ) {
        var length, value,
            i = 0,
            ret = [];
                //判斷是否是類數(shù)組對象,dom節(jié)點是一種類數(shù)組對象
        if ( isArrayLike( elems ) ) {
            length = elems.length;
            for ( ; i < length; i++ ) {
                value = callback( elems[ i ], i, arg );

                if ( value != null ) {
                    ret.push( value );
                }
            }

        } else {
            for ( i in elems ) {
                value = callback( elems[ i ], i, arg );

                if ( value != null ) {
                    ret.push( value );
                }
            }
        }
                //把數(shù)組扁平化
        return concat.apply( [], ret );
    }
})
數(shù)組扁平化:
let aa = [[1,2,3,4],[2,3,4,5]]
let bb = [].concat(aa)
bb: [1,2,3,4,2,3,4,5]

前面是預(yù)熱,接下來看這個好吧

jQuery.each( {
    parent: function( elem ) {
        var parent = elem.parentNode;
        return parent && parent.nodeType !== 11 ? parent : null;
    },
    parents: function( elem ) {
        return dir( elem, "parentNode" );
    },
    parentsUntil: function( elem, i, until ) {
        return dir( elem, "parentNode", until );
    },
    next: function( elem ) {
        return sibling( elem, "nextSibling" );
    },
    prev: function( elem ) {
        return sibling( elem, "previousSibling" );
    },
    nextAll: function( elem ) {
        return dir( elem, "nextSibling" );
    },
    nextUntil: function( elem, i, until ) {
        return dir( elem, "nextSibling", until );
    },
    siblings: function( elem ) {
        return siblings( ( elem.parentNode || {} ).firstChild, elem );
    },
    children: function( elem ) {
        return siblings( elem.firstChild );
    }
 }

        // Support: IE 9 - 11 only, iOS 7 only, Android Browser <=4.3 only
        // Treat the template element as a regular one in browsers that
        // don't support it.
        if ( nodeName( elem, "template" ) ) {
            elem = elem.content || elem;
        }

        return jQuery.merge( [], elem.childNodes );
    }
}, function( name, fn ) {
    jQuery.fn[ name ] = function( until, selector ) {
        var matched = jQuery.map( this, fn, until );

        if ( name.slice( -5 ) !== "Until" ) {
            selector = until;
        }

        if ( selector && typeof selector === "string" ) {
            matched = jQuery.filter( selector, matched );
        }

        if ( this.length > 1 ) {

            // Remove duplicates
            if ( !guaranteedUnique[ name ] ) {
                jQuery.uniqueSort( matched );
            }

            // Reverse order for parents* and prev-derivatives
            if ( rparentsprev.test( name ) ) {
                matched.reverse();
            }
        }

        return this.pushStack( matched );
    };
} );

以上代碼是什么意思呢砰蠢,我來稀釋一下赵誓。。。

jQuery.fn.parents = function(utill,selector){}
jQuery.fn.parentsUtil = function(utill,selector){}
jQuery.fn.nextUtil = function(utill,selector){}
等等

var matched = jQuery.map( this, fn, until );
這個時候用了map函數(shù)椿肩,假設(shè)此時我們調(diào)用$('#qwe').nextUtil('.aa','.bb'),那么情況發(fā)展如下:
1. jQuery.map($('#qwe'),nextUtil,'.aa')
2. $('#qwe')是類數(shù)組元素茬末,只有一個。會執(zhí)行nextUtil($('#qwe'),0,'.aa')
3.nextUtil返回的是 dir($('#qwe'),nextSbling,'.aa')
4.我們來看看dir函數(shù)

var dir = function( elem, dir, until ) {
    var matched = [],
        truncate = until !== undefined;
        // 這里是是查找#qwe的同輩 .aa節(jié)點
    while ( ( elem = elem[ dir ] ) && elem.nodeType !== 9 ) {
        if ( elem.nodeType === 1 ) {
            // 如果碰到了util也就是元素自身,就break
            if ( truncate && jQuery( elem ).is( until ) ) {
                break;
            }
            matched.push( elem );
        }
    }
    return matched;
};
注意上面是找出了所有#qwe的同輩.aa元素,但我們想在他碰到.bb就停止萄唇,所以有這樣一段代碼:
if ( selector && typeof selector === "string" ) {
            matched = jQuery.filter( selector, matched );
        }


我們上面是特殊例子,若是$('.qwe')在頁面中有多個,則會走完整個for循環(huán)。

我么來看看 jQuery.filter( selector, matched )是如何寫的

jQuery.filter = function( expr, elems, not ) {
    var elem = elems[ 0 ];
    // 過濾不符合expr的
    if ( not ) {
        expr = ":not(" + expr + ")";
    }

    if ( elems.length === 1 && elem.nodeType === 1 ) {
        return jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [];
    }

    return jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) {
        return elem.nodeType === 1;
    } ) );
};

grep: function( elems, callback, invert ) {
        var callbackInverse,
            matches = [],
            i = 0,
            length = elems.length,
            callbackExpect = !invert;

        for ( ; i < length; i++ ) {
            callbackInverse = !callback( elems[ i ], i );
            if ( callbackInverse !== callbackExpect ) {
                matches.push( elems[ i ] );
            }
        }

        return matches;
    }

grep是一個過濾函數(shù),invert如果為false,則與callback返回的之相反的元素進(jìn)去數(shù)組。
jQuery.find.matches大家可以到源碼去查看一下,這里不具體講了移国。

我們來看看jquery里面很重要的一個函數(shù)pushStack

pushStack: function( elems ) {

        // Build a new jQuery matched element set
        var ret = jQuery.merge( this.constructor(), elems );

        // Add the old object onto the stack (as a reference)
        ret.prevObject = this;

        // Return the newly-formed element set
        return ret;
    }
我們發(fā)現(xiàn)在上面任意一個節(jié)點查詢的函數(shù)最后都會有一個,this.pushStack(matched),把查找出來的元素放進(jìn)去。
---html代碼---
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<div class="aa"></div>
<div class="bb"></div>
<div class="bb ww"></div>
<div class="bb"></div>
<div class="cc"></div>

<script src="jquery-3.2.1.js"></script>
<script>

console.log($('.aa').nextUntil('.cc'))
</script>
</body>
</html>
2017-07-27 11-54-08屏幕截圖.png

相信大家看到了打印結(jié)果就很明白了颅和,我們做完節(jié)點操作后都會的到一個新的dom對象,我們想回到之前操作的元素可以怎么辦呢响鹃?

考慮下面代碼:
$('.aa').nextUntil('.cc').find('.ww') 此時我們dom對象停在了$('.ww'),如果我們想回到$('.aa')怎么辦

end: function() {
        return this.prevObject || this.constructor();
    }, 
源碼里面有這樣一段,返回上次操作的dom元素,使用end方法。

$.fn.grandparent = function() { 
var els = this.parent().parent(); 
return this.pushStack(els.get()); 
}; 
$('.child').grandparent().end()  進(jìn)行了2次查找,但一次end就可以返回最初元素秦踪。

關(guān)于節(jié)點我們再來介紹一個addback方法,結(jié)果顯然易見景馁。

<html>
<head>
  <style>
p, div { margin:5px; padding:5px; }
.border { border: 2px solid red; }
.background { background:yellow; }
.left, .right { width: 45%; float: left;}
.right { margin-left:3%; }
    </style>
  <script src="http://code.jquery.com/jquery-latest.js"></script>
</head>
<body>
 
<div class="left">
  <p><strong>Before <code>addBack()</code></strong></p>
  <div class="before-addback">
    <p>First Paragraph</p>
    <p>Second Paragraph</p>
  </div>
</div>
<div class="right">
  <p><strong>After <code>addBack()</code></strong></p>
  <div class="after-addback">
    <p>First Paragraph</p>
    <p>Second Paragraph</p>
  </div>
</div>
 
<script>
$("div.left, div.right").find("div, div > p").addClass("border");
 
// First Example
$("div.before-addback").find("p").addClass("background");
 
// Second Example
$("div.after-addback").find("p").addBack().addClass("background");
</script>
 
</body>
</html>

2. 元素操作

相信下面一段代碼大家會經(jīng)常用到,控制顯示和隱藏

jQuery.fn.extend( {
    show: function() {
        return showHide( this, true );
    },
    hide: function() {
        return showHide( this );
    },
    toggle: function( state ) {
        if ( typeof state === "boolean" ) {
            return state ? this.show() : this.hide();
        }

        return this.each( function() {
            if ( isHiddenWithinTree( this ) ) {
                jQuery( this ).show();
            } else {
                jQuery( this ).hide();
            }
        } );
    }
} );

原本簡單的功能其實在源碼里面也有復(fù)雜的判斷,來看showHide函數(shù)

function showHide( elements, show ) {
    var display, elem,
        values = [],
        index = 0,
        length = elements.length;

    // Determine new display value for elements that need to change
    for ( ; index < length; index++ ) {
        elem = elements[ index ];
        if ( !elem.style ) {
            continue;
        }

        display = elem.style.display;
        if ( show ) {

            // Since we force visibility upon cascade-hidden elements, an immediate (and slow)
            // check is required in this first loop unless we have a nonempty display value (either
            // inline or about-to-be-restored)
            if ( display === "none" ) {
                values[ index ] = dataPriv.get( elem, "display" ) || null;
                if ( !values[ index ] ) {
                    elem.style.display = "";
                }
            }
            if ( elem.style.display === "" && isHiddenWithinTree( elem ) ) {
                values[ index ] = getDefaultDisplay( elem );
            }
        } else {
            if ( display !== "none" ) {
                values[ index ] = "none";

                // Remember what we're overwriting
                dataPriv.set( elem, "display", display );
            }
        }
    }

    // Set the display of the elements in a second loop to avoid constant reflow
    for ( index = 0; index < length; index++ ) {
        if ( values[ index ] != null ) {
            elements[ index ].style.display = values[ index ];
        }
    }

    return elements;
}
showHide如果跟的參數(shù)是true蹄胰,就代表是顯示,dataPriv.set( elem, "display", display )是用來干嘛的呢妻往,記錄元素上一次的display屬性

有一個data的原型函數(shù)如下:

function Data() {
    this.expando = jQuery.expando + Data.uid++;
}

Data.uid = 1;

Data.prototype = {

    cache: function( owner ) {

        
        var value = owner[ this.expando ];

        if ( !value ) {
            value = {};
                if ( acceptData( owner ) ) {

                if ( owner.nodeType ) {
                    owner[ this.expando ] = value;

                    } else {
                    Object.defineProperty( owner, this.expando, {
                        value: value,
                        configurable: true
                    } );
                }
            }
        }
               return value;
    },
    set: function( owner, data, value ) {
        var prop,
            cache = this.cache( owner );

        // Handle: [ owner, key, value ] args
        // Always use camelCase key (gh-2257)
        if ( typeof data === "string" ) {
            cache[ jQuery.camelCase( data ) ] = value;

        // Handle: [ owner, { properties } ] args
        } else {

            // Copy the properties one-by-one to the cache object
            for ( prop in data ) {
                cache[ jQuery.camelCase( prop ) ] = data[ prop ];
            }
        }
        return cache;
    },
    get: function( owner, key ) {
        return key === undefined ?
            this.cache( owner ) :

            owner[ this.expando ] && owner[ this.expando ][ jQuery.camelCase( key ) ];
    }

其實就是一段數(shù)據(jù)儲存功能的函數(shù)昨稼,都有cache而已。咱們接著往下看

jQuery.fn.extend( {
    detach: function( selector ) {
        return remove( this, selector, true );
    },

    remove: function( selector ) {
        return remove( this, selector );
    },

    text: function( value ) {
        return access( this, function( value ) {
            return value === undefined ?
                jQuery.text( this ) :
                this.empty().each( function() {
                    if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
                        this.textContent = value;
                    }
                } );
        }, null, value, arguments.length );
    },

    append: function() {
        return domManip( this, arguments, function( elem ) {
            if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
                var target = manipulationTarget( this, elem );
                target.appendChild( elem );
            }
        } );
    },

    prepend: function() {
        return domManip( this, arguments, function( elem ) {
            if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
                var target = manipulationTarget( this, elem );
                target.insertBefore( elem, target.firstChild );
            }
        } );
    },

    before: function() {
        return domManip( this, arguments, function( elem ) {
            if ( this.parentNode ) {
                this.parentNode.insertBefore( elem, this );
            }
        } );
    },

    after: function() {
        return domManip( this, arguments, function( elem ) {
            if ( this.parentNode ) {
                this.parentNode.insertBefore( elem, this.nextSibling );
            }
        } );
    },

    empty: function() {
        var elem,
            i = 0;

        for ( ; ( elem = this[ i ] ) != null; i++ ) {
            if ( elem.nodeType === 1 ) {

                // Prevent memory leaks
                jQuery.cleanData( getAll( elem, false ) );

                // Remove any remaining nodes
                elem.textContent = "";
            }
        }

        return this;
    },

我們看到了一堆耳熟能詳?shù)膉query方法,我們來看看是如何定義的烤芦。

detach: function( selector ) {
        return remove( this, selector, true );
    },
刪除一個元素并將他的返回值保留,以后可能會再加上去。

function remove( elem, selector, keepData ) {
    var node,
        nodes = selector ? jQuery.filter( selector, elem ) : elem,
        i = 0;

    for ( ; ( node = nodes[ i ] ) != null; i++ ) {
        if ( !keepData && node.nodeType === 1 ) {
            jQuery.cleanData( getAll( node ) );
        }

        if ( node.parentNode ) {
            if ( keepData && jQuery.contains( node.ownerDocument, node ) ) {
                setGlobalEval( getAll( node, "script" ) );
            }
            node.parentNode.removeChild( node );
        }
    }

    return elem;
}

detach() 會保留所有綁定的事件、附加的數(shù)據(jù)谬泌,這一點與 remove() 不同。

其實無論remove還是detach都能夠保留原始元素宴卖,如果是remove會調(diào)用cleanData方法,來看一下吧

cleanData: function( elems ) {
        var data, elem, type,
            special = jQuery.event.special,
            i = 0;

        for ( ; ( elem = elems[ i ] ) !== undefined; i++ ) {
            if ( acceptData( elem ) ) {
                if ( ( data = elem[ dataPriv.expando ] ) ) {
                    if ( data.events ) {
                        for ( type in data.events ) {
                            if ( special[ type ] ) {
                                jQuery.event.remove( elem, type );

                            // This is a shortcut to avoid jQuery.event.remove's overhead
                            } else {
                                jQuery.removeEvent( elem, type, data.handle );
                            }
                        }
                    }

                
                    elem[ dataPriv.expando ] = undefined;
                }
                if ( elem[ dataUser.expando ] ) {

                    // Support: Chrome <=35 - 45+
                    // Assign undefined instead of using delete, see Data#remove
                    elem[ dataUser.expando ] = undefined;
                }
            }
        }
    }
我們可以看到cleanData把元素的data和event都給清除了

3. 其他技巧

jQuery.fn.extend( {
    hover: function( fnOver, fnOut ) {
        return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );
    }
} );

我們用jquery寫一個鼠標(biāo)上去個鼠標(biāo)下來事件攘烛,上面一個函數(shù)很輕松就解決了。

trigger: function( type, data ) {
        return this.each( function() {
            jQuery.event.trigger( type, data, this );
        } );
    }
我們在個一個元素綁定事件后,會用trigger來主動觸發(fā)這個綁定的事件沟突,還可以給他傳遞一些參數(shù)庸论。

下面就會看到我們經(jīng)常用的動畫函數(shù)了

jQuery.fn.extend( {
    fadeTo: function( speed, to, easing, callback ) {
          return this.filter( isHiddenWithinTree ).css( "opacity", 0 ).show()
              .end().animate( { opacity: to }, speed, easing, callback );
    },
    animate: function( prop, speed, easing, callback ) {
        var empty = jQuery.isEmptyObject( prop ),
            optall = jQuery.speed( speed, easing, callback ),
            doAnimation = function() {

                // Operate on a copy of prop so per-property easing won't be lost
                var anim = Animation( this, jQuery.extend( {}, prop ), optall );

                // Empty animations, or finishing resolves immediately
                if ( empty || dataPriv.get( this, "finish" ) ) {
                    anim.stop( true );
                }
            };
            doAnimation.finish = doAnimation;

        return empty || optall.queue === false ?
            this.each( doAnimation ) :
            this.queue( optall.queue, doAnimation );
    }

Jquery.speed是一個控制動畫速度的函數(shù)簇秒,來看看如何定義

jQuery.speed = function( speed, easing, fn ) {
    var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : {
        complete: fn || !fn && easing ||
            jQuery.isFunction( speed ) && speed,
        duration: speed,
        easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing
    };
//注意這里飄逸的js語法,一般speed給的是數(shù)字,所以opt實際是一個運動對象掐场。

    // Go to the end state if fx are off
    if ( jQuery.fx.off ) {
        opt.duration = 0;

    } else {
        if ( typeof opt.duration !== "number" ) {
            if ( opt.duration in jQuery.fx.speeds ) {
                opt.duration = jQuery.fx.speeds[ opt.duration ];

            } else {
                opt.duration = jQuery.fx.speeds._default;
            }
        }
    }

    if ( opt.queue == null || opt.queue === true ) {
        opt.queue = "fx";
    }

    // Queueing
    opt.old = opt.complete;
       //重新定義了complete函數(shù),dequeue是干嘛的呢蝗罗。
    opt.complete = function() {
        if ( jQuery.isFunction( opt.old ) ) {
            opt.old.call( this );
        }

        if ( opt.queue ) {
            jQuery.dequeue( this, opt.queue );
        }
    };

    return opt;
};

由于dequeue涉及隊列比較復(fù)雜瓶颠,自己可以去看源碼吸祟,最后來看一個很重的函數(shù)。

jQuery.fn.extend( {
    offset: function( options ) {

        // 如果options存在,則是設(shè)置他的offset
        if ( arguments.length ) {
            return options === undefined ?
                this :
                this.each( function( i ) {
                    jQuery.offset.setOffset( this, options, i );
                } );
        }

        var doc, docElem, rect, win,
            elem = this[ 0 ];

        if ( !elem ) {
            return;
        }

        if ( !elem.getClientRects().length ) {
            return { top: 0, left: 0 };
        }

        rect = elem.getBoundingClientRect();

        doc = elem.ownerDocument;
        docElem = doc.documentElement;
        win = doc.defaultView;

        return {
            top: rect.top + win.pageYOffset - docElem.clientTop,
            left: rect.left + win.pageXOffset - docElem.clientLeft
        };
    },

我們想獲得某個元素距離窗口左邊和上邊的距離可以用這個方法纤虽,解釋一下getBoundingClientRect

rectObject = object.getBoundingClientRect();
DOMRect 對象包含了一組用于描述邊框的只讀屬性——left、top杰刽、right和bottom,單位為像素。除了 width 和 height 外的屬性都是相對于視口的左上角位置而言的蔗怠。


好了,今天就先講到這里了梁丘,以后有機(jī)會在繼續(xù)把最后一期給做完吧。值漫。。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末唐全,一起剝皮案震驚了整個濱河市捌蚊,隨后出現(xiàn)的幾起案子祷愉,更是在濱河造成了極大的恐慌,老刑警劉巖订讼,帶你破解...
    沈念sama閱讀 223,126評論 6 520
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異程拭,居然都是意外死亡恃鞋,警方通過查閱死者的電腦和手機(jī)肴楷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,421評論 3 400
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人齿诉,你說我怎么就攤上這事粤剧”δィ” “怎么了?”我有些...
    開封第一講書人閱讀 169,941評論 0 366
  • 文/不壞的土叔 我叫張陵株憾,是天一觀的道長墙歪。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么脑豹? 我笑而不...
    開封第一講書人閱讀 60,294評論 1 300
  • 正文 為了忘掉前任拌牲,我火速辦了婚禮,結(jié)果婚禮上枣购,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好芹敌,可當(dāng)我...
    茶點故事閱讀 69,295評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著逞姿,像睡著了一般。 火紅的嫁衣襯著肌膚如雪续室。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,874評論 1 314
  • 那天明郭,我揣著相機(jī)與錄音薯定,去河邊找鬼瞳购。 笑死,一個胖子當(dāng)著我的面吹牛年堆,可吹牛的內(nèi)容都是我干的变丧。 我是一名探鬼主播缠捌,決...
    沈念sama閱讀 41,285評論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼曼月,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了碴萧?” 一聲冷哼從身側(cè)響起曹质,我...
    開封第一講書人閱讀 40,249評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎匀伏,沒想到半個月后蛉抓,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體笑跛,經(jīng)...
    沈念sama閱讀 46,760評論 1 321
  • 正文 獨居荒郊野嶺守林人離奇死亡伸眶,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,840評論 3 343
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了岩四。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片桶唐。...
    茶點故事閱讀 40,973評論 1 354
  • 序言:一個原本活蹦亂跳的男人離奇死亡莫鸭,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情陷虎,我是刑警寧澤伴榔,帶...
    沈念sama閱讀 36,631評論 5 351
  • 正文 年R本政府宣布兼犯,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏熔恢。R本人自食惡果不足惜闻鉴,卻給世界環(huán)境...
    茶點故事閱讀 42,315評論 3 336
  • 文/蒙蒙 一智哀、第九天 我趴在偏房一處隱蔽的房頂上張望骗爆。 院中可真熱鬧,春花似錦体箕、人聲如沸娃兽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,797評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽撞牢。三九已至蟹但,卻和暖如春十办,著一層夾襖步出監(jiān)牢的瞬間氧苍,已是汗流浹背区赵。 一陣腳步聲響...
    開封第一講書人閱讀 33,926評論 1 275
  • 我被黑心中介騙來泰國打工摔踱, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留甸赃,地道東北人襟沮。 一個月前我還...
    沈念sama閱讀 49,431評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像根穷,于是被迫代替她去往敵國和親尘惧。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,982評論 2 361

推薦閱讀更多精彩內(nèi)容