摘要:回調隊列對象,用于構建易于操作的回調函數集合,在操作完成后進行執行。對象對象,用于管理回調函數的多用途列表。如果傳入一個延遲對象,則返回該對象的對象,可以繼續綁定其余回調,在執行結束狀態之后也同時調用其回調函數。
在工作中我們可能會把jQuery選擇做自己項目的基礎庫,因為其提供了簡便的DOM選擇器以及封裝了很多實用的方法,比如$.ajax(),它使得我們不用操作xhr和xdr對象,直接書寫我們的代碼邏輯即可。更為豐富的是它在ES6沒有原生支持的那段時間,提供了Deferred對象,類似于Promise對象,支持done/fail/progress/always方法和when批處理方法,這可能在項目上幫助過你。
ES6提供了Promise對象,但由于它是內置C++實現的,所以你也沒法看它的設計。不如我們通過jQuery的源碼來探究其設計思路,并比較一下兩者的區別。本文采用jquey-3.1.2.js版本,其中英文注釋為原版,中文注釋為我添加。
jQuery的ajax總體設計jQuery在內部設置了全局的ajax參數,在每一個ajax請求初始化時,用傳遞的參數與默認的全局參數進行混合,并構建一個jqXHR對象(提供比原生XHR更為豐富的方法,同時實現其原生方法),通過傳遞的參數,來判斷其是否跨域、傳遞的參數類型等,設置好相關頭部信息。同時其被初始化為一個內置Deferred對象用于異步操作(后面講到),添加done/fail方法作為回調。同時我們也封裝了$.get/$.post方法來快捷調用$.ajax方法。
上面提到的Deferred對象,與ES6的Promise對象類似,用于更為方便的異步操作,多種回調以及更好的書寫方式。提供progress/fail/done方法,并分別用該對象的notify/reject/resolve方法觸發,可以使用then方法快速設置三個方法,使用always添加都會執行的回調,并且提供when方法支持多個異步操作合并回調。可以追加不同的回調列表,其回調列表是使用內部Callbacks對象,更方便的按照隊列的方式來進行執行。
Callbacks回調隊列對象,用于構建易于操作的回調函數集合,在操作完成后進行執行。支持四種初始化的方式once/unique/memory/stopOnFalse,分別代表只執行依次、去重、緩存結果、鏈式調用支持終止。提供fired/locked/disabled狀態值,代表是否執行過、上鎖、禁用。提供add/remove/empty/fire/lock/disable方法操作回調函數隊列。
主要涉及到的概念就是這三個,不再做延伸,三個對象的設計代碼行數在1200行左右,斷斷續續看了我一周 (′?`」 ∠) 。我們從這三個倒序開始入手剖析其設計。
jQuery.Callbacks對象Callbacks對象,用于管理回調函數的多用途列表。它提供了六個主要方法:
add: 向列表中添加回調函數
remove: 移除列表中的回調函數
empty: 清空列表中的回調函數
fire: 依次執行列表中的回調函數
lock: 對列表上鎖,禁止一切操作,清除數據,但保留緩存的環境變量(只在memory參數時有用)
disable: 禁用該回調列表,所有數據清空
在初始化時,支持四個參數,用空格分割:
once: 該回調列表只執行依次
memory: 緩存執行環境,在添加新回調時執行先執行一次
unique: 去重,每一個函數均不同(指的是引用地址)
stopOnFalse: 在調用中,如果前一個函數返回false,中斷列表的后續執行
我們來看下其實例使用:
let cl = $.Callbacks("once memory unique stopOnFalse"); fn1 = function (data) { console.log(data); }; fn2 = function (data) { console.log("fn2 say:", data); return false; }; cl.add(fn1); cl.fire("Nicholas"); // Nicholas // 由于我們使用memory參數,保存了執行環境,在添加新的函數時自動執行一次 cl.add(fn2); // fn2 say: Nicholas // 由于我們使用once參數,所以只能執行(fire)一次,此處無任何輸出 cl.fire("Lee"); // 后面我們假設這里沒有傳入once參數,每次fire都可以執行 cl.fire("Lee"); // Lee fn2 say: Lee // 清空列表 cl.empty(); cl.add(fn2, fn1); // 由于我們設置了stopOnFalse,而fn2返回了false,則后添加的fn1不會執行 cl.fire("Nicholas"); // fn2 say: Nicholas // 上鎖cl,禁用其操作,清除數據,但是我們添加了memory參數,它依然會對后續添加的執行一次 cl.lock(); // 無響應 cl.fire(); cl.add(fn2); // fn2 say: Nicholas // 禁用cl,禁止一切操作,清除數據 cl.disable();
除了上面所說的主要功能,還提供has/locked/disabled/fireWith/fired等輔助函數。
其所有源碼實現及注釋為:
jQuery.Callbacks = function( options ) { options = typeof options === "string" ? // 將字符串中空格分割的子串,轉換為值全為true的對象屬性 createOptions( options ) : jQuery.extend( {}, options ); var // Flag to know if list is currently firing firing, // Last fire value for non-forgettable lists memory, // Flag to know if list was already fired fired, // Flag to prevent firing locked, // Actual callback list list = [], // Queue of execution data for repeatable lists queue = [], // Index of currently firing callback (modified by add/remove as needed) firingIndex = -1, // Fire callbacks fire = function() { // Enforce single-firing locked = locked || options.once; // Execute callbacks for all pending executions, // respecting firingIndex overrides and runtime changes fired = firing = true; // 為quene隊列中不同的[context, args]執行list回調列表,執行過程中會判斷stopOnFalse中間中斷 for ( ; queue.length; firingIndex = -1 ) { memory = queue.shift(); while ( ++firingIndex < list.length ) { // Run callback and check for early termination if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false && options.stopOnFalse ) { // Jump to end and forget the data so .add doesn"t re-fire firingIndex = list.length; memory = false; } } } // Forget the data if we"re done with it if ( !options.memory ) { memory = false; } firing = false; // Clean up if we"re done firing for good // 如果不再執行了,就將保存回調的list清空,對內存更好 if ( locked ) { // Keep an empty list if we have data for future add calls if ( memory ) { list = []; // Otherwise, this object is spent } else { list = ""; } } }, // Actual Callbacks object self = { // Add a callback or a collection of callbacks to the list add: function() { if ( list ) { // If we have memory from a past run, we should fire after adding // 如果我們選擇緩存執行環境,會在新添加回調時執行一次保存的環境 if ( memory && !firing ) { firingIndex = list.length - 1; queue.push( memory ); } ( function add( args ) { jQuery.each( args, function( _, arg ) { // 如果是函數,則判斷是否去重,如果為類數組,則遞歸執行該內部函數 if ( jQuery.isFunction( arg ) ) { if ( !options.unique || !self.has( arg ) ) { list.push( arg ); } } else if ( arg && arg.length && jQuery.type( arg ) !== "string" ) { // Inspect recursively add( arg ); } } ); } )( arguments ); if ( memory && !firing ) { fire(); } } return this; }, // Remove a callback from the list // 移除所有的相同回調,并同步將firingIndex-1 remove: function() { jQuery.each( arguments, function( _, arg ) { var index; while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { list.splice( index, 1 ); // Handle firing indexes if ( index <= firingIndex ) { firingIndex--; } } } ); return this; }, // Check if a given callback is in the list. // If no argument is given, return whether or not list has callbacks attached. // 檢查是否存在該函數,如果不傳遞參數,則返回是否有回調函數 has: function( fn ) { return fn ? jQuery.inArray( fn, list ) > -1 : list.length > 0; }, // Remove all callbacks from the list empty: function() { if ( list ) { list = []; } return this; }, // Disable .fire and .add // Abort any current/pending executions // Clear all callbacks and values // 置locked為[],即!![] === true,同時將隊列和列表都清空,即禁用了該回調集合 disable: function() { locked = queue = []; list = memory = ""; return this; }, disabled: function() { return !list; }, // Disable .fire // Also disable .add unless we have memory (since it would have no effect) // Abort any pending executions // 不允許執行,但如果有緩存,則我們允許添加后在緩存的環境下執行新添加的回調 lock: function() { locked = queue = []; if ( !memory && !firing ) { list = memory = ""; } return this; }, locked: function() { return !!locked; }, // Call all callbacks with the given context and arguments // 為fire附帶了一個上下文來調用fire函數, fireWith: function( context, args ) { if ( !locked ) { args = args || []; args = [ context, args.slice ? args.slice() : args ]; queue.push( args ); if ( !firing ) { fire(); } } return this; }, // Call all the callbacks with the given arguments fire: function() { self.fireWith( this, arguments ); return this; }, // To know if the callbacks have already been called at least once fired: function() { return !!fired; } }; return self; };jQuery.Deferred對象
jQuery.Deferred對象是一個工廠函數,返回一個用于異步或同步調用的deferred對象,支持鏈式調用、回調函數隊列,并且能針對返回的狀態不同執行不同的回調。它類似于ES6提供的Promise對象,提供9個主要的方法:
done: 操作成功響應時的回調函數(同步或異步,以下相同)
fail: 操作失敗響應時的回調函數
progress: 操作處理過程中的回調函數
resolve: 通過該方法解析該操作為成功狀態,調用done
reject: 通過該方法解析該操作為失敗狀態,調用fail
notify: 通過該方法解析該操作為執行過程中,調用progress
then: 設置回調的簡寫,接收三個參數,分別是done/fail/progress
always: 設置必須執行的回調,無論是done還是fail
promise: 返回一個受限制的Deferred對象,不允許外部直接改變完成狀態
它的實現思想是創建一個對象,包含不同狀態下回調函數的隊列,并在狀態為失敗或成功后不允許再次改變。通過返回的Deferred對象進行手動調用resolve/reject/notify方法來控制流程。
看一個實例(純屬胡扯,不要當真)。我們需要從間諜衛星返回的數據用不同的算法來進行解析,如果解析結果信號強度大于90%,則證明該數據有效,可以被解析;如果強度小于10%,則證明只是宇宙噪音;否則,證明數據可能有效,換一種算法解析:
// 我們封裝Deferred產生一個promise對象,其不能被外部手動解析,只能內部確定最終狀態 asynPromise = function () { let d = $.Deferred(); (function timer() { setTimeout(function () { // 產生隨機數,代替解析結果,來確定本次的狀態 let num = Math.random(); if (num > 0.9) { d.resolve(); // 解析成功 } else if (num < 0.1) { d.reject(); // 解析失敗 } else { d.notify(); // 解析過程中 } setTimeout(timer, 1000); // 持續不斷的解析數據 }, 1000); })(); // 如果不返回promise對象,則可以被外部手動調整解析狀態 return d.promise(); }; // then方法的三個參數分別代表完成、失敗、過程中的回調函數 asynPromise().then(function () { console.log("resolve success"); }, function () { console.log("reject fail"); }, function () { console.log("notify progress"); }); // 本地執行結果(每個人的不一樣,隨機分布,但最后一個一定是success或fail) notify progress notify progress notify progress notify progress notify progress reject fail // 后面不會再有輸出,因為一旦解析狀態為success或fail,則不會再改變
除了上面的主要功能,還提供了notifyWith/resolveWith/rejectWith/state輔助方法。
其所有的源碼實現和注釋為:
Deferred: function( func ) { var tuples = [ // action, add listener, callbacks, // ... .then handlers, argument index, [final state] // 用于后面進行第一個參數綁定調用第二個參數,第三個和第四個參數分別是其不同的回調函數隊列 [ "notify", "progress", jQuery.Callbacks( "memory" ), jQuery.Callbacks( "memory" ), 2 ], [ "resolve", "done", jQuery.Callbacks( "once memory" ), jQuery.Callbacks( "once memory" ), 0, "resolved" ], [ "reject", "fail", jQuery.Callbacks( "once memory" ), jQuery.Callbacks( "once memory" ), 1, "rejected" ] ], state = "pending", promise = { state: function() { return state; }, // 同時添加done和fail句柄 always: function() { deferred.done( arguments ).fail( arguments ); return this; }, "catch": function( fn ) { return promise.then( null, fn ); }, then: function( onFulfilled, onRejected, onProgress ) { var maxDepth = 0; function resolve( depth, deferred, handler, special ) { return function() { var that = this, args = arguments, mightThrow = function() { var returned, then; // Support: Promises/A+ section 2.3.3.3.3 // https://promisesaplus.com/#point-59 // Ignore double-resolution attempts if ( depth < maxDepth ) { return; } returned = handler.apply( that, args ); // Support: Promises/A+ section 2.3.1 // https://promisesaplus.com/#point-48 if ( returned === deferred.promise() ) { throw new TypeError( "Thenable self-resolution" ); } // Support: Promises/A+ sections 2.3.3.1, 3.5 // https://promisesaplus.com/#point-54 // https://promisesaplus.com/#point-75 // Retrieve `then` only once then = returned && // Support: Promises/A+ section 2.3.4 // https://promisesaplus.com/#point-64 // Only check objects and functions for thenability ( typeof returned === "object" || typeof returned === "function" ) && returned.then; // Handle a returned thenable if ( jQuery.isFunction( then ) ) { // Special processors (notify) just wait for resolution if ( special ) { then.call( returned, resolve( maxDepth, deferred, Identity, special ), resolve( maxDepth, deferred, Thrower, special ) ); // Normal processors (resolve) also hook into progress } else { // ...and disregard older resolution values maxDepth++; then.call( returned, resolve( maxDepth, deferred, Identity, special ), resolve( maxDepth, deferred, Thrower, special ), resolve( maxDepth, deferred, Identity, deferred.notifyWith ) ); } // Handle all other returned values } else { // Only substitute handlers pass on context // and multiple values (non-spec behavior) if ( handler !== Identity ) { that = undefined; args = [ returned ]; } // Process the value(s) // Default process is resolve ( special || deferred.resolveWith )( that, args ); } }, // Only normal processors (resolve) catch and reject exceptions // 只有普通的process能處理異常,其余的要進行捕獲,這里不是特別明白,應該是因為沒有改最終的狀態吧 process = special ? mightThrow : function() { try { mightThrow(); } catch ( e ) { if ( jQuery.Deferred.exceptionHook ) { jQuery.Deferred.exceptionHook( e, process.stackTrace ); } // Support: Promises/A+ section 2.3.3.3.4.1 // https://promisesaplus.com/#point-61 // Ignore post-resolution exceptions if ( depth + 1 >= maxDepth ) { // Only substitute handlers pass on context // and multiple values (non-spec behavior) if ( handler !== Thrower ) { that = undefined; args = [ e ]; } deferred.rejectWith( that, args ); } } }; // Support: Promises/A+ section 2.3.3.3.1 // https://promisesaplus.com/#point-57 // Re-resolve promises immediately to dodge false rejection from // subsequent errors if ( depth ) { process(); } else { // Call an optional hook to record the stack, in case of exception // since it"s otherwise lost when execution goes async if ( jQuery.Deferred.getStackHook ) { process.stackTrace = jQuery.Deferred.getStackHook(); } window.setTimeout( process ); } }; } return jQuery.Deferred( function( newDefer ) { // progress_handlers.add( ... ) tuples[ 0 ][ 3 ].add( resolve( 0, newDefer, jQuery.isFunction( onProgress ) ? onProgress : Identity, newDefer.notifyWith ) ); // fulfilled_handlers.add( ... ) tuples[ 1 ][ 3 ].add( resolve( 0, newDefer, jQuery.isFunction( onFulfilled ) ? onFulfilled : Identity ) ); // rejected_handlers.add( ... ) tuples[ 2 ][ 3 ].add( resolve( 0, newDefer, jQuery.isFunction( onRejected ) ? onRejected : Thrower ) ); } ).promise(); }, // Get a promise for this deferred // If obj is provided, the promise aspect is added to the object // 通過該promise對象返回一個新的擴展promise對象或自身 promise: function( obj ) { return obj != null ? jQuery.extend( obj, promise ) : promise; } }, deferred = {}; // Add list-specific methods // 給promise添加done/fail/progress事件,并添加互相的影響關系,并為deferred對象添加3個事件函數notify/resolve/reject jQuery.each( tuples, function( i, tuple ) { var list = tuple[ 2 ], stateString = tuple[ 5 ]; // promise.progress = list.add // promise.done = list.add // promise.fail = list.add promise[ tuple[ 1 ] ] = list.add; // Handle state // 只有done和fail有resolved和rejected狀態字段,給兩個事件添加回調,禁止再次done或者fail,鎖住progress不允許執行回調 if ( stateString ) { list.add( function() { // state = "resolved" (i.e., fulfilled) // state = "rejected" state = stateString; }, // rejected_callbacks.disable // fulfilled_callbacks.disable tuples[ 3 - i ][ 2 ].disable, // progress_callbacks.lock tuples[ 0 ][ 2 ].lock ); } // progress_handlers.fire // fulfilled_handlers.fire // rejected_handlers.fire // 執行第二個回調列表 list.add( tuple[ 3 ].fire ); // deferred.notify = function() { deferred.notifyWith(...) } // deferred.resolve = function() { deferred.resolveWith(...) } // deferred.reject = function() { deferred.rejectWith(...) } // 綁定notify/resolve/reject的事件,實際執行的函數體為加入上下文的With函數 deferred[ tuple[ 0 ] ] = function() { deferred[ tuple[ 0 ] + "With" ]( this === deferred ? undefined : this, arguments ); return this; }; // deferred.notifyWith = list.fireWith // deferred.resolveWith = list.fireWith // deferred.rejectWith = list.fireWith deferred[ tuple[ 0 ] + "With" ] = list.fireWith; } ); // Make the deferred a promise // 將deferred擴展為一個promise對象 promise.promise( deferred ); // Call given func if any // 在創建前執行傳入的回調函數進行修改 if ( func ) { func.call( deferred, deferred ); } // All done! return deferred; },jQuery.when方法
$.when()提供一種方法執行一個或多個函數的回調函數。如果傳入一個延遲對象,則返回該對象的Promise對象,可以繼續綁定其余回調,在執行結束狀態之后也同時調用其when回調函數。如果傳入多個延遲對象,則返回一個新的master延遲對象,跟蹤所有的聚集狀態,如果都成功解析完成,才調用其when回調函數;如果有一個失敗,則全部失敗,執行錯誤回調。
其使用方法:
$.when($.ajax("/page1.php"), $.ajax("/page2.php")) .then(myFunc, myFailure);
其所有源碼實現和注釋為(能力有限,有些地方實在不能準確理解執行流程):
// 給when傳遞的對象綁定master.resolve和master.reject,用于聚集多異步對象的狀態 function adoptValue( value, resolve, reject, noValue ) { var method; try { // Check for promise aspect first to privilege synchronous behavior // 如果when傳入的參數promise方法可用,則封裝promise并添加done和fail方法調用resolve和reject if ( value && jQuery.isFunction( ( method = value.promise ) ) ) { method.call( value ).done( resolve ).fail( reject ); // Other thenables // 否則,就判斷傳入參數的then方法是否可用,如果可用就傳入resolve和reject方法 } else if ( value && jQuery.isFunction( ( method = value.then ) ) ) { method.call( value, resolve, reject ); // Other non-thenables // 如果均不可用,則為非異步對象,直接resolve解析原值 } else { // Control `resolve` arguments by letting Array#slice cast boolean `noValue` to integer: // * false: [ value ].slice( 0 ) => resolve( value ) // * true: [ value ].slice( 1 ) => resolve() resolve.apply( undefined, [ value ].slice( noValue ) ); } // For Promises/A+, convert exceptions into rejections // Since jQuery.when doesn"t unwrap thenables, we can skip the extra checks appearing in // Deferred#then to conditionally suppress rejection. } catch ( value ) { // Support: Android 4.0 only // Strict mode functions invoked without .call/.apply get global-object context // 一個安卓4.0的bug,這里不做闡釋 reject.apply( undefined, [ value ] ); } } // Deferred helper when: function( singleValue ) { var // count of uncompleted subordinates remaining = arguments.length, // count of unprocessed arguments i = remaining, // subordinate fulfillment data resolveContexts = Array( i ), resolveValues = slice.call( arguments ), // the master Deferred master = jQuery.Deferred(), // subordinate callback factory // 將每一個響應的環境和值都保存到列表里,在全部完成后統一傳給主Promise用于執行 updateFunc = function( i ) { return function( value ) { resolveContexts[ i ] = this; resolveValues[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; if ( !( --remaining ) ) { master.resolveWith( resolveContexts, resolveValues ); } }; }; // Single- and empty arguments are adopted like Promise.resolve // 如果只有一個參數,則直接將其作為master的回調 if ( remaining <= 1 ) { adoptValue( singleValue, master.done( updateFunc( i ) ).resolve, master.reject, !remaining ); // Use .then() to unwrap secondary thenables (cf. gh-3000) if ( master.state() === "pending" || jQuery.isFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) { return master.then(); } } // Multiple arguments are aggregated like Promise.all array elements // 多參數時,進行所有參數的解析狀態聚合到master上 while ( i-- ) { adoptValue( resolveValues[ i ], updateFunc( i ), master.reject ); } return master.promise(); }后續
本來想把jQuery.Deferred和jQuery.ajax以及ES6的Promise對象給統一講一下,結果發現牽涉的東西太多,每一個都可以多帶帶寫一篇文章,怕大家說太長不看,這里先寫第一部分jQuery.Deferred吧,后續再補充另外兩篇。
看jQuery的文檔很容易,使用也很方便,但其實真正想要講好很復雜,更不要說寫篇源碼分析文章了。真的是努力理解設計者的思路,爭取每行都能理解邊界條件,但踩坑太少,應用場景太少,確實有很大的疏漏,希望大家能夠理解,不要偏聽一面之詞。
參考資料jQuery - Callbacks: http://api.jquery.com/jQuery....
segment - jQuery Callbacks: https://segmentfault.com/a/11...
jQuery-3.2.1版本
jQuery - Deferred: http://api.jquery.com/jQuery....
jQuery - when: http://www.jquery123.com/jQue...
cnblogs - 搞懂jQuery的Promise: http://www.cnblogs.com/lvdaba...
Promise A+ 規范: http://malcolmyu.github.io/ma...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/89269.html
摘要:通常的做法是,為它們指定回調函數。請求返回請求返回請求返回異步隊列解耦異步任務和回調函數為模塊隊列模塊事件提供基礎功能。 前言 jQuery整體框架甚是復雜,也不易讀懂,這幾日一直在研究這個笨重而強大的框架。jQuery的總體架構可以分為:入口模塊、底層模塊和功能模塊。這里,我們以jquery-1.7.1為例進行分析。 jquery的總體架構 16 (function( window,...
摘要:在服務器端,異步模式甚至是唯一的模式,因為執行環境是單線程的,如果允許同步執行所有請求,服務器性能會急劇下降,很快就會失去響應。第三是,捕捉不到他的錯誤異步編程方法回調函數這是異步編程最基本的方法。 前言 你可能知道,Javascript語言的執行環境是單線程(single thread)。所謂單線程,就是指一次只能完成一件任務。如果有多個任務,就必須排隊,前面一個任務完成,再執行后面...
摘要:上篇文章中講到,使用的方法操作請求,會受到回調函數嵌套的問題。第一次回調第二次回調內部實現上,和都是基于實現的對于多個同時請求,共同執行同一個回調函數這一點上,有一個方法,接受多個對象實例,同時執行。 上篇文章中講到,使用jquery的ajax方法操作ajax請求,會受到回調函數嵌套的問題。當然,jquery團隊也發現了這個問題,在2011年,也就是jquery 1.5版本之后,jQu...
摘要:根據項目選型決定是否開啟。為了壓縮,可維護為了支持從而使用代替變量存儲防沖突會用到,形如版本號聲明最終調用的是這個原型實際上。功能檢測統一兼容性問題。 概覽 (function (){ (21 , 94) 定義了一些變量和函數 jQuery=function(); (96 , 293) 給jQuery對象添加一些方法和屬性; (285 , 347) ...
摘要:根據項目選型決定是否開啟。為了壓縮,可維護為了支持從而使用代替變量存儲防沖突會用到,形如版本號聲明最終調用的是這個原型實際上。功能檢測統一兼容性問題。 概覽 (function (){ (21 , 94) 定義了一些變量和函數 jQuery=function(); (96 , 293) 給jQuery對象添加一些方法和屬性; (285 , 347) ...
閱讀 3486·2021-11-12 10:36
閱讀 2857·2021-09-22 15:35
閱讀 2807·2021-09-04 16:41
閱讀 1164·2019-08-30 15:55
閱讀 3574·2019-08-29 18:43
閱讀 2070·2019-08-23 18:24
閱讀 1412·2019-08-23 18:10
閱讀 1922·2019-08-23 11:31