摘要:若以多線程的方式操作這些,則可能出現(xiàn)操作的沖突。另外,因?yàn)槭菃尉€程的,在某一時(shí)刻內(nèi)只能執(zhí)行特定的一個(gè)任務(wù),并且會(huì)阻塞其它任務(wù)執(zhí)行。瀏覽器事件觸發(fā)線程事件觸發(fā)線程,當(dāng)一個(gè)事件被觸發(fā)時(shí)該線程會(huì)把事件添加到任務(wù)隊(duì)列的隊(duì)尾,等待引擎的處理。
首先,說(shuō)下為什么 JavaScript 是單線程?
總所周知,JavaScript是以單線程的方式運(yùn)行的。說(shuō)到線程就自然聯(lián)想到進(jìn)程。那它們有什么聯(lián)系呢?
進(jìn)程和線程都是操作系統(tǒng)的概念。進(jìn)程是應(yīng)用程序的執(zhí)行實(shí)例,每一個(gè)進(jìn)程都是由私有的虛擬地址空間、代碼、數(shù)據(jù)和其它系統(tǒng)資源所組成;進(jìn)程在運(yùn)行過(guò)程中能夠申請(qǐng)創(chuàng)建和使用系統(tǒng)資源(如獨(dú)立的內(nèi)存區(qū)域等),這些資源也會(huì)隨著進(jìn)程的終止而被銷毀。而線程則是進(jìn)程內(nèi)的一個(gè)獨(dú)立執(zhí)行單元,在不同的線程之間是可以共享進(jìn)程資源的,所以在多線程的情況下,需要特別注意對(duì)臨界資源的訪問(wèn)控制。在系統(tǒng)創(chuàng)建進(jìn)程之后就開(kāi)始啟動(dòng)執(zhí)行進(jìn)程的主線程,而進(jìn)程的生命周期和這個(gè)主線程的生命周期一致,主線程的退出也就意味著進(jìn)程的終止和銷毀。主線程是由系統(tǒng)進(jìn)程所創(chuàng)建的,同時(shí)用戶也可以自主創(chuàng)建其它線程,這一系列的線程都會(huì)并發(fā)地運(yùn)行于同一個(gè)進(jìn)程中。
顯然,在多線程操作下可以實(shí)現(xiàn)應(yīng)用的 并行處理 ,從而以更高的CPU利用率提高整個(gè)應(yīng)用程序的性能和吞吐量。特別是現(xiàn)在很多語(yǔ)言都支持多核并行處理技術(shù),然而JavaScript卻以單線程執(zhí)行,為什么呢?
其實(shí)這與它的用途有關(guān)。作為瀏覽器腳本語(yǔ)言,JavaScript的主要用途是與用戶互動(dòng),以及操作DOM。若以多線程的方式操作這些DOM,則可能出現(xiàn)操作的沖突。假設(shè)有兩個(gè)線程同時(shí)操作一個(gè)DOM元素,線程1要求瀏覽器刪除DOM,而線程2卻要求修改DOM樣式,這時(shí)瀏覽器就無(wú)法決定采用哪個(gè)線程的操作。當(dāng)然,我們可以為瀏覽器引入“鎖”的機(jī)制來(lái)解決這些沖突,但這會(huì)大大提高復(fù)雜性,所以 JavaScript 從誕生開(kāi)始就選擇了單線程執(zhí)行。
另外,因?yàn)?JavaScript 是單線程的,在某一時(shí)刻內(nèi)只能執(zhí)行特定的一個(gè)任務(wù),并且會(huì)阻塞其它任務(wù)執(zhí)行。那么對(duì)于類似I/O等耗時(shí)的任務(wù),就沒(méi)必要等待他們執(zhí)行完后才繼續(xù)后面的操作。在這些任務(wù)完成前,JavaScript完全可以往下執(zhí)行其他操作,當(dāng)這些耗時(shí)的任務(wù)完成后則以回調(diào)的方式執(zhí)行相應(yīng)處理。這些就是JavaScript與生俱來(lái)的特性:異步與回調(diào)。
當(dāng)然對(duì)于不可避免的耗時(shí)操作(如:繁重的運(yùn)算,多重循環(huán)),HTML5提出了 Web Worker ,它會(huì)在當(dāng)前JavaScript的執(zhí)行主線程中利用Worker類新開(kāi)辟一個(gè)額外的線程來(lái)加載和運(yùn)行特定的JavaScript文件,這個(gè)新的線程和JavaScript的主線程之間并不會(huì)互相影響和阻塞執(zhí)行,而且在Web Worker中提供了這個(gè)新線程和JavaScript主線程之間數(shù)據(jù)交換的接口:postMessage和onMessage事件。但在HTML5 Web Worker中是不能操作DOM的,任何需要操作DOM的任務(wù)都需要委托給JavaScript主線程來(lái)執(zhí)行,所以雖然引入HTML5 Web Worker,但仍然沒(méi)有改線JavaScript單線程的本質(zhì)。
并發(fā)模式與Event LoopJavaScript 有個(gè)基于“Event Loop”并發(fā)的模型。
啊,并發(fā)?不是說(shuō) JavaScript是單線程嗎? 沒(méi)錯(cuò),的確是單線程,但是并發(fā)與并行是有區(qū)別的。
前者是邏輯上的同時(shí)發(fā)生,而后者是物理上的同時(shí)發(fā)生。所以,單核處理器也能實(shí)現(xiàn)并發(fā)。
并發(fā)與并行
并行大家都好理解,而 所謂“并發(fā)”是指兩個(gè)或兩個(gè)以上的事件在同一時(shí)間間隔中發(fā)生。 如上圖的第一個(gè)表,由于計(jì)算機(jī)系統(tǒng)只有一個(gè)CPU,故ABC三個(gè)程序從“微觀”上是交替使用CPU,但交替時(shí)間很短,用戶察覺(jué)不到,形成了“宏觀”意義上的并發(fā)操作。
Runtime 概念下面的內(nèi)容解釋一個(gè)理論上的模型?,F(xiàn)代 JavaScript 引擎已著重實(shí)現(xiàn)和優(yōu)化了以下所描述的幾個(gè)概念。
Stack(棧)
這里放著JavaScript正在執(zhí)行的任務(wù)。每個(gè)任務(wù)被稱為幀(stack of frames)。
function f(b){ var a = 12; return a+b+35; } function g(x){ var m = 4; return f(m*x); } g(21);
上述代碼調(diào)用 g 時(shí),創(chuàng)建棧的第一幀,該幀包含了 g 的參數(shù)和局部變量。當(dāng) g 調(diào)用 f 時(shí),第二幀就會(huì)被創(chuàng)建,并且置于第一幀之上,當(dāng)然,該幀也包含了 f 的參數(shù)和局部變量。當(dāng) f 返回時(shí),其對(duì)應(yīng)的幀就會(huì)出棧。同理,當(dāng) g 返回時(shí),棧就為空了( 棧的特定就是后進(jìn)先出 Last-in first-out (LIFO))。
Heap(堆)
一個(gè)用來(lái)表示內(nèi)存中一大片非結(jié)構(gòu)化區(qū)域的名字,對(duì)象都被分配在這。
Queue(隊(duì)列)
一個(gè) JavaScript runtime 包含了一個(gè)任務(wù)隊(duì)列,該隊(duì)列是由一系列待處理的任務(wù)組成。而每個(gè)任務(wù)都有相對(duì)應(yīng)的函數(shù)。當(dāng)棧為空時(shí),就會(huì)從任務(wù)隊(duì)列中取出一個(gè)任務(wù),并處理之。該處理會(huì)調(diào)用與該任務(wù)相關(guān)聯(lián)的一系列函數(shù)(因此會(huì)創(chuàng)建一個(gè)初始棧幀)。當(dāng)該任務(wù)處理完畢后,棧就會(huì)再次為空。 (Queue的特點(diǎn)是先進(jìn)先出 First-in First-out (FIFO))。
為了方便描述與理解,作出以下約定:
Stack棧為 主線程
Queue隊(duì)列為 任務(wù)隊(duì)列(等待調(diào)度到主線程執(zhí)行)
OK,上述知識(shí)點(diǎn)幫助我們理清了一個(gè) JavaScript runtime 的相關(guān)概念,這有助于接下來(lái)的分析。
Event Loop之所以被稱為Event loop,是因?yàn)樗砸韵骂愃品绞綄?shí)現(xiàn):
while(queue.waitForMessage()){ queue.processNextMessage(); }
正如上述所說(shuō),“任務(wù)隊(duì)列”是一個(gè)事件的隊(duì)列,如果I/O設(shè)備完成任務(wù)或用戶觸發(fā)事件(該事件指定了回調(diào)函數(shù)),那么相關(guān)事件處理函數(shù)就會(huì)進(jìn)入“任務(wù)隊(duì)列”,當(dāng)主線程空閑時(shí),就會(huì)調(diào)度“任務(wù)隊(duì)列”里第一個(gè)待處理任務(wù),(FIFO)。當(dāng)然,對(duì)于定時(shí)器,當(dāng)?shù)竭_(dá)其指定時(shí)間時(shí),才會(huì)把相應(yīng)任務(wù)插到“任務(wù)隊(duì)列”尾部。
“執(zhí)行至完成”
每當(dāng)某個(gè)任務(wù)執(zhí)行完后,其它任務(wù)才會(huì)被執(zhí)行。也就是說(shuō),當(dāng)一個(gè)函數(shù)運(yùn)行時(shí),它不能被取代且會(huì)在其它代碼運(yùn)行前先完成。
當(dāng)然,這也是Event Loop的一個(gè) 缺點(diǎn) :當(dāng)一個(gè)任務(wù)完成時(shí)間過(guò)長(zhǎng),那么應(yīng)用就不能及時(shí)處理用戶的交互(如點(diǎn)擊事件),甚至導(dǎo)致該應(yīng)用奔潰。一個(gè)比較好解決方案是:將任務(wù)完成時(shí)間縮短,或者盡可能將一個(gè)任務(wù)分成多個(gè)任務(wù)執(zhí)行。
絕不阻塞
JavaScript與其它語(yǔ)言不同,其Event Loop的一個(gè)特性是永不阻塞。I/O操作通常是通過(guò)事件和回調(diào)函數(shù)處理。所以,當(dāng)應(yīng)用等待 indexedDB 或 XHR 異步請(qǐng)求返回時(shí),其仍能處理其它操作(如用戶輸入)。
例外是存在的,如alert或者同步XHR,但避免它們被認(rèn)為是最佳實(shí)踐。注意的是,例外的例外也是存在的(但通常是實(shí)現(xiàn)錯(cuò)誤而非其它原因)。
定時(shí)器定時(shí)器的一些概念
上面也提到,在到達(dá)指定時(shí)間時(shí),定時(shí)器就會(huì)將相應(yīng)回調(diào)函數(shù)插入“任務(wù)隊(duì)列”尾部。這就是“定時(shí)器(timer)”功能。
定時(shí)器 包括setTimeout與setInterval兩個(gè)方法。它們的第二個(gè)參數(shù)是指定其回調(diào)函數(shù)推遲每隔多少毫秒數(shù)后執(zhí)行。
對(duì)于第二個(gè)參數(shù)有以下需要注意的地方:
當(dāng)?shù)诙€(gè)參數(shù)缺省時(shí),默認(rèn)為0;
當(dāng)指定的值小于4毫秒,則增加到4ms(4ms是HTML5標(biāo)準(zhǔn)指定的,對(duì)于2010年及之前的瀏覽器則是10ms);
如果你理解上述知識(shí),那么以下代碼就應(yīng)該對(duì)你沒(méi)什么問(wèn)題了:
console.log(1); setTimeout(function(){ console.log(2); },10); console.log(3); // 輸出:1 3 2
深入了解定時(shí)器 零延遲 setTimeout(func, 0)
零延遲并不是意味著回調(diào)函數(shù)立刻執(zhí)行。它取決于主線程當(dāng)前是否空閑與“任務(wù)隊(duì)列”里其前面正在等待的任務(wù)。
看看以下代碼:
(function () { console.log("this is the start"); setTimeout(function cb() { console.log("this is a msg from call back"); }); console.log("this is just a message"); setTimeout(function cb1() { console.log("this is a msg from call back1"); }, 0); console.log("this is the end"); })(); // 輸出如下: this is the start this is just a message this is the end undefined // 立即調(diào)用函數(shù)的返回值 this is a msg from callback this is a msg from a callback1
setTimeout(func, 0)的作用
讓瀏覽器渲染當(dāng)前的變化(很多瀏覽器UI render和js執(zhí)行是放在一個(gè)線程中,線程阻塞會(huì)導(dǎo)致界面無(wú)法更新渲染)
重新評(píng)估”scriptis running too long”警告
改變執(zhí)行順序
再看看以下代碼:
$("#do").on("click", function(){ $("#status").text("calculating....");// 此處會(huì)觸發(fā)redraw事件,但會(huì)放到隊(duì)列里執(zhí)行,直到long()執(zhí)行完。 // 沒(méi)設(shè)定定時(shí)器,用戶將無(wú)法看到“calculating...” long();// 執(zhí)行長(zhǎng)時(shí)間任務(wù),造成阻塞 // 設(shè)定了定時(shí)器,用戶就如期看到“calculating...” //setTimeout(long,50);// 大約50ms后,將耗時(shí)長(zhǎng)的long回調(diào)函數(shù)插入“任務(wù)隊(duì)列”末尾,根據(jù)先進(jìn)先出原則,其將在redraw之后被調(diào)度到主線程執(zhí)行 }); function long(){ var result = 0 for (var i = 0; i<1000; i++){ for (var j = 0; j<1000; j++){ for (var k = 0; k<1000; k++){ result = result + i+j+k } } } $("#status").text("calclation done"); // 在本案例中,該語(yǔ)句必須放到這里,這將使它與回調(diào)函數(shù)的行為類似 }
正版與翻版setInterval的區(qū)別
大家都可能知道通過(guò)setTimeout可以模仿setInterval的效果,下面我們看看以下代碼的區(qū)別:
// 利用setTimeout模仿setInterval setTimeout(function(){ /* 執(zhí)行一些操作. */ setTimeout(arguments.callee, 10); }, 1000); setInterval(function(){ /* 執(zhí)行一些操作 */ }, 1000);
可能你認(rèn)為這沒(méi)什么區(qū)別。的確,當(dāng)回調(diào)函數(shù)里的操作耗時(shí)很短時(shí),并不能看出它們有什么區(qū)別。
其實(shí):上面案例中的 setTimeout 總是會(huì)在其回調(diào)函數(shù)執(zhí)行后延遲 10ms(或者更多,但不可能少)再次執(zhí)行回調(diào)函數(shù),從而實(shí)現(xiàn)setInterval的效果,而 setInterval 總是 10ms 執(zhí)行一次,而不管它的回調(diào)函數(shù)執(zhí)行多久。
所以,如果 setInterval 的回調(diào)函數(shù)執(zhí)行時(shí)間比你指定的間隔時(shí)間相等或者更長(zhǎng),那么其回調(diào)函數(shù)會(huì)連在一起執(zhí)行。
你可以試試運(yùn)行以下代碼:
var counter = 0; var initTime = new Date().getTime(); var timer = setInterval(function(){ if(counter===2){ clearInterval(timer); } if(counter === 0){ for(var i = 0; i < 1990000000; i++){ ; } } console.log("第"+counter+"次:" + (new Date().getTime() - initTime) + " ms"); counter++; },1000);
我電腦Chrome瀏覽器的輸入如下:
第0次:2007 ms 第1次:2013 ms 第2次:3008 ms瀏覽器
瀏覽器不是單線程的
上面說(shuō)了這么多關(guān)于JavaScript是單線程的,下面說(shuō)說(shuō)其宿主環(huán)境——瀏覽器。
瀏覽器的內(nèi)核是多線程 的,它們?cè)趦?nèi)核制控下相互配合以保持同步,一個(gè)瀏覽器至少實(shí)現(xiàn)三個(gè)常駐線程:
javascript引擎線程 javascript引擎是基于事件驅(qū)動(dòng)單線程執(zhí)行的,JS引擎一直等待著任務(wù)隊(duì)列中任務(wù)的到來(lái),然后加以處理,瀏覽器無(wú)論什么時(shí)候都只有一個(gè)JS線程在運(yùn)行JS程序。
GUI渲染線程 GUI渲染線程負(fù)責(zé)渲染瀏覽器界面,當(dāng)界面需要重繪(Repaint)或由于某種操作引發(fā)回流(reflow)時(shí),該線程就會(huì)執(zhí)行。但需要注意GUI渲染線程與JS引擎是互斥的,當(dāng)JS引擎執(zhí)行時(shí)GUI線程會(huì)被掛起,GUI更新會(huì)被保存在一個(gè)隊(duì)列中等到JS引擎空閑時(shí)立即被執(zhí)行。
瀏覽器事件觸發(fā)線程 事件觸發(fā)線程,當(dāng)一個(gè)事件被觸發(fā)時(shí)該線程會(huì)把事件添加到“任務(wù)隊(duì)列”的隊(duì)尾,等待JS引擎的處理。這些事件可來(lái)自JavaScript引擎當(dāng)前執(zhí)行的代碼塊如setTimeOut、也可來(lái)自瀏覽器內(nèi)核的其他線程如鼠標(biāo)點(diǎn)擊、AJAX異步請(qǐng)求等,但由于JS是單線程執(zhí)行的,所有這些事件都得排隊(duì)等待JS引擎處理。
在Chrome瀏覽器中,為了防止因一個(gè)標(biāo)簽頁(yè)奔潰而影響整個(gè)瀏覽器,其每個(gè)標(biāo)簽頁(yè)都是一個(gè) 進(jìn)程 。當(dāng)然,對(duì)于同一域名下的標(biāo)簽頁(yè)是能夠相互通訊的,具體可看 瀏覽器跨標(biāo)簽通訊 。在Chrome設(shè)計(jì)中存在很多的進(jìn)程,并利用進(jìn)程間通訊來(lái)完成它們之間的同步,因此這也是Chrome快速的法寶之一。對(duì)于Ajax的請(qǐng)求也需要特殊線程來(lái)執(zhí)行,當(dāng)需要發(fā)送一個(gè)Ajax請(qǐng)求時(shí),瀏覽器會(huì)開(kāi)辟一個(gè)新的線程來(lái)執(zhí)行HTTP的請(qǐng)求,它并不會(huì)阻塞JavaScript線程的執(zhí)行,當(dāng)HTTP請(qǐng)求狀態(tài)變更時(shí),相應(yīng)事件會(huì)被作為回調(diào)放入到“任務(wù)隊(duì)列”中等待被執(zhí)行。
看看以下代碼:
document.onclick = function(){ console.log("click") } for(var i = 0; i< 100000000; i++);
解釋一下代碼:首先向document注冊(cè)了一個(gè)click事件,然后就執(zhí)行了一段耗時(shí)的for循環(huán),在這段for循環(huán)結(jié)束前,你可以嘗試點(diǎn)擊頁(yè)面。當(dāng)耗時(shí)操作結(jié)束后,console控制臺(tái)就會(huì)輸出之前點(diǎn)擊事件的"click"語(yǔ)句。這視乎證明了點(diǎn)擊事件(也包括其它各種事件)是由額外多帶帶的線程觸發(fā)的,事件觸發(fā)后就會(huì)將回調(diào)函數(shù)放進(jìn)了“任務(wù)隊(duì)列”的末尾,等待著JavaScript主線程的執(zhí)行。
總結(jié)JavaScript是單線程的,同一時(shí)刻只能執(zhí)行特定的任務(wù)。而瀏覽器是多線程的。
異步任務(wù)(各種瀏覽器事件、定時(shí)器等)都是先添加到“任務(wù)隊(duì)列”(定時(shí)器則到達(dá)其指定參數(shù)時(shí))。當(dāng)Stack棧(JS主線程)為空時(shí),就會(huì)讀取Queue隊(duì)列(任務(wù)隊(duì)列)的第一個(gè)任務(wù)(隊(duì)首),然后執(zhí)行。
JavaScript為了避免復(fù)雜性,而實(shí)現(xiàn)單線程執(zhí)行。而今JavaScript卻變得越來(lái)越不簡(jiǎn)單了,當(dāng)然這也是JavaScript迷人的地方。
參考資料JavaScript運(yùn)行機(jī)制詳解:再談Event Loop
JavaScript單線程和瀏覽器事件循環(huán)
JavaScript單線程深入分析
也談setTimeout
單線程的JavaScript
原文鏈接: http://www.codeceo.com/article/javascript-threaded.html
原文作者:碼農(nóng)網(wǎng) – 劉健超
[ 原創(chuàng)作品,轉(zhuǎn)載必須在正文中標(biāo)注并保留原文鏈接和作者等信息。]
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/81001.html
摘要:廖雪峰老師的教程學(xué)習(xí)筆記錯(cuò)誤處理提供了像一樣的錯(cuò)誤處理機(jī)制,即例如其中不是必須的,也不是必須的,但二者必須有其一,其中是必定會(huì)被執(zhí)行的。其中其中函數(shù)將在超時(shí)后執(zhí)行。 廖雪峰老師的javascript教程學(xué)習(xí)筆記 1. 錯(cuò)誤處理 JavaScript 提供了像Java一樣的錯(cuò)誤處理機(jī)制,即try catch finally.例如: try{ var s = null; s...
摘要:網(wǎng)上有很多前端的學(xué)習(xí)路徑文章,大多是知識(shí)點(diǎn)羅列為主或是資料的匯總,數(shù)據(jù)量讓新人望而卻步。天了解一個(gè)前端框架。也可以關(guān)注微信公眾號(hào)曉舟報(bào)告,發(fā)送獲取資料,就能收到下載密碼,網(wǎng)盤地址在最下方,獲取教程和案例的資料。 前言 好的學(xué)習(xí)方法可以事半功倍,好的學(xué)習(xí)路徑可以指明前進(jìn)方向。這篇文章不僅要寫(xiě)學(xué)習(xí)路徑,還要寫(xiě)學(xué)習(xí)方法,還要發(fā)資料,干貨滿滿,準(zhǔn)備接招。 網(wǎng)上有很多前端的學(xué)習(xí)路徑文章,大多是知...
摘要:從異步過(guò)程的角度看,函數(shù)就是異步過(guò)程的發(fā)起函數(shù),事件監(jiān)聽(tīng)函數(shù)就是異步過(guò)程的回調(diào)函數(shù)。事件觸發(fā)時(shí),表示異步任務(wù)完成,會(huì)將事件監(jiān)聽(tīng)器函數(shù)封裝成一條消息放到消息隊(duì)列中,等待主線程執(zhí)行。 1.為什么JavaScript是單線程? JavaScript語(yǔ)言的一大特點(diǎn)就是單線程,也就是說(shuō),同一個(gè)時(shí)間只能做一件事。那么,為什么JavaScript不能有多個(gè)線程呢?這樣能提高效率啊。JavaScrip...
摘要:目前,中關(guān)村黑馬程序員訓(xùn)練營(yíng)已成長(zhǎng)為行業(yè)學(xué)員質(zhì)量好課程內(nèi)容深企業(yè)滿意的移動(dòng)開(kāi)發(fā)高端訓(xùn)練基地,并被評(píng)為中關(guān)村軟件園重點(diǎn)扶持人才企業(yè)。黑馬程序員的學(xué)員篩選制度,遠(yuǎn)比現(xiàn)在以上的企業(yè)招聘流程更為嚴(yán)格。系統(tǒng)的學(xué)習(xí)可以參考w3c的教程 web概念概述 * JavaWeb: * 使用Java語(yǔ)言開(kāi)發(fā)基于互聯(lián)網(wǎng)的項(xiàng)目 * 軟件架構(gòu): 1. C/S: Client/Server 客戶端/服務(wù)...
摘要:離線并發(fā)多個(gè)數(shù)據(jù)庫(kù)事務(wù)中支持多線程的各種應(yīng)用服務(wù)器并發(fā)問(wèn)題丟失更新同時(shí)編輯文件,相繼保存,最終丟失先保存者更新的內(nèi)容不一致性讀取期間,數(shù)據(jù)有更新執(zhí)行語(yǔ)境從與外界交互角度看的個(gè)語(yǔ)境請(qǐng)求對(duì)應(yīng)于軟件工作的外部環(huán)境發(fā)出的單個(gè)調(diào)用,處理請(qǐng)求的軟件會(huì)決 離線并發(fā):多個(gè)數(shù)據(jù)庫(kù)事務(wù)中支持多線程的各種應(yīng)用服務(wù)器 1. 并發(fā)問(wèn)題: 1)丟失更新(同時(shí)編輯文件,相繼保存,最終丟失先保存者更新的內(nèi)容) 2)不...
閱讀 2365·2023-04-25 14:29
閱讀 1522·2021-11-22 09:34
閱讀 2734·2021-11-22 09:34
閱讀 3413·2021-11-11 10:59
閱讀 1881·2021-09-26 09:46
閱讀 2262·2021-09-22 16:03
閱讀 1966·2019-08-30 12:56
閱讀 505·2019-08-30 11:12